summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java2
-rw-r--r--core/java/android/app/AppOpsManager.java147
-rw-r--r--core/java/android/app/AutomaticZenRule.java12
-rw-r--r--core/java/android/app/Instrumentation.java1
-rw-r--r--core/java/android/app/Notification.java26
-rw-r--r--core/java/android/app/OWNERS2
-rw-r--r--core/java/android/app/Presentation.java9
-rw-r--r--core/java/android/companion/virtual/flags/flags.aconfig7
-rw-r--r--core/java/android/hardware/display/DisplayManagerGlobal.java6
-rw-r--r--core/java/android/hardware/location/ContextHubManager.java7
-rw-r--r--core/java/android/service/notification/ZenModeConfig.java24
-rw-r--r--core/java/android/view/IWindowManager.aidl6
-rw-r--r--core/java/android/view/IWindowSession.aidl19
-rw-r--r--core/java/android/view/InsetsController.java37
-rw-r--r--core/java/android/view/ViewRootImpl.java7
-rw-r--r--core/java/android/view/ViewRootInsetsControllerHost.java14
-rw-r--r--core/java/android/view/WindowManager.java10
-rw-r--r--core/java/android/view/WindowlessWindowManager.java10
-rw-r--r--core/java/android/view/accessibility/flags/accessibility_flags.aconfig8
-rw-r--r--core/java/android/view/inputmethod/ImeTracker.java4
-rw-r--r--core/java/android/view/inputmethod/flags.aconfig7
-rw-r--r--core/java/android/widget/RemoteViewsAdapter.java2
-rw-r--r--core/java/android/widget/TextView.java10
-rw-r--r--core/java/android/window/DesktopModeFlags.java2
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig11
-rw-r--r--core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java4
-rw-r--r--core/java/com/android/internal/statusbar/IStatusBar.aidl2
-rw-r--r--core/res/res/values/dimens.xml3
-rw-r--r--core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java13
-rw-r--r--core/tests/coretests/src/android/view/InsetsControllerTest.java17
-rw-r--r--core/tests/coretests/src/android/view/ViewRootImplTest.java2
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt2
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt59
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml6
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml9
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt47
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt116
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt57
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java134
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt74
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt88
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt53
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt6
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt6
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt3
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt16
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt77
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt101
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java4
-rw-r--r--location/java/android/location/GnssMeasurement.java4
-rw-r--r--location/java/android/location/GnssMeasurementsEvent.java8
-rw-r--r--location/java/com/android/internal/location/GpsNetInitiatedHandler.java14
-rw-r--r--media/java/android/media/flags/projection.aconfig9
-rw-r--r--packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt18
-rw-r--r--packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt24
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java39
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java24
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java87
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java215
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java99
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java30
-rw-r--r--packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java10
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt8
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt11
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt18
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt39
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt113
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt82
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt3
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt7
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt91
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt11
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt3
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt3
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt24
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt10
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt2
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt2
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt4
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt4
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt4
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt10
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt9
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt56
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt55
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt74
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt35
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TilesAvailabilityInteractorTest.kt240
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt973
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt27
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt17
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt5
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java5
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt3
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java5
-rw-r--r--packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml27
-rw-r--r--packages/SystemUI/res/layout/notification_2025_info.xml365
-rw-r--r--packages/SystemUI/res/values/config.xml3
-rw-r--r--packages/SystemUI/res/values/dimens.xml10
-rw-r--r--packages/SystemUI/res/values/strings.xml7
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java1
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/shared/colors/SurfaceEffectColors.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt63
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java22
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/StubQSTileViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt119
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt265
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java14
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt)0
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt)0
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt10
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLoggerKosmos.kt11
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt9
-rw-r--r--ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java17
-rw-r--r--ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java7
-rw-r--r--ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java265
-rw-r--r--ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java83
-rw-r--r--ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java138
-rw-r--r--ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java68
-rw-r--r--ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java3
-rw-r--r--ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java8
-rw-r--r--ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java3
-rwxr-xr-xravenwood/scripts/add-annotations.sh2
-rw-r--r--ravenwood/tests/coretest/Android.bp31
-rw-r--r--ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java82
-rw-r--r--ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java63
-rw-r--r--ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java65
-rw-r--r--ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java13
-rw-r--r--ravenwood/texts/ravenwood-build.prop36
-rw-r--r--ravenwood/texts/ravenwood-services-jarjar-rules.txt4
-rw-r--r--services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java62
-rw-r--r--services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java5
-rw-r--r--services/core/java/com/android/server/StorageManagerService.java37
-rw-r--r--services/core/java/com/android/server/SystemTimeZone.java3
-rw-r--r--services/core/java/com/android/server/TelephonyRegistry.java12
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java2
-rw-r--r--services/core/java/com/android/server/am/BroadcastQueueImpl.java4
-rw-r--r--services/core/java/com/android/server/am/BroadcastSkipPolicy.java202
-rw-r--r--services/core/java/com/android/server/am/broadcasts_flags.aconfig11
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java10
-rw-r--r--services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java137
-rw-r--r--services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java207
-rw-r--r--services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java8
-rw-r--r--services/core/java/com/android/server/notification/ConditionProviders.java13
-rw-r--r--services/core/java/com/android/server/notification/ZenConfigTrimmer.java109
-rw-r--r--services/core/java/com/android/server/notification/ZenModeHelper.java15
-rw-r--r--services/core/java/com/android/server/notification/flags.aconfig10
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java26
-rw-r--r--services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java4
-rw-r--r--services/core/java/com/android/server/statusbar/StatusBarManagerService.java4
-rw-r--r--services/core/java/com/android/server/storage/WatchedVolumeInfo.java4
-rw-r--r--services/core/java/com/android/server/vibrator/VendorVibrationSession.java279
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java5
-rw-r--r--services/core/java/com/android/server/wm/BackgroundActivityStartController.java196
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeHelper.java2
-rw-r--r--services/core/java/com/android/server/wm/DisplayContent.java24
-rw-r--r--services/core/java/com/android/server/wm/DisplayPolicy.java2
-rw-r--r--services/core/java/com/android/server/wm/InsetsControlTarget.java14
-rw-r--r--services/core/java/com/android/server/wm/InsetsPolicy.java7
-rw-r--r--services/core/java/com/android/server/wm/InsetsStateController.java7
-rw-r--r--services/core/java/com/android/server/wm/PresentationController.java224
-rw-r--r--services/core/java/com/android/server/wm/Session.java22
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java38
-rw-r--r--services/core/java/com/android/server/wm/WindowState.java37
-rw-r--r--services/java/com/android/server/SystemServer.java2
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java133
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java52
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java46
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java305
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java11
-rw-r--r--services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java76
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java14
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java124
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java41
-rw-r--r--services/tests/wmtests/src/com/android/server/TransitionSubject.java79
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java48
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java131
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java8
-rw-r--r--telephony/java/android/telephony/euicc/EuiccManager.java9
-rw-r--r--tools/processors/view_inspector/OWNERS3
240 files changed, 6562 insertions, 2402 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 0298c1e627ee..251776e907d8 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -2995,6 +2995,8 @@ public class AlarmManagerService extends SystemService {
pw.print(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS,
Flags.startUserBeforeScheduledAlarms());
pw.println();
+ pw.print(Flags.FLAG_ACQUIRE_WAKELOCK_BEFORE_SEND, Flags.acquireWakelockBeforeSend());
+ pw.println();
pw.decreaseIndent();
pw.println();
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 458c1715d2b6..248f191cb8b8 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1651,9 +1651,65 @@ public class AppOpsManager {
/** @hide Similar to {@link OP_CONTROL_AUDIO}, but doesn't require capabilities. */
public static final int OP_CONTROL_AUDIO_PARTIAL = AppOpEnums.APP_OP_CONTROL_AUDIO_PARTIAL;
+ /**
+ * Access coarse eye tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_EYE_TRACKING_COARSE =
+ AppOpEnums.APP_OP_EYE_TRACKING_COARSE;
+
+ /**
+ * Access fine eye tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_EYE_TRACKING_FINE =
+ AppOpEnums.APP_OP_EYE_TRACKING_FINE;
+
+ /**
+ * Access face tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_FACE_TRACKING =
+ AppOpEnums.APP_OP_FACE_TRACKING;
+
+ /**
+ * Access hand tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_HAND_TRACKING =
+ AppOpEnums.APP_OP_HAND_TRACKING;
+
+ /**
+ * Access head tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_HEAD_TRACKING =
+ AppOpEnums.APP_OP_HEAD_TRACKING;
+
+ /**
+ * Access coarse scene tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_SCENE_UNDERSTANDING_COARSE =
+ AppOpEnums.APP_OP_SCENE_UNDERSTANDING_COARSE;
+
+ /**
+ * Access fine scene tracking data.
+ *
+ * @hide
+ */
+ public static final int OP_SCENE_UNDERSTANDING_FINE =
+ AppOpEnums.APP_OP_SCENE_UNDERSTANDING_FINE;
+
/** @hide */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public static final int _NUM_OP = 156;
+ public static final int _NUM_OP = 163;
/**
* All app ops represented as strings.
@@ -1813,6 +1869,13 @@ public class AppOpsManager {
OPSTR_WRITE_SYSTEM_PREFERENCES,
OPSTR_CONTROL_AUDIO,
OPSTR_CONTROL_AUDIO_PARTIAL,
+ OPSTR_EYE_TRACKING_COARSE,
+ OPSTR_EYE_TRACKING_FINE,
+ OPSTR_FACE_TRACKING,
+ OPSTR_HAND_TRACKING,
+ OPSTR_HEAD_TRACKING,
+ OPSTR_SCENE_UNDERSTANDING_COARSE,
+ OPSTR_SCENE_UNDERSTANDING_FINE,
})
public @interface AppOpString {}
@@ -2579,6 +2642,36 @@ public class AppOpsManager {
/** @hide Access to a audio playback and control APIs without capability requirements */
public static final String OPSTR_CONTROL_AUDIO_PARTIAL = "android:control_audio_partial";
+ /** @hide Access coarse eye tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_EYE_TRACKING_COARSE = "android:eye_tracking_coarse";
+
+ /** @hide Access fine eye tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_EYE_TRACKING_FINE = "android:eye_tracking_fine";
+
+ /** @hide Access face tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_FACE_TRACKING = "android:face_tracking";
+
+ /** @hide Access hand tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_HAND_TRACKING = "android:hand_tracking";
+
+ /** @hide Access head tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_HEAD_TRACKING = "android:head_tracking";
+
+ /** @hide Access coarse scene tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_SCENE_UNDERSTANDING_COARSE =
+ "android:scene_understanding_coarse";
+
+ /** @hide Access fine scene tracking data. */
+ @FlaggedApi(android.xr.Flags.FLAG_XR_MANIFEST_ENTRIES)
+ public static final String OPSTR_SCENE_UNDERSTANDING_FINE =
+ "android:scene_understanding_fine";
+
/** {@link #sAppOpsToNote} not initialized yet for this op */
private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0;
/** Should not collect noting of this app-op in {@link #sAppOpsToNote} */
@@ -2657,6 +2750,14 @@ public class AppOpsManager {
Flags.replaceBodySensorPermissionEnabled() ? OP_READ_HEART_RATE : OP_NONE,
Flags.replaceBodySensorPermissionEnabled() ? OP_READ_SKIN_TEMPERATURE : OP_NONE,
Flags.replaceBodySensorPermissionEnabled() ? OP_READ_OXYGEN_SATURATION : OP_NONE,
+ // Android XR
+ android.xr.Flags.xrManifestEntries() ? OP_EYE_TRACKING_COARSE : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_EYE_TRACKING_FINE : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_FACE_TRACKING : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_HAND_TRACKING : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_HEAD_TRACKING : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_SCENE_UNDERSTANDING_COARSE : OP_NONE,
+ android.xr.Flags.xrManifestEntries() ? OP_SCENE_UNDERSTANDING_FINE : OP_NONE,
};
/**
@@ -3192,6 +3293,41 @@ public class AppOpsManager {
"CONTROL_AUDIO").setDefaultMode(AppOpsManager.MODE_FOREGROUND).build(),
new AppOpInfo.Builder(OP_CONTROL_AUDIO_PARTIAL, OPSTR_CONTROL_AUDIO_PARTIAL,
"CONTROL_AUDIO_PARTIAL").setDefaultMode(AppOpsManager.MODE_FOREGROUND).build(),
+ new AppOpInfo.Builder(OP_EYE_TRACKING_COARSE, OPSTR_EYE_TRACKING_COARSE,
+ "EYE_TRACKING_COARSE")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.EYE_TRACKING_COARSE : null)
+ .build(),
+ new AppOpInfo.Builder(OP_EYE_TRACKING_FINE, OPSTR_EYE_TRACKING_FINE,
+ "EYE_TRACKING_FINE")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.EYE_TRACKING_FINE : null)
+ .build(),
+ new AppOpInfo.Builder(OP_FACE_TRACKING, OPSTR_FACE_TRACKING,
+ "FACE_TRACKING")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.FACE_TRACKING : null)
+ .build(),
+ new AppOpInfo.Builder(OP_HAND_TRACKING, OPSTR_HAND_TRACKING,
+ "HAND_TRACKING")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.HAND_TRACKING : null)
+ .build(),
+ new AppOpInfo.Builder(OP_HEAD_TRACKING, OPSTR_HEAD_TRACKING,
+ "HEAD_TRACKING")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.HEAD_TRACKING : null)
+ .build(),
+ new AppOpInfo.Builder(OP_SCENE_UNDERSTANDING_COARSE, OPSTR_SCENE_UNDERSTANDING_COARSE,
+ "SCENE_UNDERSTANDING_COARSE")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.SCENE_UNDERSTANDING_COARSE : null)
+ .build(),
+ new AppOpInfo.Builder(OP_SCENE_UNDERSTANDING_FINE, OPSTR_SCENE_UNDERSTANDING_FINE,
+ "SCENE_UNDERSTANDING_FINE")
+ .setPermission(android.xr.Flags.xrManifestEntries()
+ ? Manifest.permission.SCENE_UNDERSTANDING_FINE : null)
+ .build(),
};
// The number of longs needed to form a full bitmask of app ops
@@ -3301,6 +3437,15 @@ public class AppOpsManager {
}
/**
+ * Returns whether the provided {@code op} is a valid op code or not.
+ *
+ * @hide
+ */
+ public static boolean isValidOp(int op) {
+ return op >= 0 && op < sAppOpInfos.length;
+ }
+
+ /**
* @hide
*/
public static int strDebugOpToOp(String op) {
diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java
index fa977c93113a..2daa52b47102 100644
--- a/core/java/android/app/AutomaticZenRule.java
+++ b/core/java/android/app/AutomaticZenRule.java
@@ -228,7 +228,7 @@ public final class AutomaticZenRule implements Parcelable {
public AutomaticZenRule(Parcel source) {
enabled = source.readInt() == ENABLED;
if (source.readInt() == ENABLED) {
- name = getTrimmedString(source.readString());
+ name = getTrimmedString(source.readString8());
}
interruptionFilter = source.readInt();
conditionId = getTrimmedUri(source.readParcelable(null, android.net.Uri.class));
@@ -238,11 +238,11 @@ public final class AutomaticZenRule implements Parcelable {
source.readParcelable(null, android.content.ComponentName.class));
creationTime = source.readLong();
mZenPolicy = source.readParcelable(null, ZenPolicy.class);
- mPkg = source.readString();
+ mPkg = source.readString8();
mDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
mAllowManualInvocation = source.readBoolean();
mIconResId = source.readInt();
- mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH);
+ mTriggerDescription = getTrimmedString(source.readString8(), MAX_DESC_LENGTH);
mType = source.readInt();
}
@@ -514,7 +514,7 @@ public final class AutomaticZenRule implements Parcelable {
dest.writeInt(enabled ? ENABLED : DISABLED);
if (name != null) {
dest.writeInt(1);
- dest.writeString(name);
+ dest.writeString8(name);
} else {
dest.writeInt(0);
}
@@ -524,11 +524,11 @@ public final class AutomaticZenRule implements Parcelable {
dest.writeParcelable(configurationActivity, 0);
dest.writeLong(creationTime);
dest.writeParcelable(mZenPolicy, 0);
- dest.writeString(mPkg);
+ dest.writeString8(mPkg);
dest.writeParcelable(mDeviceEffects, 0);
dest.writeBoolean(mAllowManualInvocation);
dest.writeInt(mIconResId);
- dest.writeString(mTriggerDescription);
+ dest.writeString8(mTriggerDescription);
dest.writeInt(mType);
}
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index eb9feb95bf3d..8af5b1bd40f8 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -189,6 +189,7 @@ public class Instrumentation {
* @param arguments Any additional arguments that were supplied when the
* instrumentation was started.
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public void onCreate(Bundle arguments) {
}
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 719e4389d92d..1b71e73db852 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -25,6 +25,9 @@ import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICO
import static android.app.admin.DevicePolicyResources.UNDEFINED;
import static android.graphics.drawable.Icon.TYPE_URI;
import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP;
+import static android.util.TypedValue.COMPLEX_UNIT_PX;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static java.util.Objects.requireNonNull;
@@ -6001,6 +6004,8 @@ public class Notification implements Parcelable
contentView.setViewVisibility(p.mTextViewId, View.GONE);
contentView.setTextViewText(p.mTextViewId, null);
}
+
+ updateExpanderAlignment(contentView, p, hasSecondLine);
setHeaderlessVerticalMargins(contentView, p, hasSecondLine);
// Update margins to leave space for the top line (but not for headerless views like
@@ -6010,12 +6015,29 @@ public class Notification implements Parcelable
int margin = getContentMarginTop(mContext,
R.dimen.notification_2025_content_margin_top);
contentView.setViewLayoutMargin(R.id.notification_main_column,
- RemoteViews.MARGIN_TOP, margin, TypedValue.COMPLEX_UNIT_PX);
+ RemoteViews.MARGIN_TOP, margin, COMPLEX_UNIT_PX);
}
return contentView;
}
+ private static void updateExpanderAlignment(RemoteViews contentView,
+ StandardTemplateParams p, boolean hasSecondLine) {
+ if (notificationsRedesignTemplates() && p.mHeaderless) {
+ if (!hasSecondLine) {
+ // If there's no text, let's center the expand button vertically to align things
+ // more nicely. This is handled separately for notifications that use a
+ // NotificationHeaderView, see NotificationHeaderView#centerTopLine.
+ contentView.setViewLayoutHeight(R.id.expand_button, MATCH_PARENT,
+ COMPLEX_UNIT_PX);
+ } else {
+ // Otherwise, just use the default height for the button to keep it top-aligned.
+ contentView.setViewLayoutHeight(R.id.expand_button, WRAP_CONTENT,
+ COMPLEX_UNIT_PX);
+ }
+ }
+ }
+
private static void setHeaderlessVerticalMargins(RemoteViews contentView,
StandardTemplateParams p, boolean hasSecondLine) {
if (Flags.notificationsRedesignTemplates() || !p.mHeaderless) {
@@ -9560,7 +9582,7 @@ public class Notification implements Parcelable
int marginStart = res.getDimensionPixelSize(
R.dimen.notification_2025_content_margin_start);
contentView.setViewLayoutMargin(R.id.title,
- RemoteViews.MARGIN_START, marginStart, TypedValue.COMPLEX_UNIT_PX);
+ RemoteViews.MARGIN_START, marginStart, COMPLEX_UNIT_PX);
}
if (isLegacyHeaderless) {
// Collapsed legacy messaging style has a 1-line limit.
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 7a811a1cdfb8..5b0cf1158d99 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -132,7 +132,7 @@ per-file Window* = file:/services/core/java/com/android/server/wm/OWNERS
per-file ConfigurationController.java = file:/services/core/java/com/android/server/wm/OWNERS
per-file *ScreenCapture* = file:/services/core/java/com/android/server/wm/OWNERS
per-file ComponentOptions.java = file:/services/core/java/com/android/server/wm/OWNERS
-
+per-file Presentation.java = file:/services/core/java/com/android/server/wm/OWNERS
# Multitasking
per-file multitasking.aconfig = file:/services/core/java/com/android/server/wm/OWNERS
diff --git a/core/java/android/app/Presentation.java b/core/java/android/app/Presentation.java
index bdab39dcd2ac..f39e2dd8cfa2 100644
--- a/core/java/android/app/Presentation.java
+++ b/core/java/android/app/Presentation.java
@@ -20,6 +20,8 @@ import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
+import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
+
import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
@@ -34,6 +36,8 @@ import android.view.ContextThemeWrapper;
import android.view.Display;
import android.view.Gravity;
import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams.WindowType;
@@ -277,6 +281,11 @@ public class Presentation extends Dialog {
@Override
public void show() {
super.show();
+
+ WindowInsetsController controller = getWindow().getInsetsController();
+ if (controller != null && enablePresentationForConnectedDisplays()) {
+ controller.hide(WindowInsets.Type.systemBars());
+ }
}
/**
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 67ade79e1b94..0085e4f42397 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -143,3 +143,10 @@ flag {
is_fixed_read_only: true
bug: "370928384"
}
+
+flag {
+ name: "device_aware_settings_override"
+ namespace: "virtual_devices"
+ description: "Settings override for virtual devices"
+ bug: "371801645"
+}
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index c4af87116eed..bebca57125b6 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -1499,7 +1499,7 @@ public final class DisplayManagerGlobal {
}
@VisibleForTesting
- static final class DisplayListenerDelegate {
+ public static final class DisplayListenerDelegate {
public final DisplayListener mListener;
public volatile long mInternalEventFlagsMask;
@@ -1536,7 +1536,7 @@ public final class DisplayManagerGlobal {
}
@VisibleForTesting
- boolean isEventFilterExplicit() {
+ public boolean isEventFilterExplicit() {
return mIsEventFilterExplicit;
}
@@ -1892,7 +1892,7 @@ public final class DisplayManagerGlobal {
}
@VisibleForTesting
- CopyOnWriteArrayList<DisplayListenerDelegate> getDisplayListeners() {
+ public CopyOnWriteArrayList<DisplayListenerDelegate> getDisplayListeners() {
return mDisplayListeners;
}
}
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 953ee08800cf..5b5360e1ff01 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -485,6 +485,9 @@ public final class ContextHubManager {
/**
* Returns the list of ContextHubInfo objects describing the available Context Hubs.
*
+ * To find the list of hubs that include all Hubs (including both Context Hubs and Vendor Hubs),
+ * use the {@link #getHubs()} method instead.
+ *
* @return the list of ContextHubInfo objects
*
* @see ContextHubInfo
@@ -499,8 +502,8 @@ public final class ContextHubManager {
}
/**
- * Returns the list of HubInfo objects describing the available hubs (including ContextHub and
- * VendorHub). This method is primarily used for debugging purposes as most clients care about
+ * Returns the list of HubInfo objects describing the available hubs (including Context Hubs and
+ * Vendor Hubs). This method is primarily used for debugging purposes as most clients care about
* endpoints and services more than hubs.
*
* @return the list of HubInfo objects
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 4cbd5beb3a8c..1cf43d455be8 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -2636,7 +2636,7 @@ public class ZenModeConfig implements Parcelable {
enabled = source.readInt() == 1;
snoozing = source.readInt() == 1;
if (source.readInt() == 1) {
- name = source.readString();
+ name = source.readString8();
}
zenMode = source.readInt();
conditionId = source.readParcelable(null, android.net.Uri.class);
@@ -2644,18 +2644,18 @@ public class ZenModeConfig implements Parcelable {
component = source.readParcelable(null, android.content.ComponentName.class);
configurationActivity = source.readParcelable(null, android.content.ComponentName.class);
if (source.readInt() == 1) {
- id = source.readString();
+ id = source.readString8();
}
creationTime = source.readLong();
if (source.readInt() == 1) {
- enabler = source.readString();
+ enabler = source.readString8();
}
zenPolicy = source.readParcelable(null, android.service.notification.ZenPolicy.class);
zenDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
- pkg = source.readString();
+ pkg = source.readString8();
allowManualInvocation = source.readBoolean();
- iconResName = source.readString();
- triggerDescription = source.readString();
+ iconResName = source.readString8();
+ triggerDescription = source.readString8();
type = source.readInt();
userModifiedFields = source.readInt();
zenPolicyUserModifiedFields = source.readInt();
@@ -2703,7 +2703,7 @@ public class ZenModeConfig implements Parcelable {
dest.writeInt(snoozing ? 1 : 0);
if (name != null) {
dest.writeInt(1);
- dest.writeString(name);
+ dest.writeString8(name);
} else {
dest.writeInt(0);
}
@@ -2714,23 +2714,23 @@ public class ZenModeConfig implements Parcelable {
dest.writeParcelable(configurationActivity, 0);
if (id != null) {
dest.writeInt(1);
- dest.writeString(id);
+ dest.writeString8(id);
} else {
dest.writeInt(0);
}
dest.writeLong(creationTime);
if (enabler != null) {
dest.writeInt(1);
- dest.writeString(enabler);
+ dest.writeString8(enabler);
} else {
dest.writeInt(0);
}
dest.writeParcelable(zenPolicy, 0);
dest.writeParcelable(zenDeviceEffects, 0);
- dest.writeString(pkg);
+ dest.writeString8(pkg);
dest.writeBoolean(allowManualInvocation);
- dest.writeString(iconResName);
- dest.writeString(triggerDescription);
+ dest.writeString8(iconResName);
+ dest.writeString8(triggerDescription);
dest.writeInt(type);
dest.writeInt(userModifiedFields);
dest.writeInt(zenPolicyUserModifiedFields);
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index f58baffb1367..4fc894ca9ff4 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -789,6 +789,12 @@ interface IWindowManager
in @nullable ImeTracker.Token statsToken);
/**
+ * Updates the currently animating insets types of a remote process.
+ */
+ @EnforcePermission("MANAGE_APP_TOKENS")
+ void updateDisplayWindowAnimatingTypes(int displayId, int animatingTypes);
+
+ /**
* Called to get the expected window insets.
*
* @return {@code true} if system bars are always consumed.
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 1f8f0820ca3a..7d6d5a269b4c 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -272,6 +272,15 @@ interface IWindowSession {
in @nullable ImeTracker.Token imeStatsToken);
/**
+ * Notifies WindowState what insets types are currently running within the Window.
+ * see {@link com.android.server.wm.WindowState#mInsetsAnimationRunning).
+ *
+ * @param window The window that is insets animaiton is running.
+ * @param animatingTypes Indicates the currently animating insets types.
+ */
+ oneway void updateAnimatingTypes(IWindow window, int animatingTypes);
+
+ /**
* Called when the system gesture exclusion has changed.
*/
oneway void reportSystemGestureExclusionChanged(IWindow window, in List<Rect> exclusionRects);
@@ -372,14 +381,4 @@ interface IWindowSession {
*/
oneway void notifyImeWindowVisibilityChangedFromClient(IWindow window, boolean visible,
in ImeTracker.Token statsToken);
-
- /**
- * Notifies WindowState whether inset animations are currently running within the Window.
- * This value is used by the server to vote for refresh rate.
- * see {@link com.android.server.wm.WindowState#mInsetsAnimationRunning).
- *
- * @param window The window that is insets animaiton is running.
- * @param running Indicates the insets animation state.
- */
- oneway void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running);
}
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 0d82acd2bdf0..462c5c630759 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -211,12 +211,12 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
}
/**
- * Notifies when the state of running animation is changed. The state is either "running" or
- * "idle".
+ * Notifies when the insets types of running animation have changed. The animatingTypes
+ * contain all types, which have an ongoing animation.
*
- * @param running {@code true} if there is any animation running; {@code false} otherwise.
+ * @param animatingTypes the {@link InsetsType}s that are currently animating
*/
- default void notifyAnimationRunningStateChanged(boolean running) {}
+ default void updateAnimatingTypes(@InsetsType int animatingTypes) {}
/** @see ViewRootImpl#isHandlingPointerEvent */
default boolean isHandlingPointerEvent() {
@@ -665,6 +665,9 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
/** Set of inset types which are requested visible which are reported to the host */
private @InsetsType int mReportedRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
+ /** Set of insets types which are currently animating */
+ private @InsetsType int mAnimatingTypes = 0;
+
/** Set of inset types that we have controls of */
private @InsetsType int mControllableTypes;
@@ -745,9 +748,10 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
mFrame, mFromState, mToState, RESIZE_INTERPOLATOR,
ANIMATION_DURATION_RESIZE, mTypes, InsetsController.this);
if (mRunningAnimations.isEmpty()) {
- mHost.notifyAnimationRunningStateChanged(true);
+ mHost.updateAnimatingTypes(runner.getTypes());
}
mRunningAnimations.add(new RunningAnimation(runner, runner.getAnimationType()));
+ mAnimatingTypes |= runner.getTypes();
}
};
@@ -1564,9 +1568,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
}
}
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_CLIENT_ANIMATION_RUNNING);
- if (mRunningAnimations.isEmpty()) {
- mHost.notifyAnimationRunningStateChanged(true);
- }
+ mAnimatingTypes |= runner.getTypes();
+ mHost.updateAnimatingTypes(mAnimatingTypes);
mRunningAnimations.add(new RunningAnimation(runner, animationType));
if (DEBUG) Log.d(TAG, "Animation added to runner. useInsetsAnimationThread: "
+ useInsetsAnimationThread);
@@ -1827,7 +1830,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
dispatchAnimationEnd(runningAnimation.runner.getAnimation());
} else {
if (Flags.refactorInsetsController()) {
- if (removedTypes == ime()
+ if ((removedTypes & ime()) != 0
&& control.getAnimationType() == ANIMATION_TYPE_HIDE) {
if (mHost != null) {
// if the (hide) animation is cancelled, the
@@ -1842,9 +1845,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
break;
}
}
- if (mRunningAnimations.isEmpty()) {
- mHost.notifyAnimationRunningStateChanged(false);
+ if (removedTypes > 0) {
+ mAnimatingTypes &= ~removedTypes;
+ mHost.updateAnimatingTypes(mAnimatingTypes);
}
+
onAnimationStateChanged(removedTypes, false /* running */);
}
@@ -1969,14 +1974,6 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
return animatingTypes;
}
- private @InsetsType int computeAnimatingTypes() {
- int animatingTypes = 0;
- for (int i = 0; i < mRunningAnimations.size(); i++) {
- animatingTypes |= mRunningAnimations.get(i).runner.getTypes();
- }
- return animatingTypes;
- }
-
/**
* Called when finishing setting requested visible types or finishing setting controls.
*
@@ -1989,7 +1986,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
// report its requested visibility at the end of the animation, otherwise we would
// lose the leash, and it would disappear during the animation
// TODO(b/326377046) revisit this part and see if we can make it more general
- typesToReport = mRequestedVisibleTypes | (computeAnimatingTypes() & ime());
+ typesToReport = mRequestedVisibleTypes | (mAnimatingTypes & ime());
} else {
typesToReport = mRequestedVisibleTypes;
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 9498407fb33b..7fd7be8585a4 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2540,11 +2540,12 @@ public final class ViewRootImpl implements ViewParent,
}
/**
- * Notify the when the running state of a insets animation changed.
+ * Notify the when the animating insets types have changed.
*/
@VisibleForTesting
- public void notifyInsetsAnimationRunningStateChanged(boolean running) {
+ public void updateAnimatingTypes(@InsetsType int animatingTypes) {
if (sToolkitSetFrameRateReadOnlyFlagValue) {
+ boolean running = animatingTypes != 0;
if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
Trace.instant(Trace.TRACE_TAG_VIEW,
TextUtils.formatSimple("notifyInsetsAnimationRunningStateChanged(%s)",
@@ -2552,7 +2553,7 @@ public final class ViewRootImpl implements ViewParent,
}
mInsetsAnimationRunning = running;
try {
- mWindowSession.notifyInsetsAnimationRunningStateChanged(mWindow, running);
+ mWindowSession.updateAnimatingTypes(mWindow, animatingTypes);
} catch (RemoteException e) {
}
}
diff --git a/core/java/android/view/ViewRootInsetsControllerHost.java b/core/java/android/view/ViewRootInsetsControllerHost.java
index 889acca4b8b1..8954df6b1aaa 100644
--- a/core/java/android/view/ViewRootInsetsControllerHost.java
+++ b/core/java/android/view/ViewRootInsetsControllerHost.java
@@ -171,6 +171,13 @@ public class ViewRootInsetsControllerHost implements InsetsController.Host {
}
@Override
+ public void updateAnimatingTypes(@WindowInsets.Type.InsetsType int animatingTypes) {
+ if (mViewRoot != null) {
+ mViewRoot.updateAnimatingTypes(animatingTypes);
+ }
+ }
+
+ @Override
public boolean hasAnimationCallbacks() {
if (mViewRoot.mView == null) {
return false;
@@ -275,13 +282,6 @@ public class ViewRootInsetsControllerHost implements InsetsController.Host {
}
@Override
- public void notifyAnimationRunningStateChanged(boolean running) {
- if (mViewRoot != null) {
- mViewRoot.notifyInsetsAnimationRunningStateChanged(running);
- }
- }
-
- @Override
public boolean isHandlingPointerEvent() {
return mViewRoot != null && mViewRoot.isHandlingPointerEvent();
}
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 24647f459ab5..196ae5e59fa7 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -625,6 +625,12 @@ public interface WindowManager extends ViewManager {
int TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH = (1 << 14); // 0x4000
/**
+ * Transition flag: Indicates that aod is showing hidden by entering doze
+ * @hide
+ */
+ int TRANSIT_FLAG_AOD_APPEARING = (1 << 15); // 0x8000
+
+ /**
* @hide
*/
@IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = {
@@ -643,6 +649,7 @@ public interface WindowManager extends ViewManager {
TRANSIT_FLAG_KEYGUARD_OCCLUDING,
TRANSIT_FLAG_KEYGUARD_UNOCCLUDING,
TRANSIT_FLAG_PHYSICAL_DISPLAY_SWITCH,
+ TRANSIT_FLAG_AOD_APPEARING,
})
@Retention(RetentionPolicy.SOURCE)
@interface TransitionFlags {}
@@ -659,7 +666,8 @@ public interface WindowManager extends ViewManager {
(TRANSIT_FLAG_KEYGUARD_GOING_AWAY
| TRANSIT_FLAG_KEYGUARD_APPEARING
| TRANSIT_FLAG_KEYGUARD_OCCLUDING
- | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING);
+ | TRANSIT_FLAG_KEYGUARD_UNOCCLUDING
+ | TRANSIT_FLAG_AOD_APPEARING);
/**
* Remove content mode: Indicates remove content mode is currently not defined.
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index 72a595d95ec2..0a86ff89c53c 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -597,6 +597,11 @@ public class WindowlessWindowManager implements IWindowSession {
}
@Override
+ public void updateAnimatingTypes(IWindow window, @InsetsType int animatingTypes) {
+ // NO-OP
+ }
+
+ @Override
public void reportSystemGestureExclusionChanged(android.view.IWindow window,
List<Rect> exclusionRects) {
}
@@ -679,11 +684,6 @@ public class WindowlessWindowManager implements IWindowSession {
@NonNull ImeTracker.Token statsToken) {
}
- @Override
- public void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running) {
- // NO-OP
- }
-
void setParentInterface(@Nullable ISurfaceControlViewHostParent parentInterface) {
IBinder oldInterface = mParentInterface == null ? null : mParentInterface.asBinder();
IBinder newInterface = parentInterface == null ? null : parentInterface.asBinder();
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 49a11cab1de9..80a9cbc2f859 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -235,6 +235,14 @@ flag {
}
flag {
+ name: "request_rectangle_with_source"
+ namespace: "accessibility"
+ description: "Request rectangle on screen with source parameter"
+ bug: "391877896"
+ is_exported: true
+}
+
+flag {
name: "restore_a11y_secure_settings_on_hsum_device"
namespace: "accessibility"
description: "Grab the a11y settings and send the settings restored broadcast for current visible foreground user"
diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
index aa0111a13b8e..60178cde249f 100644
--- a/core/java/android/view/inputmethod/ImeTracker.java
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -225,6 +225,7 @@ public interface ImeTracker {
PHASE_SERVER_UPDATE_CLIENT_VISIBILITY,
PHASE_WM_DISPLAY_IME_CONTROLLER_SET_IME_REQUESTED_VISIBLE,
PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES,
+ PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED,
})
@Retention(RetentionPolicy.SOURCE)
@interface Phase {}
@@ -445,6 +446,9 @@ public interface ImeTracker {
/** The control target reported its requestedVisibleTypes back to WindowManagerService. */
int PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES =
ImeProtoEnums.PHASE_WM_UPDATE_DISPLAY_WINDOW_REQUESTED_VISIBLE_TYPES;
+ /** The requestedVisibleTypes have not been changed, so this request is not continued. */
+ int PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED =
+ ImeProtoEnums.PHASE_WM_REQUESTED_VISIBLE_TYPES_NOT_CHANGED;
/**
* Called when an IME request is started.
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index 16f41146fd6b..c81c2bbc2f27 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -196,3 +196,10 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "report_animating_insets_types"
+ namespace: "input_method"
+ description: "Adding animating insets types and report IME visibility at the beginning of hiding"
+ bug: "393049691"
+}
diff --git a/core/java/android/widget/RemoteViewsAdapter.java b/core/java/android/widget/RemoteViewsAdapter.java
index 118edc29f378..fa7b74f37ed0 100644
--- a/core/java/android/widget/RemoteViewsAdapter.java
+++ b/core/java/android/widget/RemoteViewsAdapter.java
@@ -242,7 +242,7 @@ public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback
@Override
public void onNullBinding(ComponentName name) {
- enqueueDeferredUnbindServiceMessage();
+ unbindNow();
}
@Override
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 99fe0cbdca25..5e828ba46df7 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -5211,7 +5211,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
*/
@Nullable
public String getFontVariationSettings() {
- return mTextPaint.getFontVariationSettings();
+ if (Flags.typefaceRedesignReadonly()) {
+ return mTextPaint.getFontVariationOverride();
+ } else {
+ return mTextPaint.getFontVariationSettings();
+ }
}
/**
@@ -5567,10 +5571,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
Math.clamp(400 + mFontWeightAdjustment,
FontStyle.FONT_WEIGHT_MIN, FontStyle.FONT_WEIGHT_MAX)));
}
- mTextPaint.setFontVariationSettings(
+ mTextPaint.setFontVariationOverride(
FontVariationAxis.toFontVariationSettings(axes));
} else {
- mTextPaint.setFontVariationSettings(fontVariationSettings);
+ mTextPaint.setFontVariationOverride(fontVariationSettings);
}
effective = true;
} else {
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 4aeedbb72903..42bf6d150bb4 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -97,6 +97,8 @@ public enum DesktopModeFlags {
ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity,
true),
ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD(Flags::enableDragResizeSetUpInBgThread, false),
+ ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX(
+ Flags::enableDragToDesktopIncomingTransitionsBugfix, false),
ENABLE_FULLY_IMMERSIVE_IN_DESKTOP(Flags::enableFullyImmersiveInDesktop, true),
ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true),
ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true),
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 2e36e9a205bf..684f320162c0 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -811,4 +811,13 @@ flag {
metadata {
purpose: PURPOSE_BUGFIX
}
-} \ No newline at end of file
+}
+flag {
+ name: "enable_drag_to_desktop_incoming_transitions_bugfix"
+ namespace: "lse_desktop_experience"
+ description: "Enables bugfix handling incoming transitions during the DragToDesktop transition."
+ bug: "397135730"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java b/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
index 69c04807c604..7ee22f30ace0 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedComponentImpl.java
@@ -157,7 +157,7 @@ public abstract class ParsedComponentImpl implements ParsedComponent, Parcelable
@Override
public void writeToParcel(Parcel dest, int flags) {
- sForInternedString.parcel(this.name, dest, flags);
+ dest.writeString(this.name);
dest.writeInt(this.getIcon());
dest.writeInt(this.getLabelRes());
dest.writeCharSequence(this.getNonLocalizedLabel());
@@ -175,7 +175,7 @@ public abstract class ParsedComponentImpl implements ParsedComponent, Parcelable
// We use the boot classloader for all classes that we load.
final ClassLoader boot = Object.class.getClassLoader();
//noinspection ConstantConditions
- this.name = sForInternedString.unparcel(in);
+ this.name = in.readString();
this.icon = in.readInt();
this.labelRes = in.readInt();
this.nonLocalizedLabel = in.readCharSequence();
diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl
index 7018ebcbe9f4..5a180d7358dd 100644
--- a/core/java/com/android/internal/statusbar/IStatusBar.aidl
+++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl
@@ -82,7 +82,7 @@ oneway interface IStatusBar
* Notify system UI the immersive mode changed. This shall be removed when client immersive is
* enabled.
*/
- void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode);
+ void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode, int windowType);
void dismissKeyboardShortcutsMenu();
void toggleKeyboardShortcutsMenu(int deviceId);
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 9acb2427aaab..a1961aedf6b7 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -268,6 +268,9 @@
72dp (content margin) - 12dp (action padding) - 4dp (button inset) -->
<dimen name="notification_2025_actions_margin_start">56dp</dimen>
+ <!-- Notification action button text size -->
+ <dimen name="notification_2025_action_text_size">16sp</dimen>
+
<!-- The margin on the end of most content views (ignores the expander) -->
<dimen name="notification_content_margin_end">16dp</dimen>
diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
index 8ef105f79988..de5f0ffbe23f 100644
--- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
+++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java
@@ -177,8 +177,10 @@ public class DisplayManagerGlobalTest {
@RequiresFlagsEnabled(Flags.FLAG_DELAY_IMPLICIT_RR_REGISTRATION_UNTIL_RR_ACCESSED)
public void test_refreshRateRegistration_implicitRRCallbacksEnabled()
throws RemoteException {
+ DisplayManager.DisplayListener displayListener1 =
+ Mockito.mock(DisplayManager.DisplayListener.class);
// Subscription without supplied events doesn't subscribe to RR events
- mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler,
+ mDisplayManagerGlobal.registerDisplayListener(displayListener1, mHandler,
ALL_DISPLAY_EVENTS, /* packageName= */ null,
/* isEventFilterExplicit */ false);
Mockito.verify(mDisplayManager)
@@ -187,7 +189,9 @@ public class DisplayManagerGlobalTest {
// After registering to refresh rate changes, subscription without supplied events subscribe
// to RR events
mDisplayManagerGlobal.registerForRefreshRateChanges();
- mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler,
+ DisplayManager.DisplayListener displayListener2 =
+ Mockito.mock(DisplayManager.DisplayListener.class);
+ mDisplayManagerGlobal.registerDisplayListener(displayListener2, mHandler,
ALL_DISPLAY_EVENTS, /* packageName= */ null,
/* isEventFilterExplicit */ false);
Mockito.verify(mDisplayManager)
@@ -203,7 +207,9 @@ public class DisplayManagerGlobalTest {
}
// Subscription to RR when events are supplied doesn't happen
- mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler,
+ DisplayManager.DisplayListener displayListener3 =
+ Mockito.mock(DisplayManager.DisplayListener.class);
+ mDisplayManagerGlobal.registerDisplayListener(displayListener3, mHandler,
ALL_DISPLAY_EVENTS, /* packageName= */ null,
/* isEventFilterExplicit */ true);
Mockito.verify(mDisplayManager)
@@ -214,7 +220,6 @@ public class DisplayManagerGlobalTest {
int subscribedListenersCount = 0;
int nonSubscribedListenersCount = 0;
for (DisplayManagerGlobal.DisplayListenerDelegate delegate: delegates) {
-
if (delegate.isEventFilterExplicit()) {
assertEquals(ALL_DISPLAY_EVENTS, delegate.mInternalEventFlagsMask);
nonSubscribedListenersCount++;
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index 4516e9ce72fc..af87af0d243f 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -1195,6 +1195,23 @@ public class InsetsControllerTest {
});
}
+ @Test
+ public void testAnimatingTypes() throws Exception {
+ prepareControls();
+
+ final int types = navigationBars() | statusBars();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ clearInvocations(mTestHost);
+ mController.hide(types);
+ // quickly jump to final state by cancelling it.
+ mController.cancelExistingAnimations();
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+ verify(mTestHost, times(1)).updateAnimatingTypes(eq(types));
+ verify(mTestHost, times(1)).updateAnimatingTypes(eq(0) /* animatingTypes */);
+ }
+
private void waitUntilNextFrame() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT,
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index c40137f1bd34..f5d1e7a85e83 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -1054,7 +1054,7 @@ public class ViewRootImplTest {
ViewRootImpl viewRootImpl = mView.getViewRootImpl();
sInstrumentation.runOnMainSync(() -> {
mView.invalidate();
- viewRootImpl.notifyInsetsAnimationRunningStateChanged(true);
+ viewRootImpl.updateAnimatingTypes(Type.systemBars());
mView.invalidate();
});
sInstrumentation.waitForIdleSync();
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
index 3aefcd5ec6c0..9087da34d259 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
@@ -552,7 +552,9 @@ class BubbleTaskViewListenerTest {
private fun createAppBubble(usePendingIntent: Boolean = false): Bubble {
val target = Intent(context, TestActivity::class.java)
+ val component = ComponentName(context, TestActivity::class.java)
target.setPackage(context.packageName)
+ target.setComponent(component)
if (usePendingIntent) {
// Robolectric doesn't seem to play nice with PendingIntents, have to mock it.
val pendingIntent = mock<PendingIntent>()
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
index 7b5831376dc0..14c15210252a 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
@@ -19,7 +19,9 @@ package com.android.wm.shell.bubbles.bar
import android.animation.AnimatorTestRule
import android.content.Context
import android.content.pm.LauncherApps
+import android.graphics.Insets
import android.graphics.PointF
+import android.graphics.Rect
import android.os.Handler
import android.os.UserManager
import android.view.IWindowManager
@@ -61,6 +63,7 @@ import com.android.wm.shell.common.TestShellExecutor
import com.android.wm.shell.shared.TransactionPool
import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+import com.android.wm.shell.shared.bubbles.DeviceConfig
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
@@ -80,6 +83,10 @@ import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
class BubbleBarLayerViewTest {
+ companion object {
+ const val SCREEN_WIDTH = 2000
+ const val SCREEN_HEIGHT = 1000
+ }
@get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
@@ -111,6 +118,16 @@ class BubbleBarLayerViewTest {
bubblePositioner = BubblePositioner(context, windowManager)
bubblePositioner.setShowingInBubbleBar(true)
+ val deviceConfig =
+ DeviceConfig(
+ windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT),
+ isLargeScreen = true,
+ isSmallTablet = false,
+ isLandscape = true,
+ isRtl = false,
+ insets = Insets.of(10, 20, 30, 40)
+ )
+ bubblePositioner.update(deviceConfig)
testBubblesList = mutableListOf()
val bubbleData = mock<BubbleData>()
@@ -313,6 +330,48 @@ class BubbleBarLayerViewTest {
assertThat(uiEventLoggerFake.logs[0]).hasBubbleInfo(bubble)
}
+ @Test
+ fun testUpdateExpandedView_updateLocation() {
+ bubblePositioner.bubbleBarLocation = BubbleBarLocation.RIGHT
+ val bubble = createBubble("first")
+
+ getInstrumentation().runOnMainSync {
+ bubbleBarLayerView.showExpandedView(bubble)
+ }
+ waitForExpandedViewAnimation()
+
+ val previousX = bubble.bubbleBarExpandedView!!.x
+
+ bubblePositioner.bubbleBarLocation = BubbleBarLocation.LEFT
+ getInstrumentation().runOnMainSync {
+ bubbleBarLayerView.updateExpandedView()
+ }
+
+ assertThat(bubble.bubbleBarExpandedView!!.x).isNotEqualTo(previousX)
+ }
+
+ @Test
+ fun testUpdatedExpandedView_updateLocation_skipWhileAnimating() {
+ bubblePositioner.bubbleBarLocation = BubbleBarLocation.RIGHT
+ val bubble = createBubble("first")
+
+ getInstrumentation().runOnMainSync {
+ bubbleBarLayerView.showExpandedView(bubble)
+ }
+ waitForExpandedViewAnimation()
+
+ val previousX = bubble.bubbleBarExpandedView!!.x
+ bubble.bubbleBarExpandedView!!.isAnimating = true
+
+ bubblePositioner.bubbleBarLocation = BubbleBarLocation.LEFT
+ getInstrumentation().runOnMainSync {
+ bubbleBarLayerView.updateExpandedView()
+ }
+
+ // Expanded view is not updated while animating
+ assertThat(bubble.bubbleBarExpandedView!!.x).isEqualTo(previousX)
+ }
+
private fun createBubble(key: String): Bubble {
val bubbleTaskView = FakeBubbleTaskViewFactory(context, mainExecutor).create()
val bubbleBarExpandedView =
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index d50a14cf5dae..c2aa146d6437 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -79,7 +79,7 @@
android:layout_marginEnd="4dp">
<Button
- android:layout_width="94dp"
+ android:layout_width="108dp"
android:layout_height="60dp"
android:id="@+id/maximize_menu_size_toggle_button"
style="?android:attr/buttonBarButtonStyle"
@@ -126,7 +126,7 @@
<Button
android:id="@+id/maximize_menu_snap_left_button"
style="?android:attr/buttonBarButtonStyle"
- android:layout_width="41dp"
+ android:layout_width="48dp"
android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
android:layout_marginEnd="4dp"
android:background="@drawable/desktop_mode_maximize_menu_button_background"
@@ -137,7 +137,7 @@
<Button
android:id="@+id/maximize_menu_snap_right_button"
style="?android:attr/buttonBarButtonStyle"
- android:layout_width="41dp"
+ android:layout_width="48dp"
android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
android:background="@drawable/desktop_mode_maximize_menu_button_background"
android:importantForAccessibility="yes"
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index a0c68ad44379..32660e8fca27 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -618,6 +618,15 @@
<!-- The vertical inset to apply to the app chip's ripple drawable -->
<dimen name="desktop_mode_header_app_chip_ripple_inset_vertical">4dp</dimen>
+ <!-- The corner radius of the windowing actions pill buttons's ripple drawable -->
+ <dimen name="desktop_mode_handle_menu_windowing_action_ripple_radius">24dp</dimen>
+ <!-- The horizontal/vertical inset to apply to the ripple drawable effect of windowing
+ actions pill central buttons -->
+ <dimen name="desktop_mode_handle_menu_windowing_action_ripple_inset_base">2dp</dimen>
+ <!-- The horizontal/vertical vertical inset to apply to the ripple drawable effect of windowing
+ actions pill edge buttons -->
+ <dimen name="desktop_mode_handle_menu_windowing_action_ripple_inset_shift">4dp</dimen>
+
<!-- The corner radius of the minimize button's ripple drawable -->
<dimen name="desktop_mode_header_minimize_ripple_radius">18dp</dimen>
<!-- The vertical inset to apply to the minimize button's ripple drawable -->
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index 00c446c3da60..7acad5054e98 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -374,7 +374,7 @@ public class DesktopModeStatus {
* of the display's root [TaskDisplayArea] is set to WINDOWING_MODE_FREEFORM.
*/
public static boolean enterDesktopByDefaultOnFreeformDisplay(@NonNull Context context) {
- if (!Flags.enterDesktopByDefaultOnFreeformDisplays()) {
+ if (!DesktopExperienceFlags.ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS.isTrue()) {
return false;
}
return SystemProperties.getBoolean(ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAY_SYS_PROP,
@@ -387,7 +387,7 @@ public class DesktopModeStatus {
* screen.
*/
public static boolean shouldMaximizeWhenDragToTopEdge(@NonNull Context context) {
- if (!Flags.enableDragToMaximize()) {
+ if (!DesktopExperienceFlags.ENABLE_DRAG_TO_MAXIMIZE.isTrue()) {
return false;
}
return SystemProperties.getBoolean(ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
index 53551387230c..26c362611518 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java
@@ -147,10 +147,9 @@ class ActivityEmbeddingAnimationAdapter {
/** To be overridden by subclasses to adjust the animation surface change. */
void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
// Update the surface position and alpha.
- if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
- && mAnimation.getExtensionEdges() != 0x0
+ if (mAnimation.getExtensionEdges() != 0x0
&& !(mChange.hasFlags(FLAG_TRANSLUCENT)
- && mChange.getActivityComponent() != null)) {
+ && mChange.getActivityComponent() != null)) {
// Extend non-translucent activities
t.setEdgeExtensionEffect(mLeash, mAnimation.getExtensionEdges());
}
@@ -189,8 +188,7 @@ class ActivityEmbeddingAnimationAdapter {
@CallSuper
void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
onAnimationUpdate(t, mAnimation.getDuration());
- if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()
- && mAnimation.getExtensionEdges() != 0x0) {
+ if (mAnimation.getExtensionEdges() != 0x0) {
t.setEdgeExtensionEffect(mLeash, /* edge */ 0);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index c3e783ddf4f1..85b7ac27daa0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -20,11 +20,9 @@ import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;
import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
-import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationSpec.createShowSnapshotForClosingAnimation;
import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
-import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE;
@@ -143,10 +141,6 @@ class ActivityEmbeddingAnimationRunner {
// ending states.
prepareForJumpCut(info, startTransaction);
} else {
- if (!com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) {
- addEdgeExtensionIfNeeded(startTransaction, finishTransaction,
- postStartTransactionCallbacks, adapters);
- }
addBackgroundColorIfNeeded(info, startTransaction, finishTransaction, adapters);
for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
duration = Math.max(duration, adapter.getDurationHint());
@@ -329,34 +323,6 @@ class ActivityEmbeddingAnimationRunner {
}
}
- /** Adds edge extension to the surfaces that have such an animation property. */
- private void addEdgeExtensionIfNeeded(@NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull List<Consumer<SurfaceControl.Transaction>> postStartTransactionCallbacks,
- @NonNull List<ActivityEmbeddingAnimationAdapter> adapters) {
- for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
- final Animation animation = adapter.mAnimation;
- if (animation.getExtensionEdges() == 0) {
- continue;
- }
- if (adapter.mChange.hasFlags(FLAG_TRANSLUCENT)
- && adapter.mChange.getActivityComponent() != null) {
- // Skip edge extension for translucent activity.
- continue;
- }
- final TransitionInfo.Change change = adapter.mChange;
- if (TransitionUtil.isOpeningType(adapter.mChange.getMode())) {
- // Need to screenshot after startTransaction is applied otherwise activity
- // may not be visible or ready yet.
- postStartTransactionCallbacks.add(
- t -> edgeExtendWindow(change, animation, t, finishTransaction));
- } else {
- // Can screenshot now (before startTransaction is applied)
- edgeExtendWindow(change, animation, startTransaction, finishTransaction);
- }
- }
- }
-
/** Adds background color to the transition if any animation has such a property. */
private void addBackgroundColorIfNeeded(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 313d151aeab7..d9489287ff42 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -364,7 +364,7 @@ public class Bubble implements BubbleViewProvider {
@ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
return new Bubble(intent,
user,
- /* key= */ getAppBubbleKeyForApp(intent.getIntent().getPackage(), user),
+ /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user),
mainExecutor, bgExecutor);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 29837dc04423..677c21c96f4b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -473,7 +473,7 @@ public class BubbleBarLayerView extends FrameLayout
/** Updates the expanded view size and position. */
public void updateExpandedView() {
- if (mExpandedView == null || mExpandedBubble == null) return;
+ if (mExpandedView == null || mExpandedBubble == null || mExpandedView.isAnimating()) return;
boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
mPositioner.getBubbleBarExpandedViewBounds(mPositioner.isBubbleBarOnLeft(),
isOverflowExpanded, mTempRect);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index df82091ef002..dd2050a5fd5d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -461,6 +461,14 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
}
}
+ private void setAnimating(boolean imeAnimationOngoing) {
+ int animatingTypes = imeAnimationOngoing ? WindowInsets.Type.ime() : 0;
+ try {
+ mWmService.updateDisplayWindowAnimatingTypes(mDisplayId, animatingTypes);
+ } catch (RemoteException e) {
+ }
+ }
+
private int imeTop(float surfaceOffset, float surfacePositionY) {
// surfaceOffset is already offset by the surface's top inset, so we need to subtract
// the top inset so that the return value is in screen coordinates.
@@ -619,6 +627,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
+ imeTop(hiddenY, defaultY) + "->" + imeTop(shownY, defaultY)
+ " showing:" + (mAnimationDirection == DIRECTION_SHOW));
}
+ if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+ setAnimating(true);
+ }
int flags = dispatchStartPositioning(mDisplayId, imeTop(hiddenY, defaultY),
imeTop(shownY, defaultY), mAnimationDirection == DIRECTION_SHOW,
isFloating, t);
@@ -666,6 +677,8 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
}
if (!android.view.inputmethod.Flags.refactorInsetsController()) {
dispatchEndPositioning(mDisplayId, mCancelled, t);
+ } else if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+ setAnimating(false);
}
if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
ImeTracker.forLogging().onProgress(mStatsToken,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 59acdc574434..48fadc02ff1f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -100,6 +100,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
+import com.android.wm.shell.desktopmode.DragToDisplayTransitionHandler;
import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.OverviewToDesktopTransitionObserver;
@@ -770,7 +771,8 @@ public abstract class WMShellModule {
DesksOrganizer desksOrganizer,
DesksTransitionObserver desksTransitionObserver,
UserProfileContexts userProfileContexts,
- DesktopModeCompatPolicy desktopModeCompatPolicy) {
+ DesktopModeCompatPolicy desktopModeCompatPolicy,
+ DragToDisplayTransitionHandler dragToDisplayTransitionHandler) {
return new DesktopTasksController(
context,
shellInit,
@@ -808,7 +810,8 @@ public abstract class WMShellModule {
desksOrganizer,
desksTransitionObserver,
userProfileContexts,
- desktopModeCompatPolicy);
+ desktopModeCompatPolicy,
+ dragToDisplayTransitionHandler);
}
@WMSingleton
@@ -934,6 +937,12 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
+ static DragToDisplayTransitionHandler provideDragToDisplayTransitionHandler() {
+ return new DragToDisplayTransitionHandler();
+ }
+
+ @WMSingleton
+ @Provides
static Optional<DesktopModeKeyGestureHandler> provideDesktopModeKeyGestureHandler(
Context context,
Optional<DesktopModeWindowDecorViewModel> desktopModeWindowDecorViewModel,
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 c9a63ff818f5..e89aafe267ed 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
@@ -27,9 +27,9 @@ import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERN
import android.view.Display.DEFAULT_DISPLAY
import android.view.IWindowManager
import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.DesktopExperienceFlags
import android.window.WindowContainerTransaction
import com.android.internal.protolog.ProtoLog
-import com.android.window.flags.Flags
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
@@ -47,31 +47,9 @@ class DesktopDisplayModeController(
) {
fun refreshDisplayWindowingMode() {
- if (!Flags.enableDisplayWindowingModeSwitching()) return
- // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available.
- val isExtendedDisplayEnabled =
- 0 !=
- Settings.Global.getInt(
- context.contentResolver,
- DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
- 0,
- )
- if (!isExtendedDisplayEnabled) {
- // No action needed in mirror or projected mode.
- return
- }
+ if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return
- val hasNonDefaultDisplay =
- rootTaskDisplayAreaOrganizer.getDisplayIds().any { displayId ->
- displayId != DEFAULT_DISPLAY
- }
- val targetDisplayWindowingMode =
- if (hasNonDefaultDisplay) {
- WINDOWING_MODE_FREEFORM
- } else {
- // Use the default display windowing mode when no non-default display.
- windowManager.getWindowingMode(DEFAULT_DISPLAY)
- }
+ val targetDisplayWindowingMode = getTargetWindowingModeForDefaultDisplay()
val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
requireNotNull(tdaInfo) { "DisplayAreaInfo of DEFAULT_DISPLAY must be non-null." }
val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
@@ -111,6 +89,25 @@ class DesktopDisplayModeController(
transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
}
+ private fun getTargetWindowingModeForDefaultDisplay(): Int {
+ if (isExtendedDisplayEnabled() && hasExternalDisplay()) {
+ return WINDOWING_MODE_FREEFORM
+ }
+ return windowManager.getWindowingMode(DEFAULT_DISPLAY)
+ }
+
+ // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available.
+ private fun isExtendedDisplayEnabled() =
+ 0 !=
+ Settings.Global.getInt(
+ context.contentResolver,
+ DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS,
+ 0,
+ )
+
+ private fun hasExternalDisplay() =
+ rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY }
+
private fun logV(msg: String, vararg arguments: Any?) {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index 04e609ec3820..03423ba3b96a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -1007,6 +1007,21 @@ class DesktopRepository(
fun saveBoundsBeforeFullImmersive(taskId: Int, bounds: Rect) =
boundsBeforeFullImmersiveByTaskId.set(taskId, Rect(bounds))
+ /** Returns the current state of the desktop, formatted for usage by remote clients. */
+ fun getDeskDisplayStateForRemote(): Array<DisplayDeskState> =
+ desktopData
+ .desksSequence()
+ .groupBy { it.displayId }
+ .map { (displayId, desks) ->
+ val activeDeskId = desktopData.getActiveDesk(displayId)?.deskId
+ DisplayDeskState().apply {
+ this.displayId = displayId
+ this.activeDeskId = activeDeskId ?: INVALID_DESK_ID
+ this.deskIds = desks.map { it.deskId }.toIntArray()
+ }
+ }
+ .toTypedArray()
+
/** TODO: b/389960283 - consider updating only the changing desks. */
private fun updatePersistentRepository(displayId: Int) {
val desks = desktopData.desksSequence(displayId).map { desk -> desk.deepCopy() }.toList()
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 180d069f359d..fca5084b65bc 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
@@ -205,6 +205,7 @@ class DesktopTasksController(
private val desksTransitionObserver: DesksTransitionObserver,
private val userProfileContexts: UserProfileContexts,
private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
+ private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler,
) :
RemoteCallable<DesktopTasksController>,
Transitions.TransitionHandler,
@@ -811,7 +812,7 @@ class DesktopTasksController(
willExitDesktop(
triggerTaskId = taskInfo.taskId,
displayId = displayId,
- forceToFullscreen = false,
+ forceExitDesktop = false,
)
taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true)
val desktopExitRunnable =
@@ -884,7 +885,7 @@ class DesktopTasksController(
snapEventHandler.removeTaskIfTiled(displayId, taskId)
taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true)
- val willExitDesktop = willExitDesktop(taskId, displayId, forceToFullscreen = false)
+ val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false)
val desktopExitRunnable =
performDesktopExitCleanUp(
wct = wct,
@@ -977,7 +978,7 @@ class DesktopTasksController(
) {
logV("moveToFullscreenWithAnimation taskId=%d", task.taskId)
val wct = WindowContainerTransaction()
- val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceToFullscreen = true)
+ val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceExitDesktop = true)
val deactivationRunnable = addMoveToFullscreenChanges(wct, task, willExitDesktop)
// We are moving a freeform task to fullscreen, put the home task under the fullscreen task.
@@ -996,7 +997,14 @@ class DesktopTasksController(
deactivationRunnable?.invoke(transition)
// handles case where we are moving to full screen without closing all DW tasks.
- if (!taskRepository.isOnlyVisibleNonClosingTask(task.taskId)) {
+ if (
+ !taskRepository.isOnlyVisibleNonClosingTask(task.taskId)
+ // This callback is already invoked by |addMoveToFullscreenChanges| when one of these
+ // flags is enabled.
+ &&
+ !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue &&
+ !Flags.enableDesktopWindowingPip()
+ ) {
desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted(
FULLSCREEN_ANIMATION_DURATION
)
@@ -1893,16 +1901,24 @@ class DesktopTasksController(
private fun willExitDesktop(
triggerTaskId: Int,
displayId: Int,
- forceToFullscreen: Boolean,
+ forceExitDesktop: Boolean,
): Boolean {
+ if (
+ forceExitDesktop &&
+ (Flags.enableDesktopWindowingPip() ||
+ DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue)
+ ) {
+ // |forceExitDesktop| is true when the callers knows we'll exit desktop, such as when
+ // explicitly going fullscreen, so there's no point in checking the desktop state.
+ return true
+ }
if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId, displayId)) {
return false
}
} else if (
Flags.enableDesktopWindowingPip() &&
- taskRepository.isMinimizedPipPresentInDisplay(displayId) &&
- !forceToFullscreen
+ taskRepository.isMinimizedPipPresentInDisplay(displayId)
) {
return false
} else {
@@ -2295,7 +2311,7 @@ class DesktopTasksController(
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
- forceToFullscreen = true,
+ forceExitDesktop = true,
),
)
wct.reorder(task.token, true)
@@ -2328,7 +2344,7 @@ class DesktopTasksController(
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
- forceToFullscreen = true,
+ forceExitDesktop = true,
),
)
return wct
@@ -2433,7 +2449,7 @@ class DesktopTasksController(
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
- forceToFullscreen = true,
+ forceExitDesktop = true,
),
)
}
@@ -2471,7 +2487,7 @@ class DesktopTasksController(
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
- forceToFullscreen = true,
+ forceExitDesktop = true,
),
)
}
@@ -3173,25 +3189,24 @@ class DesktopTasksController(
val wct = WindowContainerTransaction()
wct.setBounds(taskInfo.token, destinationBounds)
- // TODO: b/362720497 - reparent to a specific desk within the target display.
- // Reparent task if it has been moved to a new display.
- if (Flags.enableConnectedDisplaysWindowDrag()) {
- val newDisplayId = motionEvent.getDisplayId()
- if (newDisplayId != taskInfo.getDisplayId()) {
- val displayAreaInfo =
- rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId)
- if (displayAreaInfo == null) {
- logW(
- "Task reparent cannot find DisplayAreaInfo for displayId=%d",
- newDisplayId,
- )
- } else {
- wct.reparent(taskInfo.token, displayAreaInfo.token, /* onTop= */ true)
- }
+ val newDisplayId = motionEvent.getDisplayId()
+ val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId)
+ val isCrossDisplayDrag =
+ Flags.enableConnectedDisplaysWindowDrag() &&
+ newDisplayId != taskInfo.getDisplayId() &&
+ displayAreaInfo != null
+ val handler =
+ if (isCrossDisplayDrag) {
+ dragToDisplayTransitionHandler
+ } else {
+ null
}
+ if (isCrossDisplayDrag) {
+ // TODO: b/362720497 - reparent to a specific desk within the target display.
+ wct.reparent(taskInfo.token, displayAreaInfo.token, /* onTop= */ true)
}
- transitions.startTransition(TRANSIT_CHANGE, wct, null)
+ transitions.startTransition(TRANSIT_CHANGE, wct, handler)
releaseVisualIndicator()
}
@@ -3613,27 +3628,11 @@ class DesktopTasksController(
controller,
{ c ->
run {
- c.taskRepository.addDeskChangeListener(
- deskChangeListener,
- c.mainExecutor,
- )
- c.taskRepository.addVisibleTasksListener(
- visibleTasksListener,
- c.mainExecutor,
- )
- c.taskbarDesktopTaskListener = taskbarDesktopTaskListener
- c.desktopModeEnterExitTransitionListener =
- desktopModeEntryExitTransitionListener
- }
- },
- { c ->
- run {
- c.taskRepository.removeDeskChangeListener(deskChangeListener)
- c.taskRepository.removeVisibleTasksListener(visibleTasksListener)
- c.taskbarDesktopTaskListener = null
- c.desktopModeEnterExitTransitionListener = null
+ syncInitialState(c)
+ registerListeners(c)
}
},
+ { c -> run { unregisterListeners(c) } },
)
}
@@ -3729,6 +3728,31 @@ class DesktopTasksController(
c.startLaunchIntentTransition(intent, options, displayId)
}
}
+
+ private fun syncInitialState(c: DesktopTasksController) {
+ remoteListener.call { l ->
+ // TODO: b/393962589 - implement desks limit.
+ val canCreateDesks = true
+ l.onListenerConnected(
+ c.taskRepository.getDeskDisplayStateForRemote(),
+ canCreateDesks,
+ )
+ }
+ }
+
+ private fun registerListeners(c: DesktopTasksController) {
+ c.taskRepository.addDeskChangeListener(deskChangeListener, c.mainExecutor)
+ c.taskRepository.addVisibleTasksListener(visibleTasksListener, c.mainExecutor)
+ c.taskbarDesktopTaskListener = taskbarDesktopTaskListener
+ c.desktopModeEnterExitTransitionListener = desktopModeEntryExitTransitionListener
+ }
+
+ private fun unregisterListeners(c: DesktopTasksController) {
+ c.taskRepository.removeDeskChangeListener(deskChangeListener)
+ c.taskRepository.removeVisibleTasksListener(visibleTasksListener)
+ c.taskbarDesktopTaskListener = null
+ c.desktopModeEnterExitTransitionListener = null
+ }
}
private fun logV(msg: String, vararg arguments: Any?) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.kt
new file mode 100644
index 000000000000..d51576a5148e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandler.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.wm.shell.desktopmode
+
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import android.window.WindowContainerTransaction
+import com.android.wm.shell.transition.Transitions
+
+/** Handles the transition to drag a window to another display by dragging the caption. */
+class DragToDisplayTransitionHandler : Transitions.TransitionHandler {
+ override fun handleRequest(
+ transition: IBinder,
+ request: TransitionRequestInfo,
+ ): WindowContainerTransaction? {
+ return null
+ }
+
+ override fun startAnimation(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ ): Boolean {
+ for (change in info.changes) {
+ val sc = change.leash
+ val endBounds = change.endAbsBounds
+ val endPosition = change.endRelOffset
+ startTransaction
+ .setWindowCrop(sc, endBounds.width(), endBounds.height())
+ .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat())
+ finishTransaction
+ .setWindowCrop(sc, endBounds.width(), endBounds.height())
+ .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat())
+ }
+
+ startTransaction.apply()
+ finishCallback.onTransitionFinished(null)
+ return true
+ }
+}
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 e9c6adec75d7..3652a1661f28 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
@@ -67,7 +67,6 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN;
import static com.android.wm.shell.transition.DefaultSurfaceAnimator.buildSurfaceAnimation;
-import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionTypeFromInfo;
import static com.android.wm.shell.transition.TransitionAnimationHelper.isCoveredByOpaqueFullscreenChange;
@@ -543,21 +542,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
backgroundColorForTransition);
if (!isTask && a.getExtensionEdges() != 0x0) {
- if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader()) {
- startTransaction.setEdgeExtensionEffect(
- change.getLeash(), a.getExtensionEdges());
- finishTransaction.setEdgeExtensionEffect(change.getLeash(), /* edge */ 0);
- } else {
- if (!TransitionUtil.isOpeningType(mode)) {
- // Can screenshot now (before startTransaction is applied)
- edgeExtendWindow(change, a, startTransaction, finishTransaction);
- } else {
- // Need to screenshot after startTransaction is applied otherwise
- // activity may not be visible or ready yet.
- postStartTransactionCallbacks
- .add(t -> edgeExtendWindow(change, a, t, finishTransaction));
- }
- }
+ startTransaction.setEdgeExtensionEffect(
+ change.getLeash(), a.getExtensionEdges());
+ finishTransaction.setEdgeExtensionEffect(change.getLeash(), /* edge */ 0);
}
final Rect clipRect = TransitionUtil.isClosingType(mode)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index 7984bcedc4e5..edfb56019a60 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -26,7 +26,6 @@ import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
-import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
@@ -39,20 +38,10 @@ import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.WindowConfiguration;
-import android.graphics.BitmapShader;
-import android.graphics.Canvas;
import android.graphics.Color;
-import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.Shader;
-import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.view.animation.Animation;
-import android.view.animation.Transformation;
-import android.window.ScreenCapture;
import android.window.TransitionInfo;
import com.android.internal.R;
@@ -317,129 +306,6 @@ public class TransitionAnimationHelper {
}
/**
- * Adds edge extension surface to the given {@code change} for edge extension animation.
- */
- public static void edgeExtendWindow(@NonNull TransitionInfo.Change change,
- @NonNull Animation a, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction) {
- // Do not create edge extension surface for transfer starting window change.
- // The app surface could be empty thus nothing can draw on the hardware renderer, which will
- // block this thread when calling Surface#unlockCanvasAndPost.
- if ((change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) != 0) {
- return;
- }
- final Transformation transformationAtStart = new Transformation();
- a.getTransformationAt(0, transformationAtStart);
- final Transformation transformationAtEnd = new Transformation();
- a.getTransformationAt(1, transformationAtEnd);
-
- // We want to create an extension surface that is the maximal size and the animation will
- // take care of cropping any part that overflows.
- final Insets maxExtensionInsets = Insets.min(
- transformationAtStart.getInsets(), transformationAtEnd.getInsets());
-
- final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(),
- change.getEndAbsBounds().height());
- final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(),
- change.getEndAbsBounds().width());
- if (maxExtensionInsets.left < 0) {
- final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- -maxExtensionInsets.left, targetSurfaceHeight);
- final int xPos = maxExtensionInsets.left;
- final int yPos = 0;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Left Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.top < 0) {
- final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1);
- final Rect extensionRect = new Rect(0, 0,
- targetSurfaceWidth, -maxExtensionInsets.top);
- final int xPos = 0;
- final int yPos = maxExtensionInsets.top;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Top Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.right < 0) {
- final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0,
- targetSurfaceWidth, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- -maxExtensionInsets.right, targetSurfaceHeight);
- final int xPos = targetSurfaceWidth;
- final int yPos = 0;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Right Edge Extension", startTransaction, finishTransaction);
- }
-
- if (maxExtensionInsets.bottom < 0) {
- final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1,
- targetSurfaceWidth, targetSurfaceHeight);
- final Rect extensionRect = new Rect(0, 0,
- targetSurfaceWidth, -maxExtensionInsets.bottom);
- final int xPos = maxExtensionInsets.left;
- final int yPos = targetSurfaceHeight;
- createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos,
- "Bottom Edge Extension", startTransaction, finishTransaction);
- }
- }
-
- /**
- * Takes a screenshot of {@code surfaceToExtend}'s edge and extends it for edge extension
- * animation.
- */
- private static SurfaceControl createExtensionSurface(@NonNull SurfaceControl surfaceToExtend,
- @NonNull Rect edgeBounds, @NonNull Rect extensionRect, int xPos, int yPos,
- @NonNull String layerName, @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction) {
- final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder()
- .setName(layerName)
- .setParent(surfaceToExtend)
- .setHidden(true)
- .setCallsite("TransitionAnimationHelper#createExtensionSurface")
- .setOpaque(true)
- .setBufferSize(extensionRect.width(), extensionRect.height())
- .build();
-
- final ScreenCapture.LayerCaptureArgs captureArgs =
- new ScreenCapture.LayerCaptureArgs.Builder(surfaceToExtend)
- .setSourceCrop(edgeBounds)
- .setFrameScale(1)
- .setPixelFormat(PixelFormat.RGBA_8888)
- .setChildrenOnly(true)
- .setAllowProtected(false)
- .setCaptureSecureLayers(true)
- .build();
- final ScreenCapture.ScreenshotHardwareBuffer edgeBuffer =
- ScreenCapture.captureLayers(captureArgs);
-
- if (edgeBuffer == null) {
- ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
- "Failed to capture edge of window.");
- return null;
- }
-
- final BitmapShader shader = new BitmapShader(edgeBuffer.asBitmap(),
- Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
- final Paint paint = new Paint();
- paint.setShader(shader);
-
- final Surface surface = new Surface(edgeExtensionLayer);
- final Canvas c = surface.lockHardwareCanvas();
- c.drawRect(extensionRect, paint);
- surface.unlockCanvasAndPost(c);
- surface.release();
-
- startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE);
- startTransaction.setPosition(edgeExtensionLayer, xPos, yPos);
- startTransaction.setVisibility(edgeExtensionLayer, true);
- finishTransaction.remove(edgeExtensionLayer);
-
- return edgeExtensionLayer;
- }
-
- /**
* Returns whether there is an opaque fullscreen Change positioned in front of the given Change
* in the given TransitionInfo.
*/
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 ff50672953c9..ad2e23cb4028 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
@@ -50,6 +50,7 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.Accessibilit
import androidx.core.view.isGone
import com.android.window.flags.Flags
import com.android.wm.shell.R
+import com.android.wm.shell.bubbles.ContextUtils.isRtl
import com.android.wm.shell.shared.annotations.ShellBackgroundThread
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
@@ -60,6 +61,8 @@ import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewCo
import com.android.wm.shell.windowdecor.common.DecorThemeUtil
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader
import com.android.wm.shell.windowdecor.common.calculateMenuPosition
+import com.android.wm.shell.windowdecor.common.DrawableInsets
+import com.android.wm.shell.windowdecor.common.createRippleDrawable
import com.android.wm.shell.windowdecor.extension.isFullscreen
import com.android.wm.shell.windowdecor.extension.isMultiWindow
import com.android.wm.shell.windowdecor.extension.isPinned
@@ -71,6 +74,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+
/**
* Handle menu opened when the appropriate button is clicked on.
*
@@ -467,6 +471,33 @@ class HandleMenu(
val rootView = LayoutInflater.from(context)
.inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View
+ private val windowingButtonRippleRadius = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_handle_menu_windowing_action_ripple_radius)
+ private val windowingButtonDrawableInsets = DrawableInsets(
+ vertical = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base),
+ horizontal = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base)
+ )
+ private val windowingButtonDrawableInsetsLeft = DrawableInsets(
+ vertical = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base),
+ horizontalLeft = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_shift),
+ )
+ private val windowingButtonDrawableInsetsRight = DrawableInsets(
+ vertical = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_base),
+ horizontalRight = context.resources
+ .getDimensionPixelSize(
+ R.dimen.desktop_mode_handle_menu_windowing_action_ripple_inset_shift)
+ )
+
// App Info Pill.
private val appInfoPill = rootView.requireViewById<View>(R.id.app_info_pill)
private val collapseMenuButton = appInfoPill.requireViewById<HandleMenuImageButton>(
@@ -708,6 +739,49 @@ class HandleMenu(
desktopBtn.isSelected = taskInfo.isFreeform
desktopBtn.isEnabled = !taskInfo.isFreeform
desktopBtn.imageTintList = style.windowingButtonColor
+
+ val startInsets = if (context.isRtl) {
+ windowingButtonDrawableInsetsRight
+ } else {
+ windowingButtonDrawableInsetsLeft
+ }
+ val endInsets = if (context.isRtl) {
+ windowingButtonDrawableInsetsLeft
+ } else {
+ windowingButtonDrawableInsetsRight
+ }
+
+ fullscreenBtn.apply {
+ background = createRippleDrawable(
+ color = style.textColor,
+ cornerRadius = windowingButtonRippleRadius,
+ drawableInsets = startInsets
+ )
+ }
+
+ splitscreenBtn.apply {
+ background = createRippleDrawable(
+ color = style.textColor,
+ cornerRadius = windowingButtonRippleRadius,
+ drawableInsets = windowingButtonDrawableInsets
+ )
+ }
+
+ floatingBtn.apply {
+ background = createRippleDrawable(
+ color = style.textColor,
+ cornerRadius = windowingButtonRippleRadius,
+ drawableInsets = windowingButtonDrawableInsets
+ )
+ }
+
+ desktopBtn.apply {
+ background = createRippleDrawable(
+ color = style.textColor,
+ cornerRadius = windowingButtonRippleRadius,
+ drawableInsets = endInsets
+ )
+ }
}
private fun bindMoreActionsPill(style: MenuStyle) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
index c6cb62d153ac..1b0e0f70ed21 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt
@@ -363,10 +363,11 @@ class MultiDisplayVeiledResizeTaskPositioner(
dragEventListeners.remove(dragEventListener)
}
- override fun onTopologyChanged(topology: DisplayTopology) {
+ override fun onTopologyChanged(topology: DisplayTopology?) {
// TODO: b/383069173 - Cancel window drag when topology changes happen during drag.
displayIds.clear()
+ if (topology == null) return
val displayBounds = topology.getAbsoluteBounds()
displayIds.addAll(List(displayBounds.size()) { displayBounds.keyAt(it) })
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
index 7af6b8e26cbf..5bd42280e790 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
@@ -225,7 +225,7 @@ public class ResizeVeil @JvmOverloads constructor(
val veilAnimT = surfaceControlTransactionSupplier.get()
val iconAnimT = surfaceControlTransactionSupplier.get()
veilAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
- duration = RESIZE_ALPHA_DURATION
+ duration = VEIL_ENTRY_ALPHA_ANIMATION_DURATION
addUpdateListener {
veilAnimT.setAlpha(background, animatedValue as Float)
.apply()
@@ -243,7 +243,8 @@ public class ResizeVeil @JvmOverloads constructor(
})
}
iconAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
- duration = RESIZE_ALPHA_DURATION
+ duration = ICON_ALPHA_ANIMATION_DURATION
+ startDelay = ICON_ENTRY_DELAY
addUpdateListener {
iconAnimT.setAlpha(icon, animatedValue as Float)
.apply()
@@ -387,23 +388,38 @@ public class ResizeVeil @JvmOverloads constructor(
if (background == null || icon == null) return
veilAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
- duration = RESIZE_ALPHA_DURATION
+ duration = VEIL_EXIT_ALPHA_ANIMATION_DURATION
+ startDelay = VEIL_EXIT_DELAY
addUpdateListener {
surfaceControlTransactionSupplier.get()
.setAlpha(background, animatedValue as Float)
- .setAlpha(icon, animatedValue as Float)
.apply()
}
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
surfaceControlTransactionSupplier.get()
.hide(background)
- .hide(icon)
.apply()
}
})
}
+ iconAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = ICON_ALPHA_ANIMATION_DURATION
+ addUpdateListener {
+ surfaceControlTransactionSupplier.get()
+ .setAlpha(icon, animatedValue as Float)
+ .apply()
+ }
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ surfaceControlTransactionSupplier.get()
+ .hide(icon)
+ .apply()
+ }
+ })
+ }
veilAnimator?.start()
+ iconAnimator?.start()
isVisible = false
}
@@ -451,7 +467,11 @@ public class ResizeVeil @JvmOverloads constructor(
companion object {
private const val TAG = "ResizeVeil"
- private const val RESIZE_ALPHA_DURATION = 100L
+ private const val ICON_ALPHA_ANIMATION_DURATION = 50L
+ private const val VEIL_ENTRY_ALPHA_ANIMATION_DURATION = 50L
+ private const val VEIL_EXIT_ALPHA_ANIMATION_DURATION = 200L
+ private const val ICON_ENTRY_DELAY = 33L
+ private const val VEIL_EXIT_DELAY = 33L
private const val VEIL_CONTAINER_LAYER = TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL
/** The background is a child of the veil container layer and goes at the bottom. */
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
new file mode 100644
index 000000000000..e18239d3eb70
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ButtonBackgroundDrawableUtils.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor.common
+
+import android.annotation.ColorInt
+import android.graphics.Color
+import android.graphics.drawable.LayerDrawable
+import android.graphics.drawable.RippleDrawable
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.RoundRectShape
+import com.android.wm.shell.windowdecor.common.OPACITY_11
+import com.android.wm.shell.windowdecor.common.OPACITY_15
+import android.content.res.ColorStateList
+
+/**
+ * Represents drawable insets, specifying the number of pixels to inset a drawable from its bounds.
+ */
+data class DrawableInsets(val l: Int, val t: Int, val r: Int, val b: Int) {
+ constructor(vertical: Int = 0, horizontal: Int = 0) :
+ this(horizontal, vertical, horizontal, vertical)
+ constructor(vertical: Int = 0, horizontalLeft: Int = 0, horizontalRight: Int = 0) :
+ this(horizontalLeft, vertical, horizontalRight, vertical)
+}
+
+/**
+ * Replaces the alpha component of a color with the given alpha value.
+ */
+@ColorInt
+fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
+ return Color.argb(
+ alpha,
+ Color.red(color),
+ Color.green(color),
+ Color.blue(color)
+ )
+}
+
+/**
+ * Creates a RippleDrawable with specified color, corner radius, and insets.
+ */
+fun createRippleDrawable(
+ @ColorInt color: Int,
+ cornerRadius: Int,
+ drawableInsets: DrawableInsets,
+): RippleDrawable {
+ return RippleDrawable(
+ ColorStateList(
+ arrayOf(
+ intArrayOf(android.R.attr.state_hovered),
+ intArrayOf(android.R.attr.state_pressed),
+ intArrayOf(),
+ ),
+ intArrayOf(
+ replaceColorAlpha(color, OPACITY_11),
+ replaceColorAlpha(color, OPACITY_15),
+ Color.TRANSPARENT,
+ )
+ ),
+ null /* content */,
+ LayerDrawable(arrayOf(
+ ShapeDrawable().apply {
+ shape = RoundRectShape(
+ FloatArray(8) { cornerRadius.toFloat() },
+ null /* inset */,
+ null /* innerRadii */
+ )
+ paint.color = Color.WHITE
+ }
+ )).apply {
+ require(numberOfLayers == 1) { "Must only contain one layer" }
+ setLayerInset(0 /* index */,
+ drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b)
+ }
+ )
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 870c894fe885..eb8b617df4ce 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -61,6 +61,8 @@ import com.android.wm.shell.windowdecor.common.OPACITY_15
import com.android.wm.shell.windowdecor.common.OPACITY_55
import com.android.wm.shell.windowdecor.common.OPACITY_65
import com.android.wm.shell.windowdecor.common.Theme
+import com.android.wm.shell.windowdecor.common.DrawableInsets
+import com.android.wm.shell.windowdecor.common.createRippleDrawable
import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance
import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance
@@ -635,61 +637,10 @@ class AppHeaderViewHolder(
)
}
- @ColorInt
- private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
- return Color.argb(
- alpha,
- Color.red(color),
- Color.green(color),
- Color.blue(color)
- )
- }
-
- private fun createRippleDrawable(
- @ColorInt color: Int,
- cornerRadius: Int,
- drawableInsets: DrawableInsets,
- ): RippleDrawable {
- return RippleDrawable(
- ColorStateList(
- arrayOf(
- intArrayOf(android.R.attr.state_hovered),
- intArrayOf(android.R.attr.state_pressed),
- intArrayOf(),
- ),
- intArrayOf(
- replaceColorAlpha(color, OPACITY_11),
- replaceColorAlpha(color, OPACITY_15),
- Color.TRANSPARENT
- )
- ),
- null /* content */,
- LayerDrawable(arrayOf(
- ShapeDrawable().apply {
- shape = RoundRectShape(
- FloatArray(8) { cornerRadius.toFloat() },
- null /* inset */,
- null /* innerRadii */
- )
- paint.color = Color.WHITE
- }
- )).apply {
- require(numberOfLayers == 1) { "Must only contain one layer" }
- setLayerInset(0 /* index */,
- drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b)
- }
- )
- }
-
private enum class SizeToggleDirection {
MAXIMIZE, RESTORE
}
- private data class DrawableInsets(val l: Int, val t: Int, val r: Int, val b: Int) {
- constructor(vertical: Int = 0, horizontal: Int = 0) :
- this(horizontal, vertical, horizontal, vertical)
- }
-
private data class Header(
val type: Type,
val appTheme: Theme,
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
index 28008393da84..d82c06691e46 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/ExitDesktopWithDragToTopDragZone.kt
@@ -18,9 +18,9 @@ package com.android.wm.shell.scenarios
import android.tools.NavBar
import android.tools.Rotation
-import com.android.internal.R
import com.android.window.flags.Flags
import com.android.wm.shell.Utils
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import org.junit.After
import org.junit.Assume
import org.junit.Before
@@ -42,8 +42,8 @@ constructor(
fun setup() {
Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
// Skip the test when the drag-to-maximize is enabled on this device.
- Assume.assumeFalse(Flags.enableDragToMaximize() &&
- instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode))
+ Assume.assumeFalse(
+ DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(instrumentation.context))
tapl.setEnableRotation(true)
tapl.setExpectedRotation(rotation.value)
testApp.enterDesktopMode(wmHelper, device)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
index 60a0fb547909..675b63cf56bb 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MaximizeAppWindowWithDragToTopDragZone.kt
@@ -23,12 +23,12 @@ import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
-import com.android.internal.R
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.window.flags.Flags
import com.android.wm.shell.Utils
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import org.junit.After
import org.junit.Assume
import org.junit.Before
@@ -54,8 +54,8 @@ constructor(private val rotation: Rotation = Rotation.ROTATION_0) {
fun setup() {
Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
// Skip the test when the drag-to-maximize is disabled on this device.
- Assume.assumeTrue(Flags.enableDragToMaximize() &&
- instrumentation.context.resources.getBoolean(R.bool.config_dragToMaximizeInDesktopMode))
+ Assume.assumeTrue(
+ DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(instrumentation.context))
tapl.setEnableRotation(true)
tapl.setExpectedRotation(rotation.value)
ChangeDisplayOrientationRule.setRotation(rotation)
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
index 81c46f13b384..b9a5e4a95e36 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
@@ -25,6 +25,7 @@ import android.tools.Rotation
import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.traces.parsers.WindowManagerStateHelper
import android.util.DisplayMetrics
+import android.window.DesktopExperienceFlags
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
@@ -64,7 +65,7 @@ constructor(private val rotation: Rotation = Rotation.ROTATION_0) {
@Before
fun setup() {
Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
- Assume.assumeTrue(Flags.enableDisplayWindowingModeSwitching())
+ Assume.assumeTrue(DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue)
tapl.setEnableRotation(true)
tapl.setExpectedRotation(rotation.value)
ChangeDisplayOrientationRule.setRotation(rotation)
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index 7f48499b0558..e39fa3a71b03 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -22,7 +22,6 @@ import android.tools.flicker.legacy.FlickerBuilder
import android.tools.flicker.legacy.LegacyFlickerTest
import android.tools.flicker.legacy.LegacyFlickerTestFactory
import android.tools.traces.component.ComponentNameMatcher
-import android.tools.traces.component.EdgeExtensionComponentMatcher
import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
import com.android.wm.shell.flicker.splitscreen.benchmark.CopyContentInSplitBenchmark
@@ -99,7 +98,6 @@ class CopyContentInSplit(override val flicker: LegacyFlickerTest) :
ComponentNameMatcher.SPLASH_SCREEN,
ComponentNameMatcher.SNAPSHOT,
ComponentNameMatcher.IME_SNAPSHOT,
- EdgeExtensionComponentMatcher(),
magnifierLayer,
popupWindowLayer
)
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 0ff7230f6e0c..f0c97d359a16 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,7 +101,7 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
private fun testDisplayWindowingModeSwitch(
defaultWindowingMode: Int,
extendedDisplayEnabled: Boolean,
- expectTransition: Boolean,
+ expectToSwitch: Boolean,
) {
defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode
whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode)
@@ -113,10 +113,14 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
settingsSession.use {
connectExternalDisplay()
- defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ if (expectToSwitch) {
+ // Assumes [connectExternalDisplay] properly triggered the switching transition.
+ // Will verify the transition later along with [disconnectExternalDisplay].
+ defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ }
disconnectExternalDisplay()
- if (expectTransition) {
+ if (expectToSwitch) {
val arg = argumentCaptor<WindowContainerTransaction>()
verify(transitions, times(2))
.startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull())
@@ -139,7 +143,7 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
testDisplayWindowingModeSwitch(
defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
extendedDisplayEnabled = false,
- expectTransition = false,
+ expectToSwitch = false,
)
}
@@ -148,7 +152,7 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
testDisplayWindowingModeSwitch(
defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
extendedDisplayEnabled = true,
- expectTransition = true,
+ expectToSwitch = true,
)
}
@@ -157,7 +161,7 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
testDisplayWindowingModeSwitch(
defaultWindowingMode = WINDOWING_MODE_FREEFORM,
extendedDisplayEnabled = true,
- expectTransition = false,
+ expectToSwitch = false,
)
}
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 ac1deec53bf6..04acaef344eb 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
@@ -261,6 +261,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver
@Mock private lateinit var packageManager: PackageManager
@Mock private lateinit var mockDisplayContext: Context
+ @Mock private lateinit var dragToDisplayTransitionHandler: DragToDisplayTransitionHandler
private lateinit var controller: DesktopTasksController
private lateinit var shellInit: ShellInit
@@ -431,6 +432,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
desksTransitionsObserver,
userProfileContexts,
desktopModeCompatPolicy,
+ dragToDisplayTransitionHandler,
)
@After
@@ -2069,6 +2071,21 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToFullscreen_fromDeskWithMultipleTasks_deactivatesDesk() {
+ val deskId = 1
+ taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+
+ controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ verify(desksOrganizer).deactivateDesk(wct, deskId = deskId)
+ }
+
+ @Test
fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() {
val task = setUpFreeformTask()
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
@@ -2278,7 +2295,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+ )
fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -2305,29 +2325,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
- fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() {
- val homeTask = setUpHomeTask()
- val task1 = setUpFreeformTask()
- // Setup task2
- setUpFreeformTask()
-
- val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)
- assertNotNull(tdaInfo).configuration.windowConfiguration.windowingMode =
- WINDOWING_MODE_FULLSCREEN
-
- controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN)
-
- val wct = getLatestExitDesktopWct()
- val task1Change = assertNotNull(wct.changes[task1.token.asBinder()])
- assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED)
- verify(desktopModeEnterExitTransitionListener)
- .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION)
- // Does not remove wallpaper activity, as desktop still has a visible desktop task
- wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = false))
- }
-
- @Test
fun moveToFullscreen_nonExistentTask_doesNothing() {
controller.moveToFullscreen(999, transitionSource = UNKNOWN)
verifyExitDesktopWCTNotExecuted()
@@ -4455,7 +4452,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+ )
fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -4480,27 +4480,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
- fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() {
- val homeTask = setUpHomeTask()
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
- val task3 = setUpFreeformTask()
-
- task1.isFocused = false
- task2.isFocused = true
- task3.isFocused = false
- controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
-
- val wct = getLatestExitDesktopWct()
- val taskChange = assertNotNull(wct.changes[task2.token.asBinder()])
- assertThat(taskChange.windowingMode)
- .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
- // Does not remove wallpaper activity
- wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = null))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveFocusedTaskToFullscreen_multipleVisibleTasks_fullscreenOverHome_multiDesksEnabled() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -5031,7 +5010,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
Mockito.argThat { wct ->
return@argThat wct.hierarchyOps[0].isReparent
},
- eq(null),
+ eq(dragToDisplayTransitionHandler),
)
}
@@ -5225,6 +5204,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP,
+ )
fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() {
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt
new file mode 100644
index 000000000000..51c302983fd0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDisplayTransitionHandlerTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.desktopmode
+
+import android.graphics.Point
+import android.graphics.Rect
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.window.TransitionInfo
+import android.window.TransitionRequestInfo
+import com.android.wm.shell.transition.Transitions
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+/**
+ * Test class for {@link DragToDisplayTransitionHandler}
+ *
+ * Usage: atest WMShellUnitTests:DragToDisplayTransitionHandlerTest
+ */
+class DragToDisplayTransitionHandlerTest {
+ private lateinit var handler: DragToDisplayTransitionHandler
+ private val mockTransition: IBinder = mock()
+ private val mockRequestInfo: TransitionRequestInfo = mock()
+ private val mockTransitionInfo: TransitionInfo = mock()
+ private val mockStartTransaction: SurfaceControl.Transaction = mock()
+ private val mockFinishTransaction: SurfaceControl.Transaction = mock()
+ private val mockFinishCallback: Transitions.TransitionFinishCallback = mock()
+
+ @Before
+ fun setUp() {
+ handler = DragToDisplayTransitionHandler()
+ whenever(mockStartTransaction.setWindowCrop(any(), any(), any()))
+ .thenReturn(mockStartTransaction)
+ whenever(mockFinishTransaction.setWindowCrop(any(), any(), any()))
+ .thenReturn(mockFinishTransaction)
+ }
+
+ @Test
+ fun handleRequest_anyRequest_returnsNull() {
+ val result = handler.handleRequest(mockTransition, mockRequestInfo)
+ assert(result == null)
+ }
+
+ @Test
+ fun startAnimation_verifyTransformationsApplied() {
+ val mockChange1 = mock<TransitionInfo.Change>()
+ val leash1 = mock<SurfaceControl>()
+ val endBounds1 = Rect(0, 0, 50, 50)
+ val endPosition1 = Point(5, 5)
+
+ whenever(mockChange1.leash).doReturn(leash1)
+ whenever(mockChange1.endAbsBounds).doReturn(endBounds1)
+ whenever(mockChange1.endRelOffset).doReturn(endPosition1)
+
+ val mockChange2 = mock<TransitionInfo.Change>()
+ val leash2 = mock<SurfaceControl>()
+ val endBounds2 = Rect(100, 100, 200, 150)
+ val endPosition2 = Point(15, 25)
+
+ whenever(mockChange2.leash).doReturn(leash2)
+ whenever(mockChange2.endAbsBounds).doReturn(endBounds2)
+ whenever(mockChange2.endRelOffset).doReturn(endPosition2)
+
+ whenever(mockTransitionInfo.changes).doReturn(listOf(mockChange1, mockChange2))
+
+ handler.startAnimation(
+ mockTransition,
+ mockTransitionInfo,
+ mockStartTransaction,
+ mockFinishTransaction,
+ mockFinishCallback,
+ )
+
+ verify(mockStartTransaction).setWindowCrop(leash1, endBounds1.width(), endBounds1.height())
+ verify(mockStartTransaction)
+ .setPosition(leash1, endPosition1.x.toFloat(), endPosition1.y.toFloat())
+ verify(mockStartTransaction).setWindowCrop(leash2, endBounds2.width(), endBounds2.height())
+ verify(mockStartTransaction)
+ .setPosition(leash2, endPosition2.x.toFloat(), endPosition2.y.toFloat())
+ verify(mockStartTransaction).apply()
+ verify(mockFinishCallback).onTransitionFinished(null)
+ }
+}
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 6f73db0bacc3..677330790bab 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
@@ -1316,9 +1316,11 @@ public class ShellTransitionTests extends ShellTestCase {
mTransactionPool, createTestDisplayController(), mMainExecutor,
mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class),
mock(FocusTransitionObserver.class));
+ final RecentTasksController mockRecentsTaskController = mock(RecentTasksController.class);
+ doReturn(mContext).when(mockRecentsTaskController).getContext();
final RecentsTransitionHandler recentsHandler =
new RecentsTransitionHandler(shellInit, mock(ShellTaskOrganizer.class), transitions,
- mock(RecentTasksController.class), mock(HomeTransitionObserver.class));
+ mockRecentsTaskController, mock(HomeTransitionObserver.class));
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
shellInit.init();
diff --git a/location/java/android/location/GnssMeasurement.java b/location/java/android/location/GnssMeasurement.java
index 200d4ef86146..6ae73a2e6fe5 100644
--- a/location/java/android/location/GnssMeasurement.java
+++ b/location/java/android/location/GnssMeasurement.java
@@ -1484,6 +1484,10 @@ public final class GnssMeasurement implements Parcelable {
* in an open sky test - the important aspect of this output is that changes in this value are
* indicative of changes on input signal power in the frequency band for this measurement.
*
+ * <p> This field is part of the GnssMeasurement object so it is only reported when the GNSS
+ * measurement is reported. E.g., when a GNSS signal is too weak to be acquired, the AGC value
+ * is not reported.
+ *
* <p> The value is only available if {@link #hasAutomaticGainControlLevelDb()} is {@code true}
*
* @deprecated Use {@link GnssMeasurementsEvent#getGnssAutomaticGainControls()} instead.
diff --git a/location/java/android/location/GnssMeasurementsEvent.java b/location/java/android/location/GnssMeasurementsEvent.java
index 4fc2ee8b7fb0..8cdfd013130a 100644
--- a/location/java/android/location/GnssMeasurementsEvent.java
+++ b/location/java/android/location/GnssMeasurementsEvent.java
@@ -158,6 +158,14 @@ public final class GnssMeasurementsEvent implements Parcelable {
/**
* Gets the collection of {@link GnssAutomaticGainControl} associated with the
* current event.
+ *
+ * <p>This field must be reported when the GNSS measurement engine is running, even when the
+ * GnssMeasurement or GnssClock fields are not reported yet. E.g., when a GNSS signal is too
+ * weak to be acquired, the AGC value must still be reported.
+ *
+ * <p>For devices that do not support this field, an empty collection is returned. In that case,
+ * please use {@link GnssMeasurement#hasAutomaticGainControlLevelDb()}
+ * and {@link GnssMeasuremen#getAutomaticGainControlLevelDb()}.
*/
@NonNull
public Collection<GnssAutomaticGainControl> getGnssAutomaticGainControls() {
diff --git a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
index 8b6194fa66f5..fb89973bcc11 100644
--- a/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
+++ b/location/java/com/android/internal/location/GpsNetInitiatedHandler.java
@@ -28,7 +28,6 @@ import android.telephony.emergency.EmergencyNumber;
import android.util.Log;
import com.android.internal.annotations.KeepForWeakReference;
-import com.android.internal.telephony.flags.Flags;
import java.util.concurrent.TimeUnit;
@@ -146,17 +145,12 @@ public class GpsNetInitiatedHandler {
< emergencyExtensionMillis);
boolean isInEmergencyCallback = false;
boolean isInEmergencySmsMode = false;
- if (!Flags.enforceTelephonyFeatureMappingForPublicApis()) {
+ PackageManager pm = mContext.getPackageManager();
+ if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
+ }
+ if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
- } else {
- PackageManager pm = mContext.getPackageManager();
- if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)) {
- isInEmergencyCallback = mTelephonyManager.getEmergencyCallbackMode();
- }
- if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)) {
- isInEmergencySmsMode = mTelephonyManager.isInEmergencySmsMode();
- }
}
return mIsInEmergencyCall || isInEmergencyCallback || isInEmergencyExtension
|| isInEmergencySmsMode;
diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig
index 6d4f0b4f47d5..846448b6afbf 100644
--- a/media/java/android/media/flags/projection.aconfig
+++ b/media/java/android/media/flags/projection.aconfig
@@ -39,3 +39,12 @@ flag {
}
is_exported: true
}
+
+flag {
+ namespace: "media_projection"
+ name: "app_content_sharing"
+ description: "Enable apps to share some sub-surface"
+ bug: "379989921"
+ is_exported: true
+}
+
diff --git a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
index 9d037e91a86f..806580b10cc8 100644
--- a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
+++ b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt
@@ -17,20 +17,20 @@
package com.android.settingslib.widget
import android.content.Context
-import android.os.Build
import android.text.TextUtils
import android.util.AttributeSet
import android.view.View
-import androidx.annotation.RequiresApi
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import com.android.settingslib.widget.preference.intro.R
-class IntroPreference @JvmOverloads constructor(
+class IntroPreference
+@JvmOverloads
+constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
- defStyleRes: Int = 0
+ defStyleRes: Int = 0,
) : Preference(context, attrs, defStyleAttr, defStyleRes), GroupSectionDividerMixin {
private var isCollapsable: Boolean = true
@@ -66,9 +66,9 @@ class IntroPreference @JvmOverloads constructor(
/**
* Sets whether the summary is collapsable.
+ *
* @param collapsable True if the summary should be collapsable, false otherwise.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setCollapsable(collapsable: Boolean) {
isCollapsable = collapsable
minLines = if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES
@@ -77,9 +77,9 @@ class IntroPreference @JvmOverloads constructor(
/**
* Sets the minimum number of lines to display when collapsed.
+ *
* @param lines The minimum number of lines.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setMinLines(lines: Int) {
minLines = lines.coerceIn(1, DEFAULT_MAX_LINES)
notifyChanged()
@@ -87,9 +87,9 @@ class IntroPreference @JvmOverloads constructor(
/**
* Sets the action when clicking on the hyperlink in the text.
+ *
* @param listener The click listener for hyperlink.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setHyperlinkListener(listener: View.OnClickListener) {
if (hyperlinkListener != listener) {
hyperlinkListener = listener
@@ -99,9 +99,9 @@ class IntroPreference @JvmOverloads constructor(
/**
* Sets the action when clicking on the learn more view.
+ *
* @param listener The click listener for learn more.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setLearnMoreAction(listener: View.OnClickListener) {
if (learnMoreListener != listener) {
learnMoreListener = listener
@@ -111,9 +111,9 @@ class IntroPreference @JvmOverloads constructor(
/**
* Sets the text of learn more view.
+ *
* @param text The text of learn more.
*/
- @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
fun setLearnMoreText(text: CharSequence) {
if (!TextUtils.equals(learnMoreText, text)) {
learnMoreText = text
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
index 1cb8005ddae0..02bef9fd2fb2 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -59,8 +59,6 @@ class PreferenceScreenBindingHelper(
private val preferenceHierarchy: PreferenceHierarchy,
) : KeyedDataObservable<String>() {
- private val mainExecutor = HandlerExecutor.main
-
private val preferenceLifecycleContext =
object : PreferenceLifecycleContext(context) {
override val lifecycleScope: LifecycleCoroutineScope
@@ -88,11 +86,11 @@ class PreferenceScreenBindingHelper(
private val preferences: ImmutableMap<String, PreferenceHierarchyNode>
private val dependencies: ImmutableMultimap<String, String>
private val lifecycleAwarePreferences: Array<PreferenceLifecycleProvider>
- private val storages = mutableMapOf<String, KeyedObservable<String>>()
+ private val observables = mutableMapOf<String, KeyedObservable<String>>()
private val preferenceObserver: KeyedObserver<String?>
- private val storageObserver =
+ private val observer =
KeyedObserver<String> { key, reason ->
if (DataChangeReason.isDataChange(reason)) {
notifyChange(key, PreferenceChangeReason.VALUE)
@@ -133,15 +131,19 @@ class PreferenceScreenBindingHelper(
this.dependencies = dependenciesBuilder.build()
this.lifecycleAwarePreferences = lifecycleAwarePreferences.toTypedArray()
+ val executor = HandlerExecutor.main
preferenceObserver = KeyedObserver { key, reason -> onPreferenceChange(key, reason) }
- addObserver(preferenceObserver, mainExecutor)
+ addObserver(preferenceObserver, executor)
preferenceScreen.forEachRecursively {
- it.preferenceDataStore?.findKeyValueStore()?.let { keyValueStore ->
- val key = it.key
- storages[key] = keyValueStore
- keyValueStore.addObserver(key, storageObserver, mainExecutor)
- }
+ val key = it.key ?: return@forEachRecursively
+ @Suppress("UNCHECKED_CAST")
+ val observable =
+ it.preferenceDataStore?.findKeyValueStore()
+ ?: (preferences[key]?.metadata as? KeyedObservable<String>)
+ ?: return@forEachRecursively
+ observables[key] = observable
+ observable.addObserver(key, observer, executor)
}
}
@@ -212,7 +214,7 @@ class PreferenceScreenBindingHelper(
fun onDestroy() {
removeObserver(preferenceObserver)
- for ((key, storage) in storages) storage.removeObserver(key, storageObserver)
+ for ((key, observable) in observables) observable.removeObserver(key, observer)
for (preference in lifecycleAwarePreferences) {
preference.onDestroy(preferenceLifecycleContext)
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
index ebd5a1deffd2..3625c002e9d8 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -29,6 +29,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.UserHandle;
+import android.os.UserManager;
import android.telephony.TelephonyManager;
import android.util.Log;
@@ -37,6 +38,8 @@ import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.settingslib.R;
+import com.android.settingslib.flags.Flags;
+import com.android.settingslib.utils.ThreadUtils;
import java.util.Collection;
import java.util.HashMap;
@@ -65,6 +68,7 @@ public class BluetoothEventManager {
private final android.os.Handler mReceiverHandler;
private final UserHandle mUserHandle;
private final Context mContext;
+ private boolean mIsWorkProfile = false;
interface Handler {
void onReceive(Context context, Intent intent, BluetoothDevice device);
@@ -140,6 +144,9 @@ public class BluetoothEventManager {
addHandler(BluetoothAdapter.ACTION_AUTO_ON_STATE_CHANGED, new AutoOnStateChangedHandler());
registerAdapterIntentReceiver();
+
+ UserManager userManager = context.getSystemService(UserManager.class);
+ mIsWorkProfile = userManager != null && userManager.isManagedProfile();
}
/** Register to start receiving callbacks for Bluetooth events. */
@@ -220,20 +227,32 @@ public class BluetoothEventManager {
callback.onProfileConnectionStateChanged(device, state, bluetoothProfile);
}
+ if (mIsWorkProfile) {
+ Log.d(TAG, "Skip profileConnectionStateChanged for audio sharing, work profile");
+ return;
+ }
+
+ LocalBluetoothLeBroadcast broadcast = mBtManager == null ? null
+ : mBtManager.getProfileManager().getLeAudioBroadcastProfile();
+ LocalBluetoothLeBroadcastAssistant assistant = mBtManager == null ? null
+ : mBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
// Trigger updateFallbackActiveDeviceIfNeeded when ASSISTANT profile disconnected when
// audio sharing is enabled.
if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
&& state == BluetoothAdapter.STATE_DISCONNECTED
- && BluetoothUtils.isAudioSharingUIAvailable(mContext)) {
- LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
- if (profileManager != null
- && profileManager.getLeAudioBroadcastProfile() != null
- && profileManager.getLeAudioBroadcastProfile().isProfileReady()
- && profileManager.getLeAudioBroadcastAssistantProfile() != null
- && profileManager.getLeAudioBroadcastAssistantProfile().isProfileReady()) {
- Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, ASSISTANT profile disconnected");
- profileManager.getLeAudioBroadcastProfile().updateFallbackActiveDeviceIfNeeded();
- }
+ && BluetoothUtils.isAudioSharingUIAvailable(mContext)
+ && broadcast != null && assistant != null && broadcast.isProfileReady()
+ && assistant.isProfileReady()) {
+ Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, ASSISTANT profile disconnected");
+ broadcast.updateFallbackActiveDeviceIfNeeded();
+ }
+ // Dispatch handleOnProfileStateChanged to local broadcast profile
+ if (Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()
+ && broadcast != null
+ && state == BluetoothAdapter.STATE_CONNECTED) {
+ Log.d(TAG, "dispatchProfileConnectionStateChanged to local broadcast profile");
+ var unused = ThreadUtils.postOnBackgroundThread(
+ () -> broadcast.handleProfileConnected(device, bluetoothProfile, mBtManager));
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 31948e49b4ce..e78a69239334 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -719,6 +719,30 @@ public class BluetoothUtils {
}
}
+ /** Check if the {@link CachedBluetoothDevice} is a media device */
+ @WorkerThread
+ public static boolean isMediaDevice(@Nullable CachedBluetoothDevice cachedDevice) {
+ if (cachedDevice == null) return false;
+ return cachedDevice.getProfiles().stream()
+ .anyMatch(
+ profile ->
+ profile instanceof A2dpProfile
+ || profile instanceof HearingAidProfile
+ || profile instanceof LeAudioProfile
+ || profile instanceof HeadsetProfile);
+ }
+
+ /** Check if the {@link CachedBluetoothDevice} supports LE Audio profile */
+ @WorkerThread
+ public static boolean isLeAudioSupported(@Nullable CachedBluetoothDevice cachedDevice) {
+ if (cachedDevice == null) return false;
+ return cachedDevice.getProfiles().stream()
+ .anyMatch(
+ profile ->
+ profile instanceof LeAudioProfile
+ && profile.isEnabled(cachedDevice.getDevice()));
+ }
+
/** Returns if the broadcast is on-going. */
@WorkerThread
public static boolean isBroadcasting(@Nullable LocalBluetoothManager manager) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index f18a2da27a70..08f7806207db 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -54,6 +54,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.annotation.WorkerThread;
import com.android.settingslib.R;
import com.android.settingslib.flags.Flags;
@@ -64,6 +65,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
+import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -107,6 +109,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
private static final String SETTINGS_PKG = "com.android.settings";
private static final String SYSUI_PKG = "com.android.systemui";
private static final String TAG = "LocalBluetoothLeBroadcast";
+ private static final String AUTO_REJOIN_BROADCAST_TAG = "REJOIN_LE_BROADCAST_ID";
private static final boolean DEBUG = BluetoothUtils.D;
private static final String VALID_PASSWORD_CHARACTERS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}|;:,"
@@ -120,6 +123,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
// Order of this profile in device profiles list
private static final int ORDINAL = 1;
static final int UNKNOWN_VALUE_PLACEHOLDER = -1;
+ private static final int JUST_BOND_MILLIS_THRESHOLD = 30000; // 30s
private static final Uri[] SETTINGS_URIS =
new Uri[] {
Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_NAME),
@@ -1283,4 +1287,87 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile {
UserManager userManager = context.getSystemService(UserManager.class);
return userManager != null && userManager.isManagedProfile();
}
+
+ /** Handle profile connected for {@link CachedBluetoothDevice}. */
+ @WorkerThread
+ public void handleProfileConnected(@NonNull CachedBluetoothDevice cachedDevice,
+ int bluetoothProfile, @Nullable LocalBluetoothManager btManager) {
+ if (!Flags.promoteAudioSharingForSecondAutoConnectedLeaDevice()) {
+ Log.d(TAG, "Skip handleProfileConnected, flag off");
+ return;
+ }
+ if (!SYSUI_PKG.equals(mContext.getPackageName())) {
+ Log.d(TAG, "Skip handleProfileConnected, not a valid caller");
+ return;
+ }
+ if (!BluetoothUtils.isMediaDevice(cachedDevice)) {
+ Log.d(TAG, "Skip handleProfileConnected, not a media device");
+ return;
+ }
+ Timestamp bondTimestamp = cachedDevice.getBondTimestamp();
+ if (bondTimestamp != null) {
+ long diff = System.currentTimeMillis() - bondTimestamp.getTime();
+ if (diff <= JUST_BOND_MILLIS_THRESHOLD) {
+ Log.d(TAG, "Skip handleProfileConnected, just bond within " + diff);
+ return;
+ }
+ }
+ if (!isEnabled(null)) {
+ Log.d(TAG, "Skip handleProfileConnected, not broadcasting");
+ return;
+ }
+ BluetoothDevice device = cachedDevice.getDevice();
+ if (device == null) {
+ Log.d(TAG, "Skip handleProfileConnected, null device");
+ return;
+ }
+ // TODO: sync source in a reasonable place
+ if (BluetoothUtils.hasConnectedBroadcastSourceForBtDevice(device, btManager)) {
+ Log.d(TAG, "Skip handleProfileConnected, already has source");
+ return;
+ }
+ if (isAutoRejoinDevice(device)) {
+ Log.d(TAG, "Skip handleProfileConnected, auto rejoin device");
+ return;
+ }
+ boolean isLeAudioSupported = BluetoothUtils.isLeAudioSupported(cachedDevice);
+ // For eligible (LE audio) remote device, we only check assistant profile connected.
+ if (isLeAudioSupported
+ && bluetoothProfile != BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT) {
+ Log.d(TAG, "Skip handleProfileConnected, lea sink, not the assistant profile");
+ return;
+ }
+ boolean isFirstConnectedProfile = isFirstConnectedProfile(cachedDevice, bluetoothProfile);
+ // For ineligible (classic) remote device, we only check its first connected profile.
+ if (!isLeAudioSupported && !isFirstConnectedProfile) {
+ Log.d(TAG, "Skip handleProfileConnected, classic sink, not the first profile");
+ return;
+ }
+
+ Intent intent = new Intent(
+ LocalBluetoothLeBroadcast.ACTION_LE_AUDIO_SHARING_DEVICE_CONNECTED);
+ intent.putExtra(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device);
+ intent.setPackage(SETTINGS_PKG);
+ Log.d(TAG, "notify device connected, device = " + device.getAnonymizedAddress());
+
+ mContext.sendBroadcast(intent);
+ }
+
+ private boolean isAutoRejoinDevice(@Nullable BluetoothDevice bluetoothDevice) {
+ String metadataValue = BluetoothUtils.getFastPairCustomizedField(bluetoothDevice,
+ AUTO_REJOIN_BROADCAST_TAG);
+ return getLatestBroadcastId() != UNKNOWN_VALUE_PLACEHOLDER && Objects.equals(metadataValue,
+ String.valueOf(getLatestBroadcastId()));
+ }
+
+ private boolean isFirstConnectedProfile(@Nullable CachedBluetoothDevice cachedDevice,
+ int bluetoothProfile) {
+ if (cachedDevice == null) return false;
+ return cachedDevice.getProfiles().stream()
+ .noneMatch(
+ profile ->
+ profile.getProfileId() != bluetoothProfile
+ && profile.getConnectionStatus(cachedDevice.getDevice())
+ == BluetoothProfile.STATE_CONNECTED);
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java
index ae17acb5104b..8bb41ccf9600 100644
--- a/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java
+++ b/packages/SettingsLib/src/com/android/settingslib/qrcode/QrCamera.java
@@ -16,8 +16,8 @@
package com.android.settingslib.qrcode;
+import android.annotation.NonNull;
import android.content.Context;
-import android.content.res.Configuration;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
@@ -75,12 +75,29 @@ public class QrCamera extends Handler {
@VisibleForTesting
Camera mCamera;
+ Camera.CameraInfo mCameraInfo;
+
+ /**
+ * The size of the preview image as requested to camera, e.g. 1920x1080.
+ */
private Size mPreviewSize;
+
+ /**
+ * Whether the preview image would be displayed in "portrait" (width less
+ * than height) orientation in current display orientation.
+ *
+ * Note that we don't distinguish between a rotation of 90 degrees or 270
+ * degrees here, since we center crop all the preview.
+ *
+ * TODO: Handle external camera / multiple display, this likely requires
+ * migrating to newer Camera2 API.
+ */
+ private boolean mPreviewInPortrait;
+
private WeakReference<Context> mContext;
private ScannerCallback mScannerCallback;
private MultiFormatReader mReader;
private DecodingTask mDecodeTask;
- private int mCameraOrientation;
@VisibleForTesting
Camera.Parameters mParameters;
@@ -152,8 +169,14 @@ public class QrCamera extends Handler {
* @param previewSize Is the preview size set by camera
* @param cameraOrientation Is the orientation of current Camera
* @return The rectangle would like to crop from the camera preview shot.
+ * @deprecated This is no longer used, and the frame position is
+ * automatically calculated from the preview size and the
+ * background View size.
*/
- Rect getFramePosition(Size previewSize, int cameraOrientation);
+ @Deprecated
+ default @NonNull Rect getFramePosition(@NonNull Size previewSize, int cameraOrientation) {
+ throw new AssertionError("getFramePosition shouldn't be used");
+ }
/**
* Sets the transform to associate with preview area.
@@ -172,6 +195,41 @@ public class QrCamera extends Handler {
boolean isValid(String qrCode);
}
+ private boolean setPreviewDisplayOrientation() {
+ if (mContext.get() == null) {
+ return false;
+ }
+
+ final WindowManager winManager =
+ (WindowManager) mContext.get().getSystemService(Context.WINDOW_SERVICE);
+ final int rotation = winManager.getDefaultDisplay().getRotation();
+ int degrees = 0;
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ degrees = 0;
+ break;
+ case Surface.ROTATION_90:
+ degrees = 90;
+ break;
+ case Surface.ROTATION_180:
+ degrees = 180;
+ break;
+ case Surface.ROTATION_270:
+ degrees = 270;
+ break;
+ }
+ int rotateDegrees = 0;
+ if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ rotateDegrees = (mCameraInfo.orientation + degrees) % 360;
+ rotateDegrees = (360 - rotateDegrees) % 360; // compensate the mirror
+ } else {
+ rotateDegrees = (mCameraInfo.orientation - degrees + 360) % 360;
+ }
+ mCamera.setDisplayOrientation(rotateDegrees);
+ mPreviewInPortrait = (rotateDegrees == 90 || rotateDegrees == 270);
+ return true;
+ }
+
@VisibleForTesting
void setCameraParameter() {
mParameters = mCamera.getParameters();
@@ -195,37 +253,39 @@ public class QrCamera extends Handler {
mCamera.setParameters(mParameters);
}
- private boolean startPreview() {
- if (mContext.get() == null) {
- return false;
- }
+ /**
+ * Set transform matrix to crop and center the preview picture.
+ */
+ private void setTransformationMatrix() {
+ final Size previewDisplaySize = rotateIfPortrait(mPreviewSize);
+ final Size viewSize = mScannerCallback.getViewSize();
+ final Rect cropRegion = calculateCenteredCrop(previewDisplaySize, viewSize);
- final WindowManager winManager =
- (WindowManager) mContext.get().getSystemService(Context.WINDOW_SERVICE);
- final int rotation = winManager.getDefaultDisplay().getRotation();
- int degrees = 0;
- switch (rotation) {
- case Surface.ROTATION_0:
- degrees = 0;
- break;
- case Surface.ROTATION_90:
- degrees = 90;
- break;
- case Surface.ROTATION_180:
- degrees = 180;
- break;
- case Surface.ROTATION_270:
- degrees = 270;
- break;
- }
- final int rotateDegrees = (mCameraOrientation - degrees + 360) % 360;
- mCamera.setDisplayOrientation(rotateDegrees);
+ // Note that strictly speaking, since the preview is mirrored in front
+ // camera case, we should also mirror the crop region here. But since
+ // we're cropping at the center, mirroring would result in the same
+ // crop region other than small off-by-one error from floating point
+ // calculation and wouldn't be noticeable.
+
+ // Calculate transformation matrix.
+ float scaleX = previewDisplaySize.getWidth() / (float) cropRegion.width();
+ float scaleY = previewDisplaySize.getHeight() / (float) cropRegion.height();
+ float translateX = -cropRegion.left / (float) cropRegion.width() * viewSize.getWidth();
+ float translateY = -cropRegion.top / (float) cropRegion.height() * viewSize.getHeight();
+
+ // Set the transform matrix.
+ final Matrix matrix = new Matrix();
+ matrix.setScale(scaleX, scaleY);
+ matrix.postTranslate(translateX, translateY);
+ mScannerCallback.setTransform(matrix);
+ }
+
+ private void startPreview() {
mCamera.startPreview();
if (Camera.Parameters.FOCUS_MODE_AUTO.equals(mParameters.getFocusMode())) {
mCamera.autoFocus(/* Camera.AutoFocusCallback */ null);
sendMessageDelayed(obtainMessage(MSG_AUTO_FOCUS), AUTOFOCUS_INTERVAL_MS);
}
- return true;
}
private class DecodingTask extends AsyncTask<Void, Void, String> {
@@ -300,7 +360,7 @@ public class QrCamera extends Handler {
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
releaseCamera();
mCamera = Camera.open(i);
- mCameraOrientation = cameraInfo.orientation;
+ mCameraInfo = cameraInfo;
break;
}
}
@@ -309,7 +369,7 @@ public class QrCamera extends Handler {
Camera.getCameraInfo(0, cameraInfo);
releaseCamera();
mCamera = Camera.open(0);
- mCameraOrientation = cameraInfo.orientation;
+ mCameraInfo = cameraInfo;
}
} catch (RuntimeException e) {
Log.e(TAG, "Fail to open camera: " + e);
@@ -323,11 +383,12 @@ public class QrCamera extends Handler {
throw new IOException("Cannot find available camera");
}
mCamera.setPreviewTexture(surface);
+ if (!setPreviewDisplayOrientation()) {
+ throw new IOException("Lost context");
+ }
setCameraParameter();
setTransformationMatrix();
- if (!startPreview()) {
- throw new IOException("Lost contex");
- }
+ startPreview();
} catch (IOException ioe) {
Log.e(TAG, "Fail to startPreview camera: " + ioe);
mCamera = null;
@@ -345,32 +406,30 @@ public class QrCamera extends Handler {
}
}
- /** Set transform matrix to crop and center the preview picture */
- private void setTransformationMatrix() {
- final boolean isPortrait = mContext.get().getResources().getConfiguration().orientation
- == Configuration.ORIENTATION_PORTRAIT;
-
- final int previewWidth = isPortrait ? mPreviewSize.getWidth() : mPreviewSize.getHeight();
- final int previewHeight = isPortrait ? mPreviewSize.getHeight() : mPreviewSize.getWidth();
- final float ratioPreview = (float) getRatio(previewWidth, previewHeight);
-
- // Calculate transformation matrix.
- float scaleX = 1.0f;
- float scaleY = 1.0f;
- if (previewWidth > previewHeight) {
- scaleY = scaleX / ratioPreview;
+ /**
+ * Calculates the crop region in `previewSize` to have the same aspect
+ * ratio as `viewSize` and center aligned.
+ */
+ private Rect calculateCenteredCrop(Size previewSize, Size viewSize) {
+ final double previewRatio = getRatio(previewSize);
+ final double viewRatio = getRatio(viewSize);
+ int width;
+ int height;
+ if (previewRatio > viewRatio) {
+ width = previewSize.getWidth();
+ height = (int) Math.round(width * viewRatio);
} else {
- scaleX = scaleY / ratioPreview;
+ height = previewSize.getHeight();
+ width = (int) Math.round(height / viewRatio);
}
-
- // Set the transform matrix.
- final Matrix matrix = new Matrix();
- matrix.setScale(scaleX, scaleY);
- mScannerCallback.setTransform(matrix);
+ final int left = (previewSize.getWidth() - width) / 2;
+ final int top = (previewSize.getHeight() - height) / 2;
+ return new Rect(left, top, left + width, top + height);
}
private QrYuvLuminanceSource getFrameImage(byte[] imageData) {
- final Rect frame = mScannerCallback.getFramePosition(mPreviewSize, mCameraOrientation);
+ final Size viewSize = mScannerCallback.getViewSize();
+ final Rect frame = calculateCenteredCrop(mPreviewSize, rotateIfPortrait(viewSize));
final QrYuvLuminanceSource image = new QrYuvLuminanceSource(imageData,
mPreviewSize.getWidth(), mPreviewSize.getHeight());
return (QrYuvLuminanceSource)
@@ -398,17 +457,18 @@ public class QrCamera extends Handler {
*/
private Size getBestPreviewSize(Camera.Parameters parameters) {
final double minRatioDiffPercent = 0.1;
- final Size windowSize = mScannerCallback.getViewSize();
- final double winRatio = getRatio(windowSize.getWidth(), windowSize.getHeight());
+ final Size viewSize = rotateIfPortrait(mScannerCallback.getViewSize());
+ final double viewRatio = getRatio(viewSize);
double bestChoiceRatio = 0;
Size bestChoice = new Size(0, 0);
for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
- double ratio = getRatio(size.width, size.height);
+ final Size newSize = toAndroidSize(size);
+ final double ratio = getRatio(newSize);
if (size.height * size.width > bestChoice.getWidth() * bestChoice.getHeight()
- && (Math.abs(bestChoiceRatio - winRatio) / winRatio > minRatioDiffPercent
- || Math.abs(ratio - winRatio) / winRatio <= minRatioDiffPercent)) {
- bestChoice = new Size(size.width, size.height);
- bestChoiceRatio = getRatio(size.width, size.height);
+ && (Math.abs(bestChoiceRatio - viewRatio) / viewRatio > minRatioDiffPercent
+ || Math.abs(ratio - viewRatio) / viewRatio <= minRatioDiffPercent)) {
+ bestChoice = newSize;
+ bestChoiceRatio = ratio;
}
}
return bestChoice;
@@ -419,25 +479,26 @@ public class QrCamera extends Handler {
* picture size and aspect ratio to choose the best one.
*/
private Size getBestPictureSize(Camera.Parameters parameters) {
- final Camera.Size previewSize = parameters.getPreviewSize();
- final double previewRatio = getRatio(previewSize.width, previewSize.height);
+ final Size previewSize = mPreviewSize;
+ final double previewRatio = getRatio(previewSize);
List<Size> bestChoices = new ArrayList<>();
final List<Size> similarChoices = new ArrayList<>();
// Filter by ratio
- for (Camera.Size size : parameters.getSupportedPictureSizes()) {
- double ratio = getRatio(size.width, size.height);
+ for (Camera.Size picSize : parameters.getSupportedPictureSizes()) {
+ final Size size = toAndroidSize(picSize);
+ final double ratio = getRatio(size);
if (ratio == previewRatio) {
- bestChoices.add(new Size(size.width, size.height));
+ bestChoices.add(size);
} else if (Math.abs(ratio - previewRatio) < MAX_RATIO_DIFF) {
- similarChoices.add(new Size(size.width, size.height));
+ similarChoices.add(size);
}
}
if (bestChoices.size() == 0 && similarChoices.size() == 0) {
Log.d(TAG, "No proper picture size, return default picture size");
Camera.Size defaultPictureSize = parameters.getPictureSize();
- return new Size(defaultPictureSize.width, defaultPictureSize.height);
+ return toAndroidSize(defaultPictureSize);
}
if (bestChoices.size() == 0) {
@@ -447,7 +508,7 @@ public class QrCamera extends Handler {
// Get the best by area
int bestAreaDifference = Integer.MAX_VALUE;
Size bestChoice = null;
- final int previewArea = previewSize.width * previewSize.height;
+ final int previewArea = previewSize.getWidth() * previewSize.getHeight();
for (Size size : bestChoices) {
int areaDifference = Math.abs(size.getWidth() * size.getHeight() - previewArea);
if (areaDifference < bestAreaDifference) {
@@ -458,8 +519,20 @@ public class QrCamera extends Handler {
return bestChoice;
}
- private double getRatio(double x, double y) {
- return (x < y) ? x / y : y / x;
+ private Size rotateIfPortrait(Size size) {
+ if (mPreviewInPortrait) {
+ return new Size(size.getHeight(), size.getWidth());
+ } else {
+ return size;
+ }
+ }
+
+ private double getRatio(Size size) {
+ return size.getHeight() / (double) size.getWidth();
+ }
+
+ private Size toAndroidSize(Camera.Size size) {
+ return new Size(size.width, size.height);
}
@VisibleForTesting
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
index b86f4b3715b5..eac6923473b1 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java
@@ -23,7 +23,6 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -38,12 +37,14 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.telephony.TelephonyManager;
import com.android.settingslib.R;
import com.android.settingslib.flags.Flags;
import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter;
+import com.android.settingslib.utils.ThreadUtils;
import org.junit.Before;
import org.junit.Rule;
@@ -54,6 +55,8 @@ import org.mockito.MockitoAnnotations;
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.shadow.api.Shadow;
import java.util.ArrayList;
@@ -61,7 +64,7 @@ import java.util.Collections;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
-@Config(shadows = {ShadowBluetoothAdapter.class})
+@Config(shadows = {ShadowBluetoothAdapter.class, BluetoothEventManagerTest.ShadowThreadUtils.class})
public class BluetoothEventManagerTest {
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -100,6 +103,8 @@ public class BluetoothEventManagerTest {
private BluetoothUtils.ErrorListener mErrorListener;
@Mock
private LocalBluetoothLeBroadcast mBroadcast;
+ @Mock
+ private UserManager mUserManager;
private Context mContext;
private Intent mIntent;
@@ -130,6 +135,7 @@ public class BluetoothEventManagerTest {
mCachedDevice1 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice1);
mCachedDevice2 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice2);
mCachedDevice3 = new CachedBluetoothDevice(mContext, mLocalProfileManager, mDevice3);
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
BluetoothUtils.setErrorListener(mErrorListener);
}
@@ -196,6 +202,7 @@ public class BluetoothEventManagerTest {
* callback.
*/
@Test
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
public void dispatchProfileConnectionStateChanged_registerCallback_shouldDispatchCallback() {
mBluetoothEventManager.registerCallback(mBluetoothCallback);
@@ -208,10 +215,12 @@ public class BluetoothEventManagerTest {
/**
* dispatchProfileConnectionStateChanged should not call {@link
- * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when audio sharing flag is off.
+ * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and
+ * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when audio sharing flag is off.
*/
@Test
- public void dispatchProfileConnectionStateChanged_flagOff_noUpdateFallbackDevice() {
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_flagOff_noCallToBroadcastProfile() {
setUpAudioSharing(/* enableFlag= */ false, /* enableFeature= */ true, /* enableProfile= */
true, /* workProfile= */ false);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -219,16 +228,19 @@ public class BluetoothEventManagerTest {
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
}
/**
* dispatchProfileConnectionStateChanged should not call {@link
- * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when the device does not
- * support audio sharing.
+ * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and
+ * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when the device does not support
+ * audio sharing.
*/
@Test
- public void dispatchProfileConnectionStateChanged_notSupport_noUpdateFallbackDevice() {
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_notSupport_noCallToBroadcastProfile() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ false, /* enableProfile= */
true, /* workProfile= */ false);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -236,7 +248,8 @@ public class BluetoothEventManagerTest {
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
}
/**
@@ -245,6 +258,7 @@ public class BluetoothEventManagerTest {
* not ready.
*/
@Test
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
public void dispatchProfileConnectionStateChanged_profileNotReady_noUpdateFallbackDevice() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
false, /* workProfile= */ false);
@@ -253,7 +267,7 @@ public class BluetoothEventManagerTest {
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
}
/**
@@ -262,6 +276,7 @@ public class BluetoothEventManagerTest {
* other than LE_AUDIO_BROADCAST_ASSISTANT or state other than STATE_DISCONNECTED.
*/
@Test
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
public void dispatchProfileConnectionStateChanged_notAssistantProfile_noUpdateFallbackDevice() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
true, /* workProfile= */ false);
@@ -270,16 +285,17 @@ public class BluetoothEventManagerTest {
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO);
- verify(mBroadcast, times(0)).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
}
/**
* dispatchProfileConnectionStateChanged should not call {@link
- * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when triggered for
- * work profile.
+ * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded and
+ * {@link LocalBluetoothLeBroadcast}#handleProfileConnected when triggered for work profile.
*/
@Test
- public void dispatchProfileConnectionStateChanged_workProfile_noUpdateFallbackDevice() {
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_workProfile_noCallToBroadcastProfile() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
true, /* workProfile= */ true);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -287,7 +303,8 @@ public class BluetoothEventManagerTest {
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
- verify(mBroadcast).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
}
/**
@@ -296,7 +313,8 @@ public class BluetoothEventManagerTest {
* disconnected and audio sharing is enabled.
*/
@Test
- public void dispatchProfileConnectionStateChanged_audioSharing_updateFallbackDevice() {
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_assistDisconnected_updateFallbackDevice() {
setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
true, /* workProfile= */ false);
mBluetoothEventManager.dispatchProfileConnectionStateChanged(
@@ -305,6 +323,27 @@ public class BluetoothEventManagerTest {
BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
verify(mBroadcast).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast, never()).handleProfileConnected(any(), anyInt(), any());
+ }
+
+ /**
+ * dispatchProfileConnectionStateChanged should call {@link
+ * LocalBluetoothLeBroadcast}#handleProfileConnected when assistant profile is connected and
+ * audio sharing is enabled.
+ */
+ @Test
+ @EnableFlags(Flags.FLAG_PROMOTE_AUDIO_SHARING_FOR_SECOND_AUTO_CONNECTED_LEA_DEVICE)
+ public void dispatchProfileConnectionStateChanged_assistConnected_handleStateChanged() {
+ setUpAudioSharing(/* enableFlag= */ true, /* enableFeature= */ true, /* enableProfile= */
+ true, /* workProfile= */ false);
+ mBluetoothEventManager.dispatchProfileConnectionStateChanged(
+ mCachedBluetoothDevice,
+ BluetoothProfile.STATE_CONNECTED,
+ BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+
+ verify(mBroadcast, never()).updateFallbackActiveDeviceIfNeeded();
+ verify(mBroadcast).handleProfileConnected(mCachedBluetoothDevice,
+ BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT, mBtManager);
}
private void setUpAudioSharing(boolean enableFlag, boolean enableFeature,
@@ -325,13 +364,19 @@ public class BluetoothEventManagerTest {
LocalBluetoothLeBroadcastAssistant assistant =
mock(LocalBluetoothLeBroadcastAssistant.class);
when(assistant.isProfileReady()).thenReturn(enableProfile);
- LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class);
- when(profileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
- when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
- when(mBtManager.getProfileManager()).thenReturn(profileManager);
- UserManager userManager = mock(UserManager.class);
- when(mContext.getSystemService(UserManager.class)).thenReturn(userManager);
- when(userManager.isManagedProfile()).thenReturn(workProfile);
+ when(mLocalProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast);
+ when(mLocalProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant);
+ when(mUserManager.isManagedProfile()).thenReturn(workProfile);
+ if (workProfile) {
+ mBluetoothEventManager =
+ new BluetoothEventManager(
+ mLocalAdapter,
+ mBtManager,
+ mCachedDeviceManager,
+ mContext,
+ /* handler= */ null,
+ /* userHandle= */ null);
+ }
}
@Test
@@ -665,4 +710,12 @@ public class BluetoothEventManagerTest {
verify(mBluetoothCallback).onAutoOnStateChanged(anyInt());
}
+
+ @Implements(value = ThreadUtils.class)
+ public static class ShadowThreadUtils {
+ @Implementation
+ protected static void postOnBackgroundThread(Runnable runnable) {
+ runnable.run();
+ }
+ }
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index 0325c0ec7915..b7814127b716 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -1349,6 +1349,36 @@ public class BluetoothUtilsTest {
}
@Test
+ public void isMediaDevice_returnsFalse() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mAssistant));
+ assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isFalse();
+ }
+
+ @Test
+ public void isMediaDevice_returnsTrue() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+ assertThat(BluetoothUtils.isMediaDevice(mCachedBluetoothDevice)).isTrue();
+ }
+
+ @Test
+ public void isLeAudioSupported_returnsFalse() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+ when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(false);
+
+ assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isFalse();
+ }
+
+ @Test
+ public void isLeAudioSupported_returnsTrue() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(ImmutableList.of(mLeAudioProfile));
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+ when(mLeAudioProfile.isEnabled(mBluetoothDevice)).thenReturn(true);
+
+ assertThat(BluetoothUtils.isLeAudioSupported(mCachedBluetoothDevice)).isTrue();
+ }
+
+ @Test
public void isTemporaryBondDevice_hasMetadata_returnsTrue() {
when(mBluetoothDevice.getMetadata(METADATA_FAST_PAIR_CUSTOMIZED_FIELDS))
.thenReturn(TEMP_BOND_METADATA.getBytes());
diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java
index a9fd380c2733..76b6aa8b5026 100644
--- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java
+++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowColorDisplayManager.java
@@ -28,6 +28,7 @@ import org.robolectric.annotation.Implements;
public class ShadowColorDisplayManager extends org.robolectric.shadows.ShadowColorDisplayManager {
private boolean mIsReduceBrightColorsActivated;
+ private int mColorMode;
@Implementation
@SystemApi
@@ -43,4 +44,13 @@ public class ShadowColorDisplayManager extends org.robolectric.shadows.ShadowCol
return mIsReduceBrightColorsActivated;
}
+ @Implementation
+ public int getColorMode() {
+ return mColorMode;
+ }
+
+ @Implementation
+ public void setColorMode(int colorMode) {
+ mColorMode = colorMode;
+ }
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
index ed144bd20234..375dadeba3ad 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/AndroidColorScheme.kt
@@ -78,6 +78,10 @@ class AndroidColorScheme(
val underSurface: Color,
val weatherTemp: Color,
val widgetBackground: Color,
+ val surfaceEffect0: Color,
+ val surfaceEffect1: Color,
+ val surfaceEffect2: Color,
+ val surfaceEffect3: Color,
) {
companion object {
internal fun color(context: Context, @ColorRes id: Int): Color {
@@ -123,6 +127,10 @@ class AndroidColorScheme(
underSurface = color(context, R.color.customColorUnderSurface),
weatherTemp = color(context, R.color.customColorWeatherTemp),
widgetBackground = color(context, R.color.customColorWidgetBackground),
+ surfaceEffect0 = color(context, R.color.surface_effect_0),
+ surfaceEffect1 = color(context, R.color.surface_effect_1),
+ surfaceEffect2 = color(context, R.color.surface_effect_2),
+ surfaceEffect3 = color(context, R.color.surface_effect_3),
)
}
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
index 71ec63c1666c..84370ed4d2c7 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt
@@ -31,6 +31,7 @@ import com.android.compose.theme.typography.TypeScaleTokens
import com.android.compose.theme.typography.TypefaceNames
import com.android.compose.theme.typography.TypefaceTokens
import com.android.compose.theme.typography.TypographyTokens
+import com.android.compose.theme.typography.VariableFontTypeScaleEmphasizedTokens
import com.android.compose.theme.typography.platformTypography
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.compose.windowsizeclass.calculateWindowSizeClass
@@ -44,9 +45,15 @@ fun PlatformTheme(isDarkTheme: Boolean = isSystemInDarkTheme(), content: @Compos
val colorScheme = remember(context, isDarkTheme) { platformColorScheme(isDarkTheme, context) }
val androidColorScheme = remember(context) { AndroidColorScheme(context) }
val typefaceNames = remember(context) { TypefaceNames.get(context) }
+ val typefaceTokens = remember(typefaceNames) { TypefaceTokens(typefaceNames) }
val typography =
- remember(typefaceNames) {
- platformTypography(TypographyTokens(TypeScaleTokens(TypefaceTokens(typefaceNames))))
+ remember(typefaceTokens) {
+ platformTypography(
+ TypographyTokens(
+ TypeScaleTokens(typefaceTokens),
+ VariableFontTypeScaleEmphasizedTokens(typefaceTokens),
+ )
+ )
}
val windowSizeClass = calculateWindowSizeClass()
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt
index 1ce1ae3c8a32..652f946a8e70 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/PlatformTypography.kt
@@ -16,6 +16,7 @@
package com.android.compose.theme.typography
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography
@@ -25,6 +26,7 @@ import androidx.compose.material3.Typography
* Do not use directly and call [MaterialTheme.typography] instead to access the different text
* styles.
*/
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
internal fun platformTypography(typographyTokens: TypographyTokens): Typography {
return Typography(
displayLarge = typographyTokens.displayLarge,
@@ -42,5 +44,21 @@ internal fun platformTypography(typographyTokens: TypographyTokens): Typography
labelLarge = typographyTokens.labelLarge,
labelMedium = typographyTokens.labelMedium,
labelSmall = typographyTokens.labelSmall,
+ // GSF emphasized tokens
+ displayLargeEmphasized = typographyTokens.displayLargeEmphasized,
+ displayMediumEmphasized = typographyTokens.displayMediumEmphasized,
+ displaySmallEmphasized = typographyTokens.displaySmallEmphasized,
+ headlineLargeEmphasized = typographyTokens.headlineLargeEmphasized,
+ headlineMediumEmphasized = typographyTokens.headlineMediumEmphasized,
+ headlineSmallEmphasized = typographyTokens.headlineSmallEmphasized,
+ titleLargeEmphasized = typographyTokens.titleLargeEmphasized,
+ titleMediumEmphasized = typographyTokens.titleMediumEmphasized,
+ titleSmallEmphasized = typographyTokens.titleSmallEmphasized,
+ bodyLargeEmphasized = typographyTokens.bodyLargeEmphasized,
+ bodyMediumEmphasized = typographyTokens.bodyMediumEmphasized,
+ bodySmallEmphasized = typographyTokens.bodySmallEmphasized,
+ labelLargeEmphasized = typographyTokens.labelLargeEmphasized,
+ labelMediumEmphasized = typographyTokens.labelMediumEmphasized,
+ labelSmallEmphasized = typographyTokens.labelSmallEmphasized,
)
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt
index 13acfd6b85f3..280b8d95c8b7 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypefaceTokens.kt
@@ -34,6 +34,29 @@ internal class TypefaceTokens(typefaceNames: TypefaceNames) {
private val brandFont = DeviceFontFamilyName(typefaceNames.brand)
private val plainFont = DeviceFontFamilyName(typefaceNames.plain)
+ // Google Sans Flex emphasized styles
+ private val displayLargeEmphasizedFont =
+ DeviceFontFamilyName("variable-display-large-emphasized")
+ private val displayMediumEmphasizedFont =
+ DeviceFontFamilyName("variable-display-medium-emphasized")
+ private val displaySmallEmphasizedFont =
+ DeviceFontFamilyName("variable-display-small-emphasized")
+ private val headlineLargeEmphasizedFont =
+ DeviceFontFamilyName("variable-headline-large-emphasized")
+ private val headlineMediumEmphasizedFont =
+ DeviceFontFamilyName("variable-headline-medium-emphasized")
+ private val headlineSmallEmphasizedFont =
+ DeviceFontFamilyName("variable-headline-small-emphasized")
+ private val titleLargeEmphasizedFont = DeviceFontFamilyName("variable-title-large-emphasized")
+ private val titleMediumEmphasizedFont = DeviceFontFamilyName("variable-title-medium-emphasized")
+ private val titleSmallEmphasizedFont = DeviceFontFamilyName("variable-title-small-emphasized")
+ private val bodyLargeEmphasizedFont = DeviceFontFamilyName("variable-body-large-emphasized")
+ private val bodyMediumEmphasizedFont = DeviceFontFamilyName("variable-body-medium-emphasized")
+ private val bodySmallEmphasizedFont = DeviceFontFamilyName("variable-body-small-emphasized")
+ private val labelLargeEmphasizedFont = DeviceFontFamilyName("variable-label-large-emphasized")
+ private val labelMediumEmphasizedFont = DeviceFontFamilyName("variable-label-medium-emphasized")
+ private val labelSmallEmphasizedFont = DeviceFontFamilyName("variable-label-small-emphasized")
+
val brand =
FontFamily(
Font(brandFont, weight = WeightMedium),
@@ -44,6 +67,22 @@ internal class TypefaceTokens(typefaceNames: TypefaceNames) {
Font(plainFont, weight = WeightMedium),
Font(plainFont, weight = WeightRegular),
)
+
+ val displayLargeEmphasized = FontFamily(Font(displayLargeEmphasizedFont))
+ val displayMediumEmphasized = FontFamily(Font(displayMediumEmphasizedFont))
+ val displaySmallEmphasized = FontFamily(Font(displaySmallEmphasizedFont))
+ val headlineLargeEmphasized = FontFamily(Font(headlineLargeEmphasizedFont))
+ val headlineMediumEmphasized = FontFamily(Font(headlineMediumEmphasizedFont))
+ val headlineSmallEmphasized = FontFamily(Font(headlineSmallEmphasizedFont))
+ val titleLargeEmphasized = FontFamily(Font(titleLargeEmphasizedFont))
+ val titleMediumEmphasized = FontFamily(Font(titleMediumEmphasizedFont))
+ val titleSmallEmphasized = FontFamily(Font(titleSmallEmphasizedFont))
+ val bodyLargeEmphasized = FontFamily(Font(bodyLargeEmphasizedFont))
+ val bodyMediumEmphasized = FontFamily(Font(bodyMediumEmphasizedFont))
+ val bodySmallEmphasized = FontFamily(Font(bodySmallEmphasizedFont))
+ val labelLargeEmphasized = FontFamily(Font(labelLargeEmphasizedFont))
+ val labelMediumEmphasized = FontFamily(Font(labelMediumEmphasizedFont))
+ val labelSmallEmphasized = FontFamily(Font(labelSmallEmphasizedFont))
}
internal data class TypefaceNames
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt
index 38aadb8c4d15..41156478b1c5 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/TypographyTokens.kt
@@ -18,7 +18,10 @@ package com.android.compose.theme.typography
import androidx.compose.ui.text.TextStyle
-internal class TypographyTokens(typeScaleTokens: TypeScaleTokens) {
+internal class TypographyTokens(
+ typeScaleTokens: TypeScaleTokens,
+ variableTypeScaleTokens: VariableFontTypeScaleEmphasizedTokens,
+) {
val bodyLarge =
TextStyle(
fontFamily = typeScaleTokens.bodyLargeFont,
@@ -139,4 +142,112 @@ internal class TypographyTokens(typeScaleTokens: TypeScaleTokens) {
lineHeight = typeScaleTokens.titleSmallLineHeight,
letterSpacing = typeScaleTokens.titleSmallTracking,
)
+ // GSF emphasized styles
+ // note: we don't need to define fontWeight or axes values because they are pre-defined
+ // as part of the font family in fonts_customization.xml (for performance optimization)
+ val displayLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.displayLargeFont,
+ fontSize = variableTypeScaleTokens.displayLargeSize,
+ lineHeight = variableTypeScaleTokens.displayLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.displayLargeTracking,
+ )
+ val displayMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.displayMediumFont,
+ fontSize = variableTypeScaleTokens.displayMediumSize,
+ lineHeight = variableTypeScaleTokens.displayMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.displayMediumTracking,
+ )
+ val displaySmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.displaySmallFont,
+ fontSize = variableTypeScaleTokens.displaySmallSize,
+ lineHeight = variableTypeScaleTokens.displaySmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.displaySmallTracking,
+ )
+ val headlineLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.headlineLargeFont,
+ fontSize = variableTypeScaleTokens.headlineLargeSize,
+ lineHeight = variableTypeScaleTokens.headlineLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.headlineLargeTracking,
+ )
+ val headlineMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.headlineMediumFont,
+ fontSize = variableTypeScaleTokens.headlineMediumSize,
+ lineHeight = variableTypeScaleTokens.headlineMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.headlineMediumTracking,
+ )
+ val headlineSmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.headlineSmallFont,
+ fontSize = variableTypeScaleTokens.headlineSmallSize,
+ lineHeight = variableTypeScaleTokens.headlineSmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.headlineSmallTracking,
+ )
+ val titleLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.titleLargeFont,
+ fontSize = variableTypeScaleTokens.titleLargeSize,
+ lineHeight = variableTypeScaleTokens.titleLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.titleLargeTracking,
+ )
+ val titleMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.titleMediumFont,
+ fontSize = variableTypeScaleTokens.titleMediumSize,
+ lineHeight = variableTypeScaleTokens.titleMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.titleMediumTracking,
+ )
+ val titleSmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.titleSmallFont,
+ fontSize = variableTypeScaleTokens.titleSmallSize,
+ lineHeight = variableTypeScaleTokens.titleSmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.titleSmallTracking,
+ )
+ val bodyLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.bodyLargeFont,
+ fontSize = variableTypeScaleTokens.bodyLargeSize,
+ lineHeight = variableTypeScaleTokens.bodyLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.bodyLargeTracking,
+ )
+ val bodyMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.bodyMediumFont,
+ fontSize = variableTypeScaleTokens.bodyMediumSize,
+ lineHeight = variableTypeScaleTokens.bodyMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.bodyMediumTracking,
+ )
+ val bodySmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.bodySmallFont,
+ fontSize = variableTypeScaleTokens.bodySmallSize,
+ lineHeight = variableTypeScaleTokens.bodySmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.bodySmallTracking,
+ )
+ val labelLargeEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.labelLargeFont,
+ fontSize = variableTypeScaleTokens.labelLargeSize,
+ lineHeight = variableTypeScaleTokens.labelLargeLineHeight,
+ letterSpacing = variableTypeScaleTokens.labelLargeTracking,
+ )
+ val labelMediumEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.labelMediumFont,
+ fontSize = variableTypeScaleTokens.labelMediumSize,
+ lineHeight = variableTypeScaleTokens.labelMediumLineHeight,
+ letterSpacing = variableTypeScaleTokens.labelMediumTracking,
+ )
+ val labelSmallEmphasized =
+ TextStyle(
+ fontFamily = variableTypeScaleTokens.labelSmallFont,
+ fontSize = variableTypeScaleTokens.labelSmallSize,
+ lineHeight = variableTypeScaleTokens.labelSmallLineHeight,
+ letterSpacing = variableTypeScaleTokens.labelSmallTracking,
+ )
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt
new file mode 100644
index 000000000000..52b93904a4a2
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/typography/VariableFontTypeScaleEmphasizedTokens.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.compose.theme.typography
+
+import androidx.compose.ui.unit.sp
+
+internal class VariableFontTypeScaleEmphasizedTokens(typefaceTokens: TypefaceTokens) {
+ val bodyLargeFont = typefaceTokens.bodyLargeEmphasized
+ val bodyLargeLineHeight = 24.0.sp
+ val bodyLargeSize = 16.sp
+ val bodyLargeTracking = 0.0.sp
+ val bodyMediumFont = typefaceTokens.bodyMediumEmphasized
+ val bodyMediumLineHeight = 20.0.sp
+ val bodyMediumSize = 14.sp
+ val bodyMediumTracking = 0.0.sp
+ val bodySmallFont = typefaceTokens.bodySmallEmphasized
+ val bodySmallLineHeight = 16.0.sp
+ val bodySmallSize = 12.sp
+ val bodySmallTracking = 0.0.sp
+ val displayLargeFont = typefaceTokens.displayLargeEmphasized
+ val displayLargeLineHeight = 64.0.sp
+ val displayLargeSize = 57.sp
+ val displayLargeTracking = 0.0.sp
+ val displayMediumFont = typefaceTokens.displayMediumEmphasized
+ val displayMediumLineHeight = 52.0.sp
+ val displayMediumSize = 45.sp
+ val displayMediumTracking = 0.0.sp
+ val displaySmallFont = typefaceTokens.displaySmallEmphasized
+ val displaySmallLineHeight = 44.0.sp
+ val displaySmallSize = 36.sp
+ val displaySmallTracking = 0.0.sp
+ val headlineLargeFont = typefaceTokens.headlineLargeEmphasized
+ val headlineLargeLineHeight = 40.0.sp
+ val headlineLargeSize = 32.sp
+ val headlineLargeTracking = 0.0.sp
+ val headlineMediumFont = typefaceTokens.headlineMediumEmphasized
+ val headlineMediumLineHeight = 36.0.sp
+ val headlineMediumSize = 28.sp
+ val headlineMediumTracking = 0.0.sp
+ val headlineSmallFont = typefaceTokens.headlineSmallEmphasized
+ val headlineSmallLineHeight = 32.0.sp
+ val headlineSmallSize = 24.sp
+ val headlineSmallTracking = 0.0.sp
+ val labelLargeFont = typefaceTokens.labelLargeEmphasized
+ val labelLargeLineHeight = 20.0.sp
+ val labelLargeSize = 14.sp
+ val labelLargeTracking = 0.0.sp
+ val labelMediumFont = typefaceTokens.labelMediumEmphasized
+ val labelMediumLineHeight = 16.0.sp
+ val labelMediumSize = 12.sp
+ val labelMediumTracking = 0.0.sp
+ val labelSmallFont = typefaceTokens.labelSmallEmphasized
+ val labelSmallLineHeight = 16.0.sp
+ val labelSmallSize = 11.sp
+ val labelSmallTracking = 0.0.sp
+ val titleLargeFont = typefaceTokens.titleLargeEmphasized
+ val titleLargeLineHeight = 28.0.sp
+ val titleLargeSize = 22.sp
+ val titleLargeTracking = 0.0.sp
+ val titleMediumFont = typefaceTokens.titleMediumEmphasized
+ val titleMediumLineHeight = 24.0.sp
+ val titleMediumSize = 16.sp
+ val titleMediumTracking = 0.0.sp
+ val titleSmallFont = typefaceTokens.titleSmallEmphasized
+ val titleSmallLineHeight = 20.0.sp
+ val titleSmallSize = 14.sp
+ val titleSmallTracking = 0.0.sp
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt
index 48dee240a1df..f1b273ae5741 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerOverlay.kt
@@ -24,6 +24,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.UserAction
@@ -102,6 +103,8 @@ private fun ContentScope.BouncerOverlay(
viewModel,
dialogFactory,
Modifier.element(Bouncer.Elements.Content)
+ // TODO(b/393516240): Use the same sysuiResTag() as views instead.
+ .testTag(Bouncer.Elements.Content.testTag)
.overscroll(verticalOverscrollEffect)
.sysuiResTag(Bouncer.TestTags.Root)
.fillMaxSize(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index 5e61af634bbc..aa07370aa9cf 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.composable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
@@ -55,7 +56,11 @@ constructor(
@Composable
override fun ContentScope.Content(modifier: Modifier) {
- LockscreenScene(lockscreenContent = lockscreenContent, modifier = modifier)
+ LockscreenScene(
+ lockscreenContent = lockscreenContent,
+ // TODO(b/393516240): Use the same sysuiResTag() as views instead.
+ modifier = modifier.testTag(key.rootElementKey.testTag),
+ )
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
index b11c83c778f4..4b3ebc2bd53d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt
@@ -35,9 +35,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon as MaterialIcon
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -73,11 +73,15 @@ import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState
+import com.android.systemui.volume.ui.slider.AccessibilityParams
+import com.android.systemui.volume.ui.slider.Haptics
+import com.android.systemui.volume.ui.slider.Slider
import kotlin.math.round
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VolumeSlider(
state: SliderState,
@@ -102,17 +106,6 @@ fun VolumeSlider(
return
}
- val value by valueState(state)
- val interactionSource = remember { MutableInteractionSource() }
- val hapticsViewModel: SliderHapticsViewModel? =
- setUpHapticsViewModel(
- value,
- state.valueRange,
- state.hapticFilter,
- interactionSource,
- hapticsViewModelFactory,
- )
-
Column(modifier = modifier.animateContentSize(), verticalArrangement = Arrangement.Top) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
@@ -134,60 +127,30 @@ fun VolumeSlider(
)
button?.invoke()
}
+
Slider(
- value = value,
+ value = state.value,
valueRange = state.valueRange,
- onValueChange = { newValue ->
- hapticsViewModel?.addVelocityDataPoint(newValue)
- onValueChange(newValue)
- },
- onValueChangeFinished = {
- hapticsViewModel?.onValueChangeEnded()
- onValueChangeFinished?.invoke()
- },
- enabled = state.isEnabled,
+ onValueChanged = onValueChange,
+ onValueChangeFinished = { onValueChangeFinished?.invoke() },
+ isEnabled = state.isEnabled,
+ stepDistance = state.a11yStep,
+ accessibilityParams =
+ AccessibilityParams(
+ label = state.label,
+ disabledMessage = state.disabledMessage,
+ currentStateDescription = state.a11yStateDescription,
+ ),
+ haptics =
+ hapticsViewModelFactory?.let {
+ Haptics.Enabled(
+ hapticsViewModelFactory = it,
+ hapticFilter = state.hapticFilter,
+ orientation = Orientation.Horizontal,
+ )
+ } ?: Haptics.Disabled,
modifier =
- Modifier.height(40.dp)
- .padding(top = 4.dp, bottom = 12.dp)
- .sysuiResTag(state.label)
- .clearAndSetSemantics {
- if (state.isEnabled) {
- contentDescription = state.label
- state.a11yClickDescription?.let {
- customActions =
- listOf(
- CustomAccessibilityAction(it) {
- onIconTapped()
- true
- }
- )
- }
-
- state.a11yStateDescription?.let { stateDescription = it }
- progressBarRangeInfo =
- ProgressBarRangeInfo(state.value, state.valueRange)
- } else {
- disabled()
- contentDescription =
- state.disabledMessage?.let { "${state.label}, $it" } ?: state.label
- }
- setProgress { targetValue ->
- val targetDirection =
- when {
- targetValue > value -> 1
- targetValue < value -> -1
- else -> 0
- }
-
- val newValue =
- (value + targetDirection * state.a11yStep).coerceIn(
- state.valueRange.start,
- state.valueRange.endInclusive,
- )
- onValueChange(newValue)
- true
- }
- },
+ Modifier.height(40.dp).padding(top = 4.dp, bottom = 12.dp).sysuiResTag(state.label),
)
state.disabledMessage?.let { disabledMessage ->
AnimatedVisibility(visible = !state.isEnabled) {
@@ -348,7 +311,7 @@ private fun SliderIcon(
}
@Composable
-fun setUpHapticsViewModel(
+private fun setUpHapticsViewModel(
value: Float,
valueRange: ClosedFloatingPointRange<Float>,
hapticFilter: SliderHapticFeedbackFilter,
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 907b5bc2143a..05958a212f47 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
@@ -169,7 +169,7 @@ internal fun Modifier.element(
Modifier.maybeElevateInContent(layoutImpl, content, key, currentTransitionStates)
}
.then(ElementModifier(layoutImpl, currentTransitionStates, content, key))
- .testTag(key.testTag)
+ .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
}
/**
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 53d0ee1d2045..404f1b217026 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
@@ -66,6 +66,8 @@ fun SceneTransitionLayout(
swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
swipeDetector: SwipeDetector = DefaultSwipeDetector,
@FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0.05f,
+ // TODO(b/240432457) Remove this once test utils can access the internal STLForTesting().
+ implicitTestTags: Boolean = false,
builder: SceneTransitionLayoutScope<ContentScope>.() -> Unit,
) {
SceneTransitionLayoutForTesting(
@@ -74,6 +76,7 @@ fun SceneTransitionLayout(
swipeSourceDetector,
swipeDetector,
transitionInterceptionThreshold,
+ implicitTestTags = implicitTestTags,
onLayoutImpl = null,
builder = builder,
)
@@ -727,10 +730,8 @@ class FixedDistance(private val distance: Dp) : UserActionDistance {
}
/**
- * An internal version of [SceneTransitionLayout] to be used for tests.
- *
- * Important: You should use this only in tests and if you need to access the underlying
- * [SceneTransitionLayoutImpl]. In other cases, you should use [SceneTransitionLayout].
+ * An internal version of [SceneTransitionLayout] to be used for tests, that provides access to the
+ * internal [SceneTransitionLayoutImpl] and implicitly tags all scenes and elements.
*/
@Composable
internal fun SceneTransitionLayoutForTesting(
@@ -743,6 +744,7 @@ internal fun SceneTransitionLayoutForTesting(
sharedElementMap: MutableMap<ElementKey, Element> = remember { mutableMapOf() },
ancestors: List<Ancestor> = remember { emptyList() },
lookaheadScope: LookaheadScope? = null,
+ implicitTestTags: Boolean = true,
builder: SceneTransitionLayoutScope<InternalContentScope>.() -> Unit,
) {
val density = LocalDensity.current
@@ -767,6 +769,7 @@ internal fun SceneTransitionLayoutForTesting(
directionChangeSlop = directionChangeSlop,
defaultEffectFactory = defaultEffectFactory,
decayAnimationSpec = decayAnimationSpec,
+ implicitTestTags = implicitTestTags,
)
.also { onLayoutImpl?.invoke(it) }
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 53996d25afdb..e3c4eb0f8bea 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -122,6 +122,9 @@ internal class SceneTransitionLayoutImpl(
* This is used to enable transformations and shared elements across NestedSTLs.
*/
internal val ancestors: List<Ancestor> = emptyList(),
+
+ /** Whether elements and scene should be tagged using `Modifier.testTag`. */
+ internal val implicitTestTags: Boolean = false,
lookaheadScope: LookaheadScope? = null,
defaultEffectFactory: OverscrollFactory,
) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index 9ca45fe92ad5..149a9e7c4705 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
@@ -173,7 +173,7 @@ internal sealed class Content(
.thenIf(layoutImpl.state.isElevationPossible(content = key, element = null)) {
Modifier.container(containerState)
}
- .testTag(key.testTag)
+ .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
) {
CompositionLocalProvider(LocalOverscrollFactory provides lastFactory) {
scope.content()
@@ -301,6 +301,7 @@ internal class ContentScopeImpl(
sharedElementMap = layoutImpl.elements,
ancestors = ancestors,
lookaheadScope = layoutImpl.lookaheadScope,
+ implicitTestTags = layoutImpl.implicitTestTags,
)
}
}
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 338fb9b674a1..86cbfe4f1a8b 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
@@ -227,7 +227,7 @@ class ElementTest {
to = SceneB,
transitionLayout = { state ->
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) {
Box(Modifier.size(layoutSize)) {
// Transformed element
@@ -633,7 +633,7 @@ class ElementTest {
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) { Box(Modifier.element(TestElements.Foo).size(20.dp)) }
scene(SceneB) {}
}
@@ -674,7 +674,7 @@ class ElementTest {
CompositionLocalProvider(
LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
) {
- SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
Spacer(Modifier.fillMaxSize())
}
@@ -734,7 +734,7 @@ class ElementTest {
CompositionLocalProvider(
LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
) {
- SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
Spacer(
Modifier.overscroll(verticalOverscrollEffect)
@@ -834,7 +834,7 @@ class ElementTest {
CompositionLocalProvider(
LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
) {
- SceneTransitionLayout(state, Modifier.size(layoutWidth, layoutHeight)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutWidth, layoutHeight)) {
scene(key = SceneA, userActions = mapOf(Swipe.Down to SceneB)) {
Spacer(Modifier.fillMaxSize())
}
@@ -893,7 +893,7 @@ class ElementTest {
CompositionLocalProvider(
LocalOverscrollFactory provides rememberOffsetOverscrollEffectFactory()
) {
- SceneTransitionLayout(
+ SceneTransitionLayoutForTesting(
state = state,
modifier = Modifier.size(layoutWidth, layoutHeight),
) {
@@ -970,7 +970,7 @@ class ElementTest {
rule.setContent {
touchSlop = LocalViewConfiguration.current.touchSlop
- SceneTransitionLayout(
+ SceneTransitionLayoutForTesting(
state = state,
modifier = Modifier.size(layoutWidth, layoutHeight),
) {
@@ -1057,7 +1057,7 @@ class ElementTest {
rule.setContent {
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) {
Box(Modifier.size(layoutSize)) {
Box(
@@ -1374,7 +1374,7 @@ class ElementTest {
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
scene(SceneA) {
Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopStart)) }
}
@@ -1742,7 +1742,7 @@ class ElementTest {
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Foo(offset = 0.dp) }
scene(SceneB) { Foo(offset = 20.dp) }
scene(SceneC) { Foo(offset = 40.dp) }
@@ -1828,7 +1828,7 @@ class ElementTest {
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) }
// Define A after B so that Foo is placed in A during A <=> B.
@@ -1887,7 +1887,7 @@ class ElementTest {
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) { Foo() }
scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
index 04c762f43907..98ecb644878b 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
@@ -90,7 +90,7 @@ class OverlayTest {
lateinit var coroutineScope: CoroutineScope
rule.setContent {
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
overlay(OverlayA) { Foo() }
}
@@ -132,7 +132,7 @@ class OverlayTest {
lateinit var coroutineScope: CoroutineScope
rule.setContent {
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
overlay(OverlayA) { Foo() }
overlay(OverlayB) { Foo() }
@@ -230,7 +230,7 @@ class OverlayTest {
lateinit var coroutineScope: CoroutineScope
rule.setContent {
coroutineScope = rememberCoroutineScope()
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) { MovableBar() } }
overlay(OverlayA) { MovableBar() }
overlay(OverlayB) { MovableBar() }
@@ -302,7 +302,7 @@ class OverlayTest {
}
var alignment by mutableStateOf(Alignment.Center)
rule.setContent {
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } }
overlay(OverlayA, alignment = alignment) { Foo() }
}
@@ -761,7 +761,7 @@ class OverlayTest {
val movableElementChildTag = "movableElementChildTag"
val scope =
rule.setContentAndCreateMainScope {
- SceneTransitionLayout(state) {
+ SceneTransitionLayoutForTesting(state) {
scene(SceneA) {
MovableElement(key, Modifier) {
content { Box(Modifier.testTag(movableElementChildTag).size(100.dp)) }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
index 2bf235846b32..366b11d9fabd 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
@@ -250,7 +250,7 @@ class PredictiveBackHandlerTest {
}
rule.setContent {
- SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(200.dp)) {
scene(SceneA) { Box(Modifier.fillMaxSize()) }
overlay(OverlayA) { Box(Modifier.fillMaxSize()) }
overlay(OverlayB) { Box(Modifier.fillMaxSize()) }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index d7f7a514682c..fa7661b6d102 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -97,7 +97,7 @@ class SceneTransitionLayoutTest {
MutableSceneTransitionLayoutStateForTests(SceneA, EmptyTestTransitions)
}
- SceneTransitionLayout(state = layoutState, modifier = Modifier.size(LayoutSize)) {
+ SceneTransitionLayoutForTesting(state = layoutState, modifier = Modifier.size(LayoutSize)) {
scene(SceneA, userActions = mapOf(Back to SceneB)) {
Box(Modifier.fillMaxSize()) {
SharedFoo(size = 50.dp, childOffset = 0.dp, Modifier.align(Alignment.TopEnd))
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
index 751b31481e3a..11abbbec79bf 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -763,7 +763,7 @@ class SwipeToSceneTest {
var touchSlop = 0f
rule.setContent {
touchSlop = LocalViewConfiguration.current.touchSlop
- SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) {
Box(Modifier.fillMaxSize())
}
@@ -837,7 +837,7 @@ class SwipeToSceneTest {
rule.setContent {
touchSlop = LocalViewConfiguration.current.touchSlop
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
- SceneTransitionLayout(state, Modifier.size(layoutSize)) {
+ SceneTransitionLayoutForTesting(state, Modifier.size(layoutSize)) {
scene(SceneA, userActions = mapOf(Swipe.Start to SceneB, Swipe.End to SceneC)) {
Box(Modifier.fillMaxSize())
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
index bb511bc27317..8b568928bde0 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
@@ -40,7 +40,7 @@ import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
import com.android.compose.animation.scene.MutableSceneTransitionLayoutStateForTests
import com.android.compose.animation.scene.Scale
import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.animation.scene.SceneTransitionLayoutForTesting
import com.android.compose.animation.scene.SceneTransitions
import com.android.compose.animation.scene.TestScenes
import com.android.compose.animation.scene.testNestedTransition
@@ -114,7 +114,7 @@ class NestedElementTransformationTest {
@Composable
(states: List<MutableSceneTransitionLayoutState>) -> Unit =
{ states ->
- SceneTransitionLayout(states[0]) {
+ SceneTransitionLayoutForTesting(states[0]) {
scene(TestScenes.SceneA, content = { TestElement(elementVariant0A) })
scene(
TestScenes.SceneB,
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
index 6d47babd716a..e56d1bed4c25 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestContentScope.kt
@@ -30,5 +30,7 @@ fun TestContentScope(
content: @Composable ContentScope.() -> Unit,
) {
val state = rememberMutableSceneTransitionLayoutState(currentScene)
- SceneTransitionLayout(state, modifier) { scene(currentScene, content = content) }
+ SceneTransitionLayout(state, modifier, implicitTestTags = true) {
+ scene(currentScene, content = content)
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index f94a7ed77341..a362a370328a 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -137,7 +137,7 @@ fun ComposeContentTestRule.testTransition(
},
changeState = changeState,
transitionLayout = { state ->
- SceneTransitionLayout(state, layoutModifier) {
+ SceneTransitionLayout(state, layoutModifier, implicitTestTags = true) {
scene(fromScene, content = fromSceneContent)
scene(toScene, content = toSceneContent)
}
@@ -163,7 +163,7 @@ fun ComposeContentTestRule.testShowOverlayTransition(
)
},
transitionLayout = { state ->
- SceneTransitionLayout(state) {
+ SceneTransitionLayout(state, implicitTestTags = true) {
scene(fromScene) { fromSceneContent() }
overlay(overlay) { overlayContent() }
}
@@ -191,7 +191,7 @@ fun ComposeContentTestRule.testHideOverlayTransition(
)
},
transitionLayout = { state ->
- SceneTransitionLayout(state) {
+ SceneTransitionLayout(state, implicitTestTags = true) {
scene(toScene) { toSceneContent() }
overlay(overlay) { overlayContent() }
}
@@ -223,7 +223,7 @@ fun ComposeContentTestRule.testReplaceOverlayTransition(
)
},
transitionLayout = { state ->
- SceneTransitionLayout(state) {
+ SceneTransitionLayout(state, implicitTestTags = true) {
scene(currentScene) { currentSceneContent() }
overlay(from, alignment = fromAlignment) { fromContent() }
overlay(to, alignment = toAlignment) { toContent() }
@@ -273,7 +273,7 @@ fun MotionTestRule<ComposeToolkit>.recordTransition(
}
}
- SceneTransitionLayout(state, layoutModifier) {
+ SceneTransitionLayout(state, layoutModifier, implicitTestTags = true) {
scene(fromScene, content = fromSceneContent)
scene(toScene, content = toSceneContent)
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index aad1276d76e5..654478af3fb0 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -28,6 +28,7 @@ import com.android.systemui.plugins.clocks.ClockMetadata
import com.android.systemui.plugins.clocks.ClockPickerConfig
import com.android.systemui.plugins.clocks.ClockProvider
import com.android.systemui.plugins.clocks.ClockSettings
+import com.android.systemui.shared.clocks.FlexClockController.Companion.AXIS_PRESETS
import com.android.systemui.shared.clocks.FlexClockController.Companion.getDefaultAxes
private val TAG = DefaultClockProvider::class.simpleName
@@ -98,16 +99,16 @@ class DefaultClockProvider(
throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG")
}
- val fontAxes =
- if (!isClockReactiveVariantsEnabled) listOf()
- else getDefaultAxes(settings).merge(settings.axes)
return ClockPickerConfig(
settings.clockId ?: DEFAULT_CLOCK_ID,
resources.getString(R.string.clock_default_name),
resources.getString(R.string.clock_default_description),
resources.getDrawable(R.drawable.clock_default_thumbnail, null),
isReactiveToTone = true,
- axes = fontAxes,
+ axes =
+ if (!isClockReactiveVariantsEnabled) emptyList()
+ else getDefaultAxes(settings).merge(settings.axes),
+ axisPresets = if (!isClockReactiveVariantsEnabled) emptyList() else AXIS_PRESETS,
)
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
index ac1c5a8dfaf3..1a1033ba42e0 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt
@@ -132,7 +132,7 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController
listOf(
GSFAxes.WEIGHT.toClockAxis(
type = AxisType.Float,
- currentValue = 400f,
+ currentValue = 475f,
name = "Weight",
description = "Glyph Weight",
),
@@ -161,5 +161,59 @@ class FlexClockController(private val clockCtx: ClockContext) : ClockController
GSFAxes.ROUND.toClockAxisSetting(100f),
GSFAxes.SLANT.toClockAxisSetting(0f),
)
+
+ val AXIS_PRESETS =
+ listOf(
+ FONT_AXES.map { it.toSetting() },
+ LEGACY_FLEX_SETTINGS,
+ listOf( // Porcelain
+ GSFAxes.WEIGHT.toClockAxisSetting(500f),
+ GSFAxes.WIDTH.toClockAxisSetting(100f),
+ GSFAxes.ROUND.toClockAxisSetting(0f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ listOf( // Midnight
+ GSFAxes.WEIGHT.toClockAxisSetting(300f),
+ GSFAxes.WIDTH.toClockAxisSetting(100f),
+ GSFAxes.ROUND.toClockAxisSetting(100f),
+ GSFAxes.SLANT.toClockAxisSetting(-10f),
+ ),
+ listOf( // Sterling
+ GSFAxes.WEIGHT.toClockAxisSetting(1000f),
+ GSFAxes.WIDTH.toClockAxisSetting(100f),
+ GSFAxes.ROUND.toClockAxisSetting(0f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ listOf( // Smoky Green
+ GSFAxes.WEIGHT.toClockAxisSetting(150f),
+ GSFAxes.WIDTH.toClockAxisSetting(50f),
+ GSFAxes.ROUND.toClockAxisSetting(0f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ listOf( // Iris
+ GSFAxes.WEIGHT.toClockAxisSetting(500f),
+ GSFAxes.WIDTH.toClockAxisSetting(100f),
+ GSFAxes.ROUND.toClockAxisSetting(100f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ listOf( // Margarita
+ GSFAxes.WEIGHT.toClockAxisSetting(300f),
+ GSFAxes.WIDTH.toClockAxisSetting(30f),
+ GSFAxes.ROUND.toClockAxisSetting(100f),
+ GSFAxes.SLANT.toClockAxisSetting(-10f),
+ ),
+ listOf( // Raspberry
+ GSFAxes.WEIGHT.toClockAxisSetting(700f),
+ GSFAxes.WIDTH.toClockAxisSetting(140f),
+ GSFAxes.ROUND.toClockAxisSetting(100f),
+ GSFAxes.SLANT.toClockAxisSetting(-7f),
+ ),
+ listOf( // Ultra Blue
+ GSFAxes.WEIGHT.toClockAxisSetting(850f),
+ GSFAxes.WIDTH.toClockAxisSetting(130f),
+ GSFAxes.ROUND.toClockAxisSetting(0f),
+ GSFAxes.SLANT.toClockAxisSetting(0f),
+ ),
+ )
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
index 781e416e6374..ede29d8f8f75 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/data/repository/PackageInstallerMonitorTest.kt
@@ -26,6 +26,9 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.PackageInstallSession
import com.android.systemui.coroutines.collectLastValue
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.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.testKosmos
@@ -173,6 +176,58 @@ class PackageInstallerMonitorTest : SysuiTestCase() {
}
@Test
+ fun onCreateUpdatedSession_ignoreNullPackageNameSessions() =
+ kosmos.runTest {
+ val nullPackageSession =
+ SessionInfo().apply {
+ sessionId = 1
+ appPackageName = null
+ appIcon = icon1
+ }
+
+ val wellFormedSession =
+ SessionInfo().apply {
+ sessionId = 2
+ appPackageName = "pkg_name"
+ appIcon = icon2
+ }
+
+ defaultSessions = listOf(wellFormedSession)
+
+ whenever(packageInstaller.allSessions).thenReturn(defaultSessions)
+ whenever(packageInstaller.getSessionInfo(1)).thenReturn(nullPackageSession)
+ whenever(packageInstaller.getSessionInfo(2)).thenReturn(wellFormedSession)
+
+ val packageInstallerMonitor =
+ PackageInstallerMonitor(
+ handler,
+ backgroundScope,
+ logcatLogBuffer("PackageInstallerRepositoryImplTest"),
+ packageInstaller,
+ )
+
+ val sessions by collectLastValue(packageInstallerMonitor.installSessionsForPrimaryUser)
+
+ // Verify flow updated with the new session
+ assertThat(sessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions)
+
+ val callback =
+ withArgCaptor<PackageInstaller.SessionCallback> {
+ verify(packageInstaller).registerSessionCallback(capture(), eq(handler))
+ }
+
+ // New session added
+ callback.onCreated(nullPackageSession.sessionId)
+
+ // Verify flow updated with the new session
+ assertThat(sessions)
+ .comparingElementsUsing(represents)
+ .containsExactlyElementsIn(defaultSessions)
+ }
+
+ @Test
fun installSessions_newSessionsAreAdded() =
testScope.runTest {
val installSessions by collectLastValue(underTest.installSessionsForPrimaryUser)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
index e53155de653d..ed73d89db2c7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalOngoingContentStartableTest.kt
@@ -21,6 +21,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.communalMediaRepository
+import com.android.systemui.communal.data.repository.communalSmartspaceRepository
import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository
import com.android.systemui.communal.data.repository.fakeCommunalSmartspaceRepository
import com.android.systemui.communal.domain.interactor.communalInteractor
@@ -28,12 +30,12 @@ import com.android.systemui.communal.domain.interactor.communalSettingsInteracto
import com.android.systemui.communal.domain.interactor.setCommunalEnabled
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -42,46 +44,64 @@ import org.junit.runner.RunWith
@EnableFlags(FLAG_COMMUNAL_HUB)
@RunWith(AndroidJUnit4::class)
class CommunalOngoingContentStartableTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
- private val mediaRepository = kosmos.fakeCommunalMediaRepository
- private val smartspaceRepository = kosmos.fakeCommunalSmartspaceRepository
+ private var showUmoOnHub = true
- private lateinit var underTest: CommunalOngoingContentStartable
+ private val Kosmos.underTest by
+ Kosmos.Fixture {
+ CommunalOngoingContentStartable(
+ bgScope = applicationCoroutineScope,
+ communalInteractor = communalInteractor,
+ communalMediaRepository = communalMediaRepository,
+ communalSettingsInteractor = communalSettingsInteractor,
+ communalSmartspaceRepository = communalSmartspaceRepository,
+ showUmoOnHub = showUmoOnHub,
+ )
+ }
@Before
fun setUp() {
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
- underTest =
- CommunalOngoingContentStartable(
- bgScope = kosmos.applicationCoroutineScope,
- communalInteractor = kosmos.communalInteractor,
- communalMediaRepository = mediaRepository,
- communalSettingsInteractor = kosmos.communalSettingsInteractor,
- communalSmartspaceRepository = smartspaceRepository,
- )
}
@Test
- fun testListenForOngoingContentWhenCommunalIsEnabled() =
- testScope.runTest {
+ fun testListenForOngoingContent() =
+ kosmos.runTest {
+ underTest.start()
+
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
+
+ kosmos.setCommunalEnabled(true)
+
+ assertThat(fakeCommunalMediaRepository.isListening()).isTrue()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue()
+
+ kosmos.setCommunalEnabled(false)
+
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
+ }
+
+ @Test
+ fun testListenForOngoingContent_showUmoFalse() =
+ kosmos.runTest {
+ showUmoOnHub = false
underTest.start()
- runCurrent()
- assertThat(mediaRepository.isListening()).isFalse()
- assertThat(smartspaceRepository.isListening()).isFalse()
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
kosmos.setCommunalEnabled(true)
- runCurrent()
- assertThat(mediaRepository.isListening()).isTrue()
- assertThat(smartspaceRepository.isListening()).isTrue()
+ // Media listening does not start when UMO is disabled.
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isTrue()
kosmos.setCommunalEnabled(false)
- runCurrent()
- assertThat(mediaRepository.isListening()).isFalse()
- assertThat(smartspaceRepository.isListening()).isFalse()
+ assertThat(fakeCommunalMediaRepository.isListening()).isFalse()
+ assertThat(fakeCommunalSmartspaceRepository.isListening()).isFalse()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
index 943ada9346e7..4e14fec8408f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/ui/binder/SeekBarObserverTest.kt
@@ -18,9 +18,6 @@ package com.android.systemui.media.controls.ui.binder
import android.animation.Animator
import android.animation.ObjectAnimator
-import android.icu.text.MeasureFormat
-import android.icu.util.Measure
-import android.icu.util.MeasureUnit
import android.testing.TestableLooper
import android.view.View
import android.widget.SeekBar
@@ -33,7 +30,6 @@ import com.android.systemui.media.controls.ui.view.MediaViewHolder
import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
import com.android.systemui.res.R
import com.google.common.truth.Truth.assertThat
-import java.util.Locale
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -65,11 +61,11 @@ class SeekBarObserverTest : SysuiTestCase() {
fun setUp() {
context.orCreateTestableResources.addOverride(
R.dimen.qs_media_enabled_seekbar_height,
- enabledHeight,
+ enabledHeight
)
context.orCreateTestableResources.addOverride(
R.dimen.qs_media_disabled_seekbar_height,
- disabledHeight,
+ disabledHeight
)
seekBarView = SeekBar(context)
@@ -114,31 +110,14 @@ class SeekBarObserverTest : SysuiTestCase() {
@Test
fun seekBarProgress() {
- val elapsedTime = 3000
- val duration = (1.5 * 60 * 60 * 1000).toInt()
// WHEN part of the track has been played
- val data = SeekBarViewModel.Progress(true, true, true, false, elapsedTime, duration, true)
+ val data = SeekBarViewModel.Progress(true, true, true, false, 3000, 120000, true)
observer.onChanged(data)
// THEN seek bar shows the progress
- assertThat(seekBarView.progress).isEqualTo(elapsedTime)
- assertThat(seekBarView.max).isEqualTo(duration)
-
- val expectedProgress =
- MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
- .formatMeasures(Measure(3, MeasureUnit.SECOND))
- val expectedDuration =
- MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
- .formatMeasures(
- Measure(1, MeasureUnit.HOUR),
- Measure(30, MeasureUnit.MINUTE),
- Measure(0, MeasureUnit.SECOND),
- )
- val desc =
- context.getString(
- R.string.controls_media_seekbar_description,
- expectedProgress,
- expectedDuration,
- )
+ assertThat(seekBarView.progress).isEqualTo(3000)
+ assertThat(seekBarView.max).isEqualTo(120000)
+
+ val desc = context.getString(R.string.controls_media_seekbar_description, "00:03", "02:00")
assertThat(seekBarView.contentDescription).isEqualTo(desc)
}
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 917f3564a1bd..80ce43d61003 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
@@ -65,8 +65,7 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val sceneInteractor = kosmos.sceneInteractor
-
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val underTest by lazy { kosmos.notificationsShadeOverlayContentViewModel }
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
index 1899b7d1f1bb..0e5e3330ccd9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/customize/TileQueryHelperTest.java
@@ -423,5 +423,15 @@ public class TileQueryHelperTest extends SysuiTestCase {
@Override
public void destroy() {}
+
+ @Override
+ public boolean isDestroyed() {
+ return false;
+ }
+
+ @Override
+ public int getCurrentTileUser() {
+ return 0;
+ }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TilesAvailabilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TilesAvailabilityInteractorTest.kt
index 5a58597bc097..67fb1003a6ce 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TilesAvailabilityInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/domain/interactor/TilesAvailabilityInteractorTest.kt
@@ -56,166 +56,178 @@ class TilesAvailabilityInteractorTest(flags: FlagsParameterization) : SysuiTestC
private val createdTiles = mutableListOf<FakeQSTile>()
- private val kosmos = testKosmos().apply {
- tileAvailabilityInteractorsMap = buildMap {
- put(AIRPLANE_MODE_TILE_SPEC, QSTileAvailabilityInteractor.AlwaysAvailableInteractor)
- put(WORK_MODE_TILE_SPEC, FakeTileAvailabilityInteractor(
- mapOf(
- fakeUserRepository.getSelectedUserInfo().id to flowOf(true),
- ).withDefault { flowOf(false) }
- ))
- put(HOTSPOT_TILE_SPEC, FakeTileAvailabilityInteractor(
- emptyMap<Int, Flow<Boolean>>().withDefault { flowOf(false) }
- ))
- }
+ private val kosmos =
+ testKosmos().apply {
+ tileAvailabilityInteractorsMap = buildMap {
+ put(AIRPLANE_MODE_TILE_SPEC, QSTileAvailabilityInteractor.AlwaysAvailableInteractor)
+ put(
+ WORK_MODE_TILE_SPEC,
+ FakeTileAvailabilityInteractor(
+ mapOf(fakeUserRepository.getSelectedUserInfo().id to flowOf(true))
+ .withDefault { flowOf(false) }
+ ),
+ )
+ put(
+ HOTSPOT_TILE_SPEC,
+ FakeTileAvailabilityInteractor(
+ emptyMap<Int, Flow<Boolean>>().withDefault { flowOf(false) }
+ ),
+ )
+ }
- qsTileFactory = constantFactory(
- tilesForCreator(
+ qsTileFactory =
+ constantFactory(
+ tilesForCreator(
userRepository.getSelectedUserInfo().id,
mapOf(
- AIRPLANE_MODE_TILE_SPEC to false,
- WORK_MODE_TILE_SPEC to false,
- HOTSPOT_TILE_SPEC to true,
- INTERNET_TILE_SPEC to true,
- FLASHLIGHT_TILE_SPEC to false,
- )
+ AIRPLANE_MODE_TILE_SPEC to false,
+ WORK_MODE_TILE_SPEC to false,
+ HOTSPOT_TILE_SPEC to true,
+ INTERNET_TILE_SPEC to true,
+ FLASHLIGHT_TILE_SPEC to false,
+ ),
+ )
)
- )
- }
+ }
private val underTest by lazy { kosmos.tilesAvailabilityInteractor }
@Test
@DisableFlags(FLAG_QS_NEW_TILES)
- fun flagOff_usesAvailabilityFromFactoryTiles() = with(kosmos) {
- testScope.runTest {
- val unavailableTiles = underTest.getUnavailableTiles(
- setOf(
- AIRPLANE_MODE_TILE_SPEC,
- WORK_MODE_TILE_SPEC,
- HOTSPOT_TILE_SPEC,
- INTERNET_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- ).map(TileSpec::create)
- )
- assertThat(unavailableTiles).isEqualTo(setOf(
- AIRPLANE_MODE_TILE_SPEC,
- WORK_MODE_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- ).mapTo(mutableSetOf(), TileSpec::create))
+ fun flagOff_usesAvailabilityFromFactoryTiles() =
+ with(kosmos) {
+ testScope.runTest {
+ val unavailableTiles =
+ underTest.getUnavailableTiles(
+ setOf(
+ AIRPLANE_MODE_TILE_SPEC,
+ WORK_MODE_TILE_SPEC,
+ HOTSPOT_TILE_SPEC,
+ INTERNET_TILE_SPEC,
+ FLASHLIGHT_TILE_SPEC,
+ )
+ .map(TileSpec::create)
+ )
+ assertThat(unavailableTiles)
+ .isEqualTo(
+ setOf(AIRPLANE_MODE_TILE_SPEC, WORK_MODE_TILE_SPEC, FLASHLIGHT_TILE_SPEC)
+ .mapTo(mutableSetOf(), TileSpec::create)
+ )
+ }
}
- }
@Test
- fun tileCannotBeCreated_isUnavailable() = with(kosmos) {
- testScope.runTest {
- val badSpec = TileSpec.create("unknown")
- val unavailableTiles = underTest.getUnavailableTiles(
- setOf(
- badSpec
- )
- )
- assertThat(unavailableTiles).contains(badSpec)
+ fun tileCannotBeCreated_isUnavailable() =
+ with(kosmos) {
+ testScope.runTest {
+ val badSpec = TileSpec.create("unknown")
+ val unavailableTiles = underTest.getUnavailableTiles(setOf(badSpec))
+ assertThat(unavailableTiles).contains(badSpec)
+ }
}
- }
@Test
@EnableFlags(FLAG_QS_NEW_TILES)
- fun flagOn_defaultsToInteractorTiles_usesFactoryForOthers() = with(kosmos) {
- testScope.runTest {
- val unavailableTiles = underTest.getUnavailableTiles(
- setOf(
- AIRPLANE_MODE_TILE_SPEC,
- WORK_MODE_TILE_SPEC,
- HOTSPOT_TILE_SPEC,
- INTERNET_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- ).map(TileSpec::create)
- )
- assertThat(unavailableTiles).isEqualTo(setOf(
- HOTSPOT_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- ).mapTo(mutableSetOf(), TileSpec::create))
+ fun flagOn_defaultsToInteractorTiles_usesFactoryForOthers() =
+ with(kosmos) {
+ testScope.runTest {
+ val unavailableTiles =
+ underTest.getUnavailableTiles(
+ setOf(
+ AIRPLANE_MODE_TILE_SPEC,
+ WORK_MODE_TILE_SPEC,
+ HOTSPOT_TILE_SPEC,
+ INTERNET_TILE_SPEC,
+ FLASHLIGHT_TILE_SPEC,
+ )
+ .map(TileSpec::create)
+ )
+ assertThat(unavailableTiles)
+ .isEqualTo(
+ setOf(HOTSPOT_TILE_SPEC, FLASHLIGHT_TILE_SPEC)
+ .mapTo(mutableSetOf(), TileSpec::create)
+ )
+ }
}
- }
@Test
@EnableFlags(FLAG_QS_NEW_TILES)
- fun flagOn_defaultsToInteractorTiles_usesFactoryForOthers_userChange() = with(kosmos) {
- testScope.runTest {
- fakeUserRepository.asMainUser()
- val unavailableTiles = underTest.getUnavailableTiles(
- setOf(
- AIRPLANE_MODE_TILE_SPEC,
- WORK_MODE_TILE_SPEC,
- HOTSPOT_TILE_SPEC,
- INTERNET_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- ).map(TileSpec::create)
- )
- assertThat(unavailableTiles).isEqualTo(setOf(
- WORK_MODE_TILE_SPEC,
- HOTSPOT_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- ).mapTo(mutableSetOf(), TileSpec::create))
+ fun flagOn_defaultsToInteractorTiles_usesFactoryForOthers_userChange() =
+ with(kosmos) {
+ testScope.runTest {
+ fakeUserRepository.asMainUser()
+ val unavailableTiles =
+ underTest.getUnavailableTiles(
+ setOf(
+ AIRPLANE_MODE_TILE_SPEC,
+ WORK_MODE_TILE_SPEC,
+ HOTSPOT_TILE_SPEC,
+ INTERNET_TILE_SPEC,
+ FLASHLIGHT_TILE_SPEC,
+ )
+ .map(TileSpec::create)
+ )
+ assertThat(unavailableTiles)
+ .isEqualTo(
+ setOf(WORK_MODE_TILE_SPEC, HOTSPOT_TILE_SPEC, FLASHLIGHT_TILE_SPEC)
+ .mapTo(mutableSetOf(), TileSpec::create)
+ )
+ }
}
- }
@Test
@EnableFlags(FLAG_QS_NEW_TILES)
- fun flagOn_onlyNeededTilesAreCreated_andThenDestroyed() = with(kosmos) {
- testScope.runTest {
- underTest.getUnavailableTiles(
+ fun flagOn_onlyNeededTilesAreCreated_andThenDestroyed() =
+ with(kosmos) {
+ testScope.runTest {
+ underTest.getUnavailableTiles(
setOf(
AIRPLANE_MODE_TILE_SPEC,
WORK_MODE_TILE_SPEC,
HOTSPOT_TILE_SPEC,
INTERNET_TILE_SPEC,
FLASHLIGHT_TILE_SPEC,
- ).map(TileSpec::create)
- )
- assertThat(createdTiles.map { it.tileSpec })
+ )
+ .map(TileSpec::create)
+ )
+ assertThat(createdTiles.map { it.tileSpec })
.containsExactly(INTERNET_TILE_SPEC, FLASHLIGHT_TILE_SPEC)
- assertThat(createdTiles.all { it.destroyed }).isTrue()
+ assertThat(createdTiles.all { it.isDestroyed }).isTrue()
+ }
}
- }
@Test
@DisableFlags(FLAG_QS_NEW_TILES)
- fun flagOn_TilesAreCreatedAndThenDestroyed() = with(kosmos) {
- testScope.runTest {
- val allTiles = setOf(
- AIRPLANE_MODE_TILE_SPEC,
- WORK_MODE_TILE_SPEC,
- HOTSPOT_TILE_SPEC,
- INTERNET_TILE_SPEC,
- FLASHLIGHT_TILE_SPEC,
- )
- underTest.getUnavailableTiles(allTiles.map(TileSpec::create))
- assertThat(createdTiles.map { it.tileSpec })
- .containsExactlyElementsIn(allTiles)
- assertThat(createdTiles.all { it.destroyed }).isTrue()
+ fun flagOn_TilesAreCreatedAndThenDestroyed() =
+ with(kosmos) {
+ testScope.runTest {
+ val allTiles =
+ setOf(
+ AIRPLANE_MODE_TILE_SPEC,
+ WORK_MODE_TILE_SPEC,
+ HOTSPOT_TILE_SPEC,
+ INTERNET_TILE_SPEC,
+ FLASHLIGHT_TILE_SPEC,
+ )
+ underTest.getUnavailableTiles(allTiles.map(TileSpec::create))
+ assertThat(createdTiles.map { it.tileSpec }).containsExactlyElementsIn(allTiles)
+ assertThat(createdTiles.all { it.isDestroyed }).isTrue()
+ }
}
- }
-
private fun constantFactory(creatorTiles: Set<FakeQSTile>): QSFactory {
return FakeQSFactory { spec ->
- creatorTiles.firstOrNull { it.tileSpec == spec }?.also {
- createdTiles.add(it)
- }
+ creatorTiles.firstOrNull { it.tileSpec == spec }?.also { createdTiles.add(it) }
}
}
companion object {
private fun tilesForCreator(
- user: Int,
- specAvailabilities: Map<String, Boolean>
+ user: Int,
+ specAvailabilities: Map<String, Boolean>,
): Set<FakeQSTile> {
return specAvailabilities.mapTo(mutableSetOf()) {
- FakeQSTile(user, it.value).apply {
- tileSpec = it.key
- }
+ FakeQSTile(user, it.value).apply { tileSpec = it.key }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index 9b50f1bd735d..c3089761effc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -17,10 +17,10 @@
package com.android.systemui.qs.pipeline.domain.interactor
import android.content.ComponentName
-import android.content.Context
import android.content.Intent
import android.content.pm.UserInfo
import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
import android.service.quicksettings.Tile
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -28,653 +28,702 @@ import com.android.systemui.Flags.FLAG_QS_NEW_TILES
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dump.nano.SystemUIProtoDump
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.qs.QSTile.BooleanState
import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.FakeQSFactory
import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.external.CustomTile
-import com.android.systemui.qs.external.CustomTileStatePersister
import com.android.systemui.qs.external.TileLifecycleManager
import com.android.systemui.qs.external.TileServiceKey
-import com.android.systemui.qs.pipeline.data.repository.CustomTileAddedRepository
-import com.android.systemui.qs.pipeline.data.repository.FakeCustomTileAddedRepository
-import com.android.systemui.qs.pipeline.data.repository.FakeInstalledTilesComponentRepository
-import com.android.systemui.qs.pipeline.data.repository.FakeTileSpecRepository
-import com.android.systemui.qs.pipeline.data.repository.MinimumTilesFixedRepository
-import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
+import com.android.systemui.qs.external.customTileStatePersister
+import com.android.systemui.qs.external.tileLifecycleManagerFactory
+import com.android.systemui.qs.pipeline.data.repository.customTileAddedRepository
+import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
+import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository
import com.android.systemui.qs.pipeline.domain.model.TileModel
-import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
-import com.android.systemui.qs.tiles.di.NewQSTileFactory
+import com.android.systemui.qs.pipeline.shared.logging.qsLogger
+import com.android.systemui.qs.qsTileFactory
+import com.android.systemui.qs.tiles.di.newQSTileFactory
import com.android.systemui.qs.toProto
-import com.android.systemui.retail.data.repository.FakeRetailModeRepository
-import com.android.systemui.settings.UserTracker
-import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.settings.fakeUserTracker
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.data.repository.userRepository
import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import com.google.protobuf.nano.MessageNano
-import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyString
-import org.mockito.Mock
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(FLAG_QS_NEW_TILES)
class CurrentTilesInteractorImplTest : SysuiTestCase() {
- private val tileSpecRepository: TileSpecRepository = FakeTileSpecRepository()
- private val userRepository = FakeUserRepository()
- private val installedTilesPackageRepository = FakeInstalledTilesComponentRepository()
- private val tileFactory = FakeQSFactory(::tileCreator)
- private val customTileAddedRepository: CustomTileAddedRepository =
- FakeCustomTileAddedRepository()
- private val pipelineFlags = QSPipelineFlagsRepository()
- private val tileLifecycleManagerFactory = TLMFactory()
- private val minimumTilesRepository = MinimumTilesFixedRepository()
- private val retailModeRepository = FakeRetailModeRepository()
-
- @Mock private lateinit var customTileStatePersister: CustomTileStatePersister
-
- @Mock private lateinit var userTracker: UserTracker
-
- @Mock private lateinit var logger: QSPipelineLogger
-
- @Mock private lateinit var newQSTileFactory: NewQSTileFactory
-
- private val testDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private val kosmos =
+ testKosmos().apply {
+ qsTileFactory = FakeQSFactory { tileCreator(it) }
+ fakeUserTracker.set(listOf(USER_INFO_0), 0)
+ fakeUserRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1))
+ tileLifecycleManagerFactory = TLMFactory()
+ newQSTileFactory = mock()
+ qsLogger = mock()
+ }
private val unavailableTiles = mutableSetOf("e")
- private lateinit var underTest: CurrentTilesInteractorImpl
-
- @Before
- fun setup() {
- MockitoAnnotations.initMocks(this)
-
- mSetFlagsRule.enableFlags(FLAG_QS_NEW_TILES)
-
- userRepository.setUserInfos(listOf(USER_INFO_0, USER_INFO_1))
-
- setUserTracker(0)
-
- underTest =
- CurrentTilesInteractorImpl(
- tileSpecRepository = tileSpecRepository,
- installedTilesComponentRepository = installedTilesPackageRepository,
- userRepository = userRepository,
- minimumTilesRepository = minimumTilesRepository,
- retailModeRepository = retailModeRepository,
- customTileStatePersister = customTileStatePersister,
- tileFactory = tileFactory,
- newQSTileFactory = { newQSTileFactory },
- customTileAddedRepository = customTileAddedRepository,
- tileLifecycleManagerFactory = tileLifecycleManagerFactory,
- userTracker = userTracker,
- mainDispatcher = testDispatcher,
- backgroundDispatcher = testDispatcher,
- scope = testScope.backgroundScope,
- logger = logger,
- featureFlags = pipelineFlags,
- )
- }
+ private val underTest = kosmos.currentTilesInteractor
@Test
fun initialState() =
- testScope.runTest(USER_INFO_0) {
- assertThat(underTest.currentTiles.value).isEmpty()
- assertThat(underTest.currentQSTiles).isEmpty()
- assertThat(underTest.currentTilesSpecs).isEmpty()
- assertThat(underTest.userId.value).isEqualTo(0)
- assertThat(underTest.userContext.value.userId).isEqualTo(0)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ assertThat(underTest.currentTiles.value).isEmpty()
+ assertThat(underTest.currentQSTiles).isEmpty()
+ assertThat(underTest.currentTilesSpecs).isEmpty()
+ assertThat(underTest.userId.value).isEqualTo(0)
+ assertThat(underTest.userContext.value.userId).isEqualTo(0)
+ }
}
@Test
fun correctTiles() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
-
- val specs =
- listOf(
- TileSpec.create("a"),
- TileSpec.create("e"),
- CUSTOM_TILE_SPEC,
- TileSpec.create("d"),
- TileSpec.create("non_existent"),
- )
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
-
- // check each tile
-
- // Tile a
- val tile0 = tiles!![0]
- assertThat(tile0.spec).isEqualTo(specs[0])
- assertThat(tile0.tile.tileSpec).isEqualTo(specs[0].spec)
- assertThat(tile0.tile).isInstanceOf(FakeQSTile::class.java)
- assertThat(tile0.tile.isAvailable).isTrue()
-
- // Tile e is not available and is not in the list
-
- // Custom Tile
- val tile1 = tiles!![1]
- assertThat(tile1.spec).isEqualTo(specs[2])
- assertThat(tile1.tile.tileSpec).isEqualTo(specs[2].spec)
- assertThat(tile1.tile).isInstanceOf(CustomTile::class.java)
- assertThat(tile1.tile.isAvailable).isTrue()
-
- // Tile d
- val tile2 = tiles!![2]
- assertThat(tile2.spec).isEqualTo(specs[3])
- assertThat(tile2.tile.tileSpec).isEqualTo(specs[3].spec)
- assertThat(tile2.tile).isInstanceOf(FakeQSTile::class.java)
- assertThat(tile2.tile.isAvailable).isTrue()
-
- // Tile non-existent shouldn't be created. Therefore, only 3 tiles total
- assertThat(tiles?.size).isEqualTo(3)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
+
+ val specs =
+ listOf(
+ TileSpec.create("a"),
+ TileSpec.create("e"),
+ CUSTOM_TILE_SPEC,
+ TileSpec.create("d"),
+ TileSpec.create("non_existent"),
+ )
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+
+ // check each tile
+
+ // Tile a
+ val tile0 = tiles!![0]
+ assertThat(tile0.spec).isEqualTo(specs[0])
+ assertThat(tile0.tile.tileSpec).isEqualTo(specs[0].spec)
+ assertThat(tile0.tile).isInstanceOf(FakeQSTile::class.java)
+ assertThat(tile0.tile.isAvailable).isTrue()
+
+ // Tile e is not available and is not in the list
+
+ // Custom Tile
+ val tile1 = tiles!![1]
+ assertThat(tile1.spec).isEqualTo(specs[2])
+ assertThat(tile1.tile.tileSpec).isEqualTo(specs[2].spec)
+ assertThat(tile1.tile).isInstanceOf(CustomTile::class.java)
+ assertThat(tile1.tile.isAvailable).isTrue()
+
+ // Tile d
+ val tile2 = tiles!![2]
+ assertThat(tile2.spec).isEqualTo(specs[3])
+ assertThat(tile2.tile.tileSpec).isEqualTo(specs[3].spec)
+ assertThat(tile2.tile).isInstanceOf(FakeQSTile::class.java)
+ assertThat(tile2.tile.isAvailable).isTrue()
+
+ // Tile non-existent shouldn't be created. Therefore, only 3 tiles total
+ assertThat(tiles?.size).isEqualTo(3)
+ }
}
@Test
fun logTileCreated() =
- testScope.runTest(USER_INFO_0) {
- val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- runCurrent()
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ runCurrent()
- specs.forEach { verify(logger).logTileCreated(it) }
+ specs.forEach { verify(qsLogger).logTileCreated(it) }
+ }
}
@Test
fun logTileNotFoundInFactory() =
- testScope.runTest(USER_INFO_0) {
- val specs = listOf(TileSpec.create("non_existing"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- runCurrent()
-
- verify(logger, never()).logTileCreated(any())
- verify(logger).logTileNotFoundInFactory(specs[0])
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val specs = listOf(TileSpec.create("non_existing"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ runCurrent()
+
+ verify(qsLogger, never()).logTileCreated(any())
+ verify(qsLogger).logTileNotFoundInFactory(specs[0])
+ }
}
@Test
fun tileNotAvailableDestroyed_logged() =
- testScope.runTest(USER_INFO_0) {
- val specs = listOf(TileSpec.create("e"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- runCurrent()
-
- verify(logger, never()).logTileCreated(any())
- verify(logger)
- .logTileDestroyed(
- specs[0],
- QSPipelineLogger.TileDestroyedReason.NEW_TILE_NOT_AVAILABLE,
- )
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val specs = listOf(TileSpec.create("e"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ runCurrent()
+
+ verify(qsLogger, never()).logTileCreated(any())
+ verify(qsLogger)
+ .logTileDestroyed(
+ specs[0],
+ QSPipelineLogger.TileDestroyedReason.NEW_TILE_NOT_AVAILABLE,
+ )
+ }
}
@Test
fun someTilesNotValid_repositorySetToDefinitiveList() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val specs = listOf(TileSpec.create("a"), TileSpec.create("e"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ val specs = listOf(TileSpec.create("a"), TileSpec.create("e"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- assertThat(tiles).isEqualTo(listOf(TileSpec.create("a")))
+ assertThat(tiles).isEqualTo(listOf(TileSpec.create("a")))
+ }
}
@Test
fun deduplicatedTiles() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"), TileSpec.create("a"))
+ val specs = listOf(TileSpec.create("a"), TileSpec.create("a"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- assertThat(tiles?.size).isEqualTo(1)
- assertThat(tiles!![0].spec).isEqualTo(specs[0])
+ assertThat(tiles?.size).isEqualTo(1)
+ assertThat(tiles!![0].spec).isEqualTo(specs[0])
+ }
}
@Test
fun tilesChange_platformTileNotRecreated() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"))
+ val specs = listOf(TileSpec.create("a"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- val originalTileA = tiles!![0].tile
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ val originalTileA = tiles!![0].tile
- tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b"))
+ tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b"))
- assertThat(tiles?.size).isEqualTo(2)
- assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA)
+ assertThat(tiles?.size).isEqualTo(2)
+ assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA)
+ }
}
@Test
fun tileRemovedIsDestroyed() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"), TileSpec.create("c"))
+ val specs = listOf(TileSpec.create("a"), TileSpec.create("c"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- val originalTileC = tiles!![1].tile
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ val originalTileC = tiles!![1].tile
- tileSpecRepository.removeTiles(USER_INFO_0.id, listOf(TileSpec.create("c")))
+ tileSpecRepository.removeTiles(USER_INFO_0.id, listOf(TileSpec.create("c")))
- assertThat(tiles?.size).isEqualTo(1)
- assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("a"))
+ assertThat(tiles?.size).isEqualTo(1)
+ assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("a"))
- assertThat((originalTileC as FakeQSTile).destroyed).isTrue()
- verify(logger)
- .logTileDestroyed(
- TileSpec.create("c"),
- QSPipelineLogger.TileDestroyedReason.TILE_REMOVED,
- )
+ assertThat(originalTileC.isDestroyed).isTrue()
+ verify(qsLogger)
+ .logTileDestroyed(
+ TileSpec.create("c"),
+ QSPipelineLogger.TileDestroyedReason.TILE_REMOVED,
+ )
+ }
}
@Test
fun tileBecomesNotAvailable_destroyed() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
- val repoTiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
-
- val specs = listOf(TileSpec.create("a"))
-
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- val originalTileA = tiles!![0].tile
-
- // Tile becomes unavailable
- (originalTileA as FakeQSTile).available = false
- unavailableTiles.add("a")
- // and there is some change in the specs
- tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b"))
- runCurrent()
-
- assertThat(originalTileA.destroyed).isTrue()
- verify(logger)
- .logTileDestroyed(
- TileSpec.create("a"),
- QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE,
- )
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
+ val repoTiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+
+ val specs = listOf(TileSpec.create("a"))
+
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ val originalTileA = tiles!![0].tile
+
+ // Tile becomes unavailable
+ (originalTileA as FakeQSTile).available = false
+ unavailableTiles.add("a")
+ // and there is some change in the specs
+ tileSpecRepository.addTile(USER_INFO_0.id, TileSpec.create("b"))
+ runCurrent()
+
+ assertThat(originalTileA.isDestroyed).isTrue()
+ verify(qsLogger)
+ .logTileDestroyed(
+ TileSpec.create("a"),
+ QSPipelineLogger.TileDestroyedReason.EXISTING_TILE_NOT_AVAILABLE,
+ )
- assertThat(tiles?.size).isEqualTo(1)
- assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("b"))
- assertThat(tiles!![0].tile).isNotSameInstanceAs(originalTileA)
+ assertThat(tiles?.size).isEqualTo(1)
+ assertThat(tiles!![0].spec).isEqualTo(TileSpec.create("b"))
+ assertThat(tiles!![0].tile).isNotSameInstanceAs(originalTileA)
- assertThat(repoTiles).isEqualTo(tiles!!.map(TileModel::spec))
+ assertThat(repoTiles).isEqualTo(tiles!!.map(TileModel::spec))
+ }
}
@Test
fun userChange_tilesChange() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs0 = listOf(TileSpec.create("a"))
- val specs1 = listOf(TileSpec.create("b"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs0)
- tileSpecRepository.setTiles(USER_INFO_1.id, specs1)
+ val specs0 = listOf(TileSpec.create("a"))
+ val specs1 = listOf(TileSpec.create("b"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs0)
+ tileSpecRepository.setTiles(USER_INFO_1.id, specs1)
- switchUser(USER_INFO_1)
+ switchUser(USER_INFO_1)
- assertThat(tiles!![0].spec).isEqualTo(specs1[0])
- assertThat(tiles!![0].tile.tileSpec).isEqualTo(specs1[0].spec)
+ assertThat(tiles!![0].spec).isEqualTo(specs1[0])
+ assertThat(tiles!![0].tile.tileSpec).isEqualTo(specs1[0].spec)
+ }
}
@Test
fun tileNotPresentInSecondaryUser_destroyedInUserChange() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs0 = listOf(TileSpec.create("a"))
- val specs1 = listOf(TileSpec.create("b"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs0)
- tileSpecRepository.setTiles(USER_INFO_1.id, specs1)
+ val specs0 = listOf(TileSpec.create("a"))
+ val specs1 = listOf(TileSpec.create("b"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs0)
+ tileSpecRepository.setTiles(USER_INFO_1.id, specs1)
- val originalTileA = tiles!![0].tile
+ val originalTileA = tiles!![0].tile
- switchUser(USER_INFO_1)
- runCurrent()
+ switchUser(USER_INFO_1)
+ runCurrent()
- assertThat((originalTileA as FakeQSTile).destroyed).isTrue()
- verify(logger)
- .logTileDestroyed(
- specs0[0],
- QSPipelineLogger.TileDestroyedReason.TILE_NOT_PRESENT_IN_NEW_USER,
- )
+ assertThat(originalTileA.isDestroyed).isTrue()
+ verify(qsLogger)
+ .logTileDestroyed(
+ specs0[0],
+ QSPipelineLogger.TileDestroyedReason.TILE_NOT_PRESENT_IN_NEW_USER,
+ )
+ }
}
@Test
- fun userChange_customTileDestroyed_lifecycleNotTerminated() {
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ fun userChange_customTileDestroyed_lifecycleNotTerminated() =
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(CUSTOM_TILE_SPEC)
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- tileSpecRepository.setTiles(USER_INFO_1.id, specs)
+ val specs = listOf(CUSTOM_TILE_SPEC)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ tileSpecRepository.setTiles(USER_INFO_1.id, specs)
- val originalCustomTile = tiles!![0].tile
+ val originalCustomTile = tiles!![0].tile
- switchUser(USER_INFO_1)
- runCurrent()
+ switchUser(USER_INFO_1)
+ runCurrent()
- verify(originalCustomTile).destroy()
- assertThat(tileLifecycleManagerFactory.created).isEmpty()
+ verify(originalCustomTile).destroy()
+ assertThat((tileLifecycleManagerFactory as TLMFactory).created).isEmpty()
+ }
}
- }
@Test
fun userChange_sameTileUserChanged() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- tileSpecRepository.setTiles(USER_INFO_1.id, specs)
+ val specs = listOf(TileSpec.create("a"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ tileSpecRepository.setTiles(USER_INFO_1.id, specs)
- val originalTileA = tiles!![0].tile as FakeQSTile
- assertThat(originalTileA.user).isEqualTo(USER_INFO_0.id)
+ val originalTileA = tiles!![0].tile as FakeQSTile
+ assertThat(originalTileA.user).isEqualTo(USER_INFO_0.id)
- switchUser(USER_INFO_1)
- runCurrent()
+ switchUser(USER_INFO_1)
+ runCurrent()
- assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA)
- assertThat(originalTileA.user).isEqualTo(USER_INFO_1.id)
- verify(logger).logTileUserChanged(specs[0], USER_INFO_1.id)
+ assertThat(tiles!![0].tile).isSameInstanceAs(originalTileA)
+ assertThat(originalTileA.user).isEqualTo(USER_INFO_1.id)
+ verify(qsLogger).logTileUserChanged(specs[0], USER_INFO_1.id)
+ }
}
@Test
fun addTile() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val spec = TileSpec.create("a")
- val currentSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"))
- tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ val spec = TileSpec.create("a")
+ val currentSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
- underTest.addTile(spec, position = 1)
+ underTest.addTile(spec, position = 1)
- val expectedSpecs = listOf(TileSpec.create("b"), spec, TileSpec.create("c"))
- assertThat(tiles).isEqualTo(expectedSpecs)
+ val expectedSpecs = listOf(TileSpec.create("b"), spec, TileSpec.create("c"))
+ assertThat(tiles).isEqualTo(expectedSpecs)
+ }
}
@Test
fun addTile_currentUser() =
- testScope.runTest(USER_INFO_1) {
- val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id))
- val spec = TileSpec.create("a")
- val currentSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"))
- tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
- tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs)
-
- switchUser(USER_INFO_1)
- underTest.addTile(spec, position = 1)
-
- assertThat(tiles0).isEqualTo(currentSpecs)
-
- val expectedSpecs = listOf(TileSpec.create("b"), spec, TileSpec.create("c"))
- assertThat(tiles1).isEqualTo(expectedSpecs)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_1) {
+ val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id))
+ val spec = TileSpec.create("a")
+ val currentSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
+ tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs)
+
+ switchUser(USER_INFO_1)
+ underTest.addTile(spec, position = 1)
+
+ assertThat(tiles0).isEqualTo(currentSpecs)
+
+ val expectedSpecs = listOf(TileSpec.create("b"), spec, TileSpec.create("c"))
+ assertThat(tiles1).isEqualTo(expectedSpecs)
+ }
}
@Test
fun removeTile_platform() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val specs = listOf(TileSpec.create("a"), TileSpec.create("b"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- runCurrent()
+ val specs = listOf(TileSpec.create("a"), TileSpec.create("b"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ runCurrent()
- underTest.removeTiles(specs.subList(0, 1))
+ underTest.removeTiles(specs.subList(0, 1))
- assertThat(tiles).isEqualTo(specs.subList(1, 2))
+ assertThat(tiles).isEqualTo(specs.subList(1, 2))
+ }
}
@Test
- fun removeTile_customTile_lifecycleEnded() {
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
-
- val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- runCurrent()
- assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
- .isTrue()
-
- underTest.removeTiles(listOf(CUSTOM_TILE_SPEC))
-
- assertThat(tiles).isEqualTo(specs.subList(0, 1))
-
- val tileLifecycleManager =
- tileLifecycleManagerFactory.created[USER_INFO_0.id to TEST_COMPONENT]
- assertThat(tileLifecycleManager).isNotNull()
-
- with(inOrder(tileLifecycleManager!!)) {
- verify(tileLifecycleManager).onStopListening()
- verify(tileLifecycleManager).onTileRemoved()
- verify(tileLifecycleManager).flushMessagesAndUnbind()
+ fun removeTile_customTile_lifecycleEnded() =
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+
+ val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ runCurrent()
+ assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
+ .isTrue()
+
+ underTest.removeTiles(listOf(CUSTOM_TILE_SPEC))
+
+ assertThat(tiles).isEqualTo(specs.subList(0, 1))
+
+ val tileLifecycleManager =
+ (tileLifecycleManagerFactory as TLMFactory)
+ .created[USER_INFO_0.id to TEST_COMPONENT]
+ assertThat(tileLifecycleManager).isNotNull()
+
+ with(inOrder(tileLifecycleManager!!)) {
+ verify(tileLifecycleManager).onStopListening()
+ verify(tileLifecycleManager).onTileRemoved()
+ verify(tileLifecycleManager).flushMessagesAndUnbind()
+ }
+ assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
+ .isFalse()
+ assertThat(
+ customTileStatePersister.readState(
+ TileServiceKey(TEST_COMPONENT, USER_INFO_0.id)
+ )
+ )
+ .isNull()
}
- assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
- .isFalse()
- verify(customTileStatePersister)
- .removeState(TileServiceKey(TEST_COMPONENT, USER_INFO_0.id))
}
- }
@Test
fun removeTiles_currentUser() =
- testScope.runTest {
- val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id))
- val currentSpecs =
- listOf(TileSpec.create("a"), TileSpec.create("b"), TileSpec.create("c"))
- tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
- tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs)
-
- switchUser(USER_INFO_1)
- runCurrent()
-
- underTest.removeTiles(currentSpecs.subList(0, 2))
-
- assertThat(tiles0).isEqualTo(currentSpecs)
- assertThat(tiles1).isEqualTo(currentSpecs.subList(2, 3))
+ with(kosmos) {
+ testScope.runTest {
+ val tiles0 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ val tiles1 by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_1.id))
+ val currentSpecs =
+ listOf(TileSpec.create("a"), TileSpec.create("b"), TileSpec.create("c"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
+ tileSpecRepository.setTiles(USER_INFO_1.id, currentSpecs)
+
+ switchUser(USER_INFO_1)
+ runCurrent()
+
+ underTest.removeTiles(currentSpecs.subList(0, 2))
+
+ assertThat(tiles0).isEqualTo(currentSpecs)
+ assertThat(tiles1).isEqualTo(currentSpecs.subList(2, 3))
+ }
}
@Test
fun setTiles() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(tileSpecRepository.tilesSpecs(USER_INFO_0.id))
- val currentSpecs = listOf(TileSpec.create("a"), TileSpec.create("b"))
- tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
- runCurrent()
+ val currentSpecs = listOf(TileSpec.create("a"), TileSpec.create("b"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
+ runCurrent()
- val newSpecs = listOf(TileSpec.create("b"), TileSpec.create("c"), TileSpec.create("a"))
- underTest.setTiles(newSpecs)
- runCurrent()
+ val newSpecs =
+ listOf(TileSpec.create("b"), TileSpec.create("c"), TileSpec.create("a"))
+ underTest.setTiles(newSpecs)
+ runCurrent()
- assertThat(tiles).isEqualTo(newSpecs)
+ assertThat(tiles).isEqualTo(newSpecs)
+ }
}
@Test
fun setTiles_customTiles_lifecycleEndedIfGone() =
- testScope.runTest(USER_INFO_0) {
- val otherCustomTileSpec = TileSpec.create("custom(b/c)")
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val otherCustomTileSpec = TileSpec.create("custom(b/c)")
- val currentSpecs = listOf(CUSTOM_TILE_SPEC, TileSpec.create("a"), otherCustomTileSpec)
- tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
- runCurrent()
+ val currentSpecs =
+ listOf(CUSTOM_TILE_SPEC, TileSpec.create("a"), otherCustomTileSpec)
+ tileSpecRepository.setTiles(USER_INFO_0.id, currentSpecs)
+ runCurrent()
- val newSpecs = listOf(otherCustomTileSpec, TileSpec.create("a"))
+ val newSpecs = listOf(otherCustomTileSpec, TileSpec.create("a"))
- underTest.setTiles(newSpecs)
- runCurrent()
+ underTest.setTiles(newSpecs)
+ runCurrent()
- val tileLifecycleManager =
- tileLifecycleManagerFactory.created[USER_INFO_0.id to TEST_COMPONENT]!!
+ val tileLifecycleManager =
+ (tileLifecycleManagerFactory as TLMFactory)
+ .created[USER_INFO_0.id to TEST_COMPONENT]!!
- with(inOrder(tileLifecycleManager)) {
- verify(tileLifecycleManager).onStopListening()
- verify(tileLifecycleManager).onTileRemoved()
- verify(tileLifecycleManager).flushMessagesAndUnbind()
+ with(inOrder(tileLifecycleManager)) {
+ verify(tileLifecycleManager).onStopListening()
+ verify(tileLifecycleManager).onTileRemoved()
+ verify(tileLifecycleManager).flushMessagesAndUnbind()
+ }
+ assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
+ .isFalse()
+ assertThat(
+ customTileStatePersister.readState(
+ TileServiceKey(TEST_COMPONENT, USER_INFO_0.id)
+ )
+ )
+ .isNull()
}
- assertThat(customTileAddedRepository.isTileAdded(TEST_COMPONENT, USER_INFO_0.id))
- .isFalse()
- verify(customTileStatePersister)
- .removeState(TileServiceKey(TEST_COMPONENT, USER_INFO_0.id))
}
@Test
fun protoDump() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
-
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
-
- val stateA = tiles!![0].tile.state
- stateA.fillIn(Tile.STATE_INACTIVE, "A", "AA")
- val stateCustom = QSTile.BooleanState()
- stateCustom.fillIn(Tile.STATE_ACTIVE, "B", "BB")
- stateCustom.spec = CUSTOM_TILE_SPEC.spec
- whenever(tiles!![1].tile.state).thenReturn(stateCustom)
-
- val proto = SystemUIProtoDump()
- underTest.dumpProto(proto, emptyArray())
-
- assertThat(MessageNano.messageNanoEquals(proto.tiles[0], stateA.toProto())).isTrue()
- assertThat(MessageNano.messageNanoEquals(proto.tiles[1], stateCustom.toProto()))
- .isTrue()
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
+ val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
+
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+
+ val stateA = tiles!![0].tile.state
+ stateA.fillIn(Tile.STATE_INACTIVE, "A", "AA")
+ val stateCustom = QSTile.BooleanState()
+ stateCustom.fillIn(Tile.STATE_ACTIVE, "B", "BB")
+ stateCustom.spec = CUSTOM_TILE_SPEC.spec
+ whenever(tiles!![1].tile.state).thenReturn(stateCustom)
+
+ val proto = SystemUIProtoDump()
+ underTest.dumpProto(proto, emptyArray())
+
+ assertThat(MessageNano.messageNanoEquals(proto.tiles[0], stateA.toProto())).isTrue()
+ assertThat(MessageNano.messageNanoEquals(proto.tiles[1], stateCustom.toProto()))
+ .isTrue()
+ }
}
@Test
fun retainedTiles_callbackNotRemoved() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
- tileSpecRepository.setTiles(USER_INFO_0.id, listOf(TileSpec.create("a")))
-
- val tileA = tiles!![0].tile
- val callback = mock<QSTile.Callback>()
- tileA.addCallback(callback)
-
- tileSpecRepository.setTiles(
- USER_INFO_0.id,
- listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC),
- )
- val newTileA = tiles!![0].tile
- assertThat(tileA).isSameInstanceAs(newTileA)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
+ tileSpecRepository.setTiles(USER_INFO_0.id, listOf(TileSpec.create("a")))
+
+ val tileA = tiles!![0].tile
+ val callback = mock<QSTile.Callback>()
+ tileA.addCallback(callback)
+
+ tileSpecRepository.setTiles(
+ USER_INFO_0.id,
+ listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC),
+ )
+ val newTileA = tiles!![0].tile
+ assertThat(tileA).isSameInstanceAs(newTileA)
- assertThat((tileA as FakeQSTile).callbacks).containsExactly(callback)
+ assertThat((tileA as FakeQSTile).callbacks).containsExactly(callback)
+ }
}
@Test
fun packageNotInstalled_customTileNotVisible() =
- testScope.runTest(USER_INFO_0) {
- installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ fakeInstalledTilesRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
- val tiles by collectLastValue(underTest.currentTiles)
+ val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- assertThat(tiles!!.size).isEqualTo(1)
- assertThat(tiles!![0].spec).isEqualTo(specs[0])
+ assertThat(tiles!!.size).isEqualTo(1)
+ assertThat(tiles!![0].spec).isEqualTo(specs[0])
+ }
}
@Test
fun packageInstalledLater_customTileAdded() =
- testScope.runTest(USER_INFO_0) {
- installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ fakeInstalledTilesRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
- val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC, TileSpec.create("b"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ val tiles by collectLastValue(underTest.currentTiles)
+ val specs = listOf(TileSpec.create("a"), CUSTOM_TILE_SPEC, TileSpec.create("b"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- assertThat(tiles!!.size).isEqualTo(2)
+ assertThat(tiles!!.size).isEqualTo(2)
- installedTilesPackageRepository.setInstalledPackagesForUser(
- USER_INFO_0.id,
- setOf(TEST_COMPONENT),
- )
+ fakeInstalledTilesRepository.setInstalledPackagesForUser(
+ USER_INFO_0.id,
+ setOf(TEST_COMPONENT),
+ )
- assertThat(tiles!!.size).isEqualTo(3)
- assertThat(tiles!![1].spec).isEqualTo(CUSTOM_TILE_SPEC)
+ assertThat(tiles!!.size).isEqualTo(3)
+ assertThat(tiles!![1].spec).isEqualTo(CUSTOM_TILE_SPEC)
+ }
}
@Test
fun tileAddedOnEmptyList_blocked() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
- val specs = listOf(TileSpec.create("a"), TileSpec.create("b"))
- val newTile = TileSpec.create("c")
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
+ val specs = listOf(TileSpec.create("a"), TileSpec.create("b"))
+ val newTile = TileSpec.create("c")
- underTest.addTile(newTile)
+ underTest.addTile(newTile)
- assertThat(tiles!!.isEmpty()).isTrue()
+ assertThat(tiles!!.isEmpty()).isTrue()
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- assertThat(tiles!!.size).isEqualTo(3)
+ assertThat(tiles!!.size).isEqualTo(3)
+ }
}
@Test
fun changeInPackagesTiles_doesntTriggerUserChange_logged() =
- testScope.runTest(USER_INFO_0) {
- val specs = listOf(TileSpec.create("a"))
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- runCurrent()
- // Settled on the same list of tiles.
- assertThat(underTest.currentTilesSpecs).isEqualTo(specs)
-
- installedTilesPackageRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
- runCurrent()
-
- verify(logger, never()).logTileUserChanged(TileSpec.create("a"), 0)
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val specs = listOf(TileSpec.create("a"))
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ runCurrent()
+ // Settled on the same list of tiles.
+ assertThat(underTest.currentTilesSpecs).isEqualTo(specs)
+
+ fakeInstalledTilesRepository.setInstalledPackagesForUser(USER_INFO_0.id, emptySet())
+ runCurrent()
+
+ verify(qsLogger, never()).logTileUserChanged(TileSpec.create("a"), 0)
+ }
}
@Test
fun getTileDetails() =
- testScope.runTest(USER_INFO_0) {
- val tiles by collectLastValue(underTest.currentTiles)
- val tileA = TileSpec.create("a")
- val tileB = TileSpec.create("b")
- val tileNoDetails = TileSpec.create("NoDetails")
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
+ val tileA = TileSpec.create("a")
+ val tileB = TileSpec.create("b")
+ val tileNoDetails = TileSpec.create("NoDetails")
+
+ val specs = listOf(tileA, tileB, tileNoDetails)
+
+ assertThat(tiles!!.isEmpty()).isTrue()
+
+ tileSpecRepository.setTiles(USER_INFO_0.id, specs)
+ assertThat(tiles!!.size).isEqualTo(3)
+
+ // The third tile doesn't have a details view.
+ assertThat(tiles!![2].spec).isEqualTo(tileNoDetails)
+ (tiles!![2].tile as FakeQSTile).hasDetailsViewModel = false
- val specs = listOf(tileA, tileB, tileNoDetails)
+ var currentModel: TileDetailsViewModel? = null
+ val setCurrentModel = { model: TileDetailsViewModel? -> currentModel = model }
+ tiles!![0].tile.getDetailsViewModel(setCurrentModel)
+ assertThat(currentModel?.getTitle()).isEqualTo("a")
- assertThat(tiles!!.isEmpty()).isTrue()
+ currentModel = null
+ tiles!![1].tile.getDetailsViewModel(setCurrentModel)
+ assertThat(currentModel?.getTitle()).isEqualTo("b")
- tileSpecRepository.setTiles(USER_INFO_0.id, specs)
- assertThat(tiles!!.size).isEqualTo(3)
+ currentModel = null
+ tiles!![2].tile.getDetailsViewModel(setCurrentModel)
+ assertThat(currentModel).isNull()
+ }
+ }
+
+ @Test
+ fun destroyedTilesNotReused() =
+ with(kosmos) {
+ testScope.runTest(USER_INFO_0) {
+ val tiles by collectLastValue(underTest.currentTiles)
+ val specs = listOf(TileSpec.create("a"), TileSpec.create("b"))
+ val newTile = TileSpec.create("c")
+
+ underTest.setTiles(specs)
- // The third tile doesn't have a details view.
- assertThat(tiles!![2].spec).isEqualTo(tileNoDetails)
- (tiles!![2].tile as FakeQSTile).hasDetailsViewModel = false
+ val tileABefore = tiles!!.first { it.spec == specs[0] }.tile
- var currentModel: TileDetailsViewModel? = null
- val setCurrentModel = { model: TileDetailsViewModel? -> currentModel = model }
- tiles!![0].tile.getDetailsViewModel(setCurrentModel)
- assertThat(currentModel?.getTitle()).isEqualTo("a")
+ // We destroy it manually, in prod, this could happen if the tile processing action
+ // is interrupted in the middle.
+ tileABefore.destroy()
- currentModel = null
- tiles!![1].tile.getDetailsViewModel(setCurrentModel)
- assertThat(currentModel?.getTitle()).isEqualTo("b")
+ underTest.addTile(newTile)
- currentModel = null
- tiles!![2].tile.getDetailsViewModel(setCurrentModel)
- assertThat(currentModel).isNull()
+ val tileAAfter = tiles!!.first { it.spec == specs[0] }.tile
+ assertThat(tileAAfter).isNotSameInstanceAs(tileABefore)
+ }
}
private fun QSTile.State.fillIn(state: Int, label: CharSequence, secondaryLabel: CharSequence) {
@@ -686,20 +735,21 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
}
}
- private fun tileCreator(spec: String): QSTile? {
- val currentUser = userTracker.userId
+ private fun Kosmos.tileCreator(spec: String): QSTile? {
+ val currentUser = userRepository.getSelectedUserInfo().id
return when (spec) {
CUSTOM_TILE_SPEC.spec ->
mock<CustomTile> {
var tileSpecReference: String? = null
- whenever(user).thenReturn(currentUser)
- whenever(component).thenReturn(CUSTOM_TILE_SPEC.componentName)
- whenever(isAvailable).thenReturn(true)
- whenever(setTileSpec(anyString())).thenAnswer {
- tileSpecReference = it.arguments[0] as? String
- Unit
- }
- whenever(tileSpec).thenAnswer { tileSpecReference }
+ on { user } doReturn currentUser
+ on { component } doReturn CUSTOM_TILE_SPEC.componentName
+ on { isAvailable } doReturn true
+ on { setTileSpec(anyString()) }
+ .thenAnswer {
+ tileSpecReference = it.arguments[0] as? String
+ Unit
+ }
+ on { tileSpec }.thenAnswer { tileSpecReference }
// Also, add it to the set of added tiles (as this happens as part of the tile
// creation).
customTileAddedRepository.setTileAdded(
@@ -714,22 +764,16 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
}
private fun TestScope.runTest(user: UserInfo, body: suspend TestScope.() -> Unit) {
- return runTest {
+ return kosmos.runTest {
switchUser(user)
body()
}
}
- private suspend fun switchUser(user: UserInfo) {
- setUserTracker(user.id)
- installedTilesPackageRepository.setInstalledPackagesForUser(user.id, setOf(TEST_COMPONENT))
- userRepository.setSelectedUserInfo(user)
- }
-
- private fun setUserTracker(user: Int) {
- val mockContext = mockUserContext(user)
- whenever(userTracker.userContext).thenReturn(mockContext)
- whenever(userTracker.userId).thenReturn(user)
+ private suspend fun Kosmos.switchUser(user: UserInfo) {
+ fakeUserTracker.set(listOf(user), 0)
+ fakeInstalledTilesRepository.setInstalledPackagesForUser(user.id, setOf(TEST_COMPONENT))
+ fakeUserRepository.setSelectedUserInfo(user)
}
private class TLMFactory : TileLifecycleManager.Factory {
@@ -745,13 +789,6 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
}
}
- private fun mockUserContext(user: Int): Context {
- return mock {
- whenever(this.userId).thenReturn(user)
- whenever(this.user).thenReturn(UserHandle.of(user))
- }
- }
-
companion object {
private val USER_INFO_0 = UserInfo().apply { id = 0 }
private val USER_INFO_1 = UserInfo().apply { id = 1 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
index 296478be77e0..1d8c6ccc75d1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tileimpl/QSTileImplTest.java
@@ -508,6 +508,12 @@ public class QSTileImplTest extends SysuiTestCase {
assertThat(mTile.mRefreshes).isEqualTo(1);
}
+ @Test
+ public void testIsDestroyedImmediately() {
+ mTile.destroy();
+ assertThat(mTile.isDestroyed()).isTrue();
+ }
+
private void assertEvent(UiEventLogger.UiEventEnum eventType,
UiEventLoggerFake.FakeUiEvent fakeEvent) {
assertEquals(eventType.getId(), fakeEvent.eventId);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
index fba615121a39..da3cebd24e6b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt
@@ -22,6 +22,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.FakeTileDetailsViewModel
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
import com.android.systemui.qs.tiles.base.interactor.FakeQSTileDataInteractor
@@ -97,6 +98,7 @@ class QSTileViewModelImplTest : SysuiTestCase() {
testCoroutineDispatcher,
testCoroutineDispatcher,
testScope.backgroundScope,
+ FakeTileDetailsViewModel("QSTileViewModelImplTest"),
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
index 3db5efcb6eb8..261e3de939f2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt
@@ -26,8 +26,6 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
-import com.android.systemui.qs.tiles.dialog.InternetDetailsContentManager
-import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
import com.android.systemui.qs.tiles.dialog.InternetDialogManager
import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
import com.android.systemui.statusbar.connectivity.AccessPointController
@@ -39,11 +37,8 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.eq
-import org.mockito.kotlin.any
import org.mockito.kotlin.mock
-import org.mockito.kotlin.times
import org.mockito.kotlin.verify
-import org.mockito.kotlin.whenever
@SmallTest
@EnabledOnRavenwood
@@ -56,31 +51,17 @@ class InternetTileUserActionInteractorTest : SysuiTestCase() {
private lateinit var internetDialogManager: InternetDialogManager
private lateinit var controller: AccessPointController
- private lateinit var internetDetailsViewModelFactory: InternetDetailsViewModel.Factory
- private lateinit var internetDetailsContentManagerFactory: InternetDetailsContentManager.Factory
- private lateinit var internetDetailsViewModel: InternetDetailsViewModel
@Before
fun setup() {
internetDialogManager = mock<InternetDialogManager>()
controller = mock<AccessPointController>()
- internetDetailsViewModelFactory = mock<InternetDetailsViewModel.Factory>()
- internetDetailsContentManagerFactory = mock<InternetDetailsContentManager.Factory>()
- internetDetailsViewModel =
- InternetDetailsViewModel(
- onLongClick = {},
- accessPointController = mock<AccessPointController>(),
- contentManagerFactory = internetDetailsContentManagerFactory,
- )
- whenever(internetDetailsViewModelFactory.create(any())).thenReturn(internetDetailsViewModel)
-
underTest =
InternetTileUserActionInteractor(
kosmos.testScope.coroutineContext,
internetDialogManager,
controller,
inputHandler,
- internetDetailsViewModelFactory,
)
}
@@ -127,12 +108,4 @@ class InternetTileUserActionInteractorTest : SysuiTestCase() {
assertThat(it.intent.action).isEqualTo(Settings.ACTION_WIFI_SETTINGS)
}
}
-
- @Test
- fun detailsViewModel() =
- kosmos.testScope.runTest {
- assertThat(underTest.detailsViewModel.getTitle()).isEqualTo("Internet")
- assertThat(underTest.detailsViewModel.getSubTitle())
- .isEqualTo("Tab a network to connect")
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
index 0598a8b9d058..4e9b63517d6d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelTest.kt
@@ -25,6 +25,7 @@ import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.FakeTileDetailsViewModel
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
@@ -171,21 +172,6 @@ class QSTileViewModelTest : SysuiTestCase() {
.isEqualTo(FakeQSTileDataInteractor.AvailabilityRequest(USER))
}
- @Test
- fun tileDetails() =
- testScope.runTest {
- assertThat(tileUserActionInteractor.detailsViewModel).isNotNull()
- assertThat(tileUserActionInteractor.detailsViewModel?.getTitle())
- .isEqualTo("FakeQSTileUserActionInteractor")
- assertThat(underTest.detailsViewModel).isNotNull()
- assertThat(underTest.detailsViewModel?.getTitle())
- .isEqualTo("FakeQSTileUserActionInteractor")
-
- tileUserActionInteractor.detailsViewModel = null
- assertThat(tileUserActionInteractor.detailsViewModel).isNull()
- assertThat(underTest.detailsViewModel).isNull()
- }
-
private fun createViewModel(
scope: TestScope,
config: QSTileConfig = tileConfig,
@@ -209,6 +195,7 @@ class QSTileViewModelTest : SysuiTestCase() {
testCoroutineDispatcher,
testCoroutineDispatcher,
scope.backgroundScope,
+ FakeTileDetailsViewModel("QSTileViewModelTest"),
)
private companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
index ece21e1bef66..166e9500cff9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelUserInputTest.kt
@@ -22,6 +22,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.classifier.FalsingManagerFake
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.FakeTileDetailsViewModel
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.FakeDisabledByPolicyInteractor
@@ -253,5 +254,6 @@ class QSTileViewModelUserInputTest : SysuiTestCase() {
testCoroutineDispatcher,
testCoroutineDispatcher,
scope.backgroundScope,
+ FakeTileDetailsViewModel("QSTileViewModelUserInputTest"),
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
index c69ebab7a170..baf0aeb701d3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt
@@ -61,8 +61,7 @@ class QuickSettingsShadeOverlayContentViewModelTest : SysuiTestCase() {
usingMediaInComposeFragment = false // This is not for the compose fragment
}
private val testScope = kosmos.testScope
- private val sceneInteractor = kosmos.sceneInteractor
-
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val underTest by lazy { kosmos.quickSettingsShadeOverlayContentViewModel }
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 559e363d8937..d3f592357b14 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -73,9 +73,8 @@ class SceneInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val fakeSceneDataSource = kosmos.fakeSceneDataSource
-
- private val underTest = kosmos.sceneInteractor
+ private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource }
+ private val underTest by lazy { kosmos.sceneInteractor }
@Before
fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
index 4a011c0844e5..ccc876c20623 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
@@ -50,11 +50,10 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val configurationRepository = kosmos.fakeConfigurationRepository
- private val keyguardRepository = kosmos.fakeKeyguardRepository
- private val sceneInteractor = kosmos.sceneInteractor
+ private val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
+ private val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
-
private val underTest by lazy { kosmos.shadeInteractorSceneContainerImpl }
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index 37b4688f753d..a832f486ef32 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -15,7 +15,9 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.plugins.activityStarter
import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -51,12 +53,11 @@ import org.mockito.MockitoAnnotations
@RunWith(AndroidJUnit4::class)
@EnableSceneContainer
class ShadeHeaderViewModelTest : SysuiTestCase() {
- private val kosmos = testKosmos()
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val testScope = kosmos.testScope
- private val mobileIconsInteractor = kosmos.fakeMobileIconsInteractor
- private val sceneInteractor = kosmos.sceneInteractor
- private val deviceEntryInteractor = kosmos.deviceEntryInteractor
-
+ private val mobileIconsInteractor by lazy { kosmos.fakeMobileIconsInteractor }
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
+ private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor }
private val underTest by lazy { kosmos.shadeHeaderViewModel }
@Before
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
index 3d8da6140ff7..70df82d95008 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -22,6 +22,7 @@ import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_OPTION_
import static android.service.quickaccesswallet.Flags.FLAG_LAUNCH_WALLET_VIA_SYSUI_CALLBACKS;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowInsetsController.BEHAVIOR_DEFAULT;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -556,9 +557,9 @@ public class CommandQueueTest extends SysuiTestCase {
@Test
public void testImmersiveModeChanged() {
final int displayAreaId = 10;
- mCommandQueue.immersiveModeChanged(displayAreaId, true);
+ mCommandQueue.immersiveModeChanged(displayAreaId, true, TYPE_APPLICATION);
waitForIdleSync();
- verify(mCallbacks).immersiveModeChanged(displayAreaId, true);
+ verify(mCallbacks).immersiveModeChanged(displayAreaId, true, TYPE_APPLICATION);
}
@Test
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
index 6e4dc1485c7b..0cbc30d399d0 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockPickerConfig.kt
@@ -34,6 +34,9 @@ constructor(
/** Font axes that can be modified on this clock */
val axes: List<ClockFontAxis> = listOf(),
+
+ /** List of font presets for this clock. Can be assigned directly. */
+ val axisPresets: List<List<ClockFontAxisSetting>> = listOf(),
)
/** Represents an Axis that can be modified */
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
index 56176cfc0f91..d197cdb792e4 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
@@ -42,7 +42,7 @@ import java.util.function.Supplier;
@DependsOn(target = Icon.class)
@DependsOn(target = State.class)
public interface QSTile {
- int VERSION = 4;
+ int VERSION = 5;
String getTileSpec();
@@ -78,6 +78,7 @@ public interface QSTile {
void longClick(@Nullable Expandable expandable);
void userSwitch(int currentUser);
+ int getCurrentTileUser();
/**
* @deprecated not needed as {@link com.android.internal.logging.UiEvent} will use
@@ -150,6 +151,8 @@ public interface QSTile {
return null;
}
+ boolean isDestroyed();
+
@ProvidesInterface(version = Callback.VERSION)
interface Callback {
static final int VERSION = 2;
diff --git a/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml b/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.xml
new file mode 100644
index 000000000000..1de8c2b6c35d
--- /dev/null
+++ b/packages/SystemUI/res/drawable/notification_2025_guts_priority_button_bg.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
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <solid
+ android:color="@color/notification_guts_priority_button_bg_fill" />
+
+ <stroke
+ android:width="1.5dp"
+ android:color="@color/notification_guts_priority_button_bg_stroke" />
+
+ <corners android:radius="16dp" />
+</shape>
diff --git a/packages/SystemUI/res/layout/notification_2025_info.xml b/packages/SystemUI/res/layout/notification_2025_info.xml
new file mode 100644
index 000000000000..7b6916652924
--- /dev/null
+++ b/packages/SystemUI/res/layout/notification_2025_info.xml
@@ -0,0 +1,365 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<!-- extends LinearLayout -->
+<com.android.systemui.statusbar.notification.row.NotificationInfo
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/notification_guts"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:focusable="true"
+ android:clipChildren="false"
+ android:clipToPadding="true"
+ android:orientation="vertical"
+ android:paddingStart="@*android:dimen/notification_2025_margin">
+
+ <!-- Package Info -->
+ <LinearLayout
+ android:id="@+id/header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="true">
+ <ImageView
+ android:id="@+id/pkg_icon"
+ android:layout_width="@*android:dimen/notification_2025_icon_circle_size"
+ android:layout_height="@*android:dimen/notification_2025_icon_circle_size"
+ android:layout_marginTop="@*android:dimen/notification_2025_margin"
+ android:layout_marginEnd="@*android:dimen/notification_2025_margin" />
+ <LinearLayout
+ android:id="@+id/names"
+ android:layout_weight="1"
+ android:layout_width="0dp"
+ android:orientation="vertical"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@*android:dimen/notification_2025_margin"
+ android:minHeight="@*android:dimen/notification_2025_icon_circle_size">
+ <TextView
+ android:id="@+id/channel_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textDirection="locale"
+ style="@style/TextAppearance.NotificationImportanceChannel"/>
+ <TextView
+ android:id="@+id/group_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textDirection="locale"
+ android:ellipsize="end"
+ style="@style/TextAppearance.NotificationImportanceChannelGroup"/>
+ <TextView
+ android:id="@+id/pkg_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/TextAppearance.NotificationImportanceApp"
+ android:ellipsize="end"
+ android:textDirection="locale"
+ android:maxLines="1"/>
+ <TextView
+ android:id="@+id/delegate_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/TextAppearance.NotificationImportanceHeader"
+ android:ellipsize="end"
+ android:textDirection="locale"
+ android:text="@string/notification_delegate_header"
+ android:maxLines="1" />
+
+ </LinearLayout>
+
+ <!-- feedback for notificationassistantservice -->
+ <ImageButton
+ android:id="@+id/feedback"
+ android:layout_width="@dimen/notification_2025_guts_button_size"
+ android:layout_height="@dimen/notification_2025_guts_button_size"
+ android:visibility="gone"
+ android:background="@drawable/ripple_drawable"
+ android:contentDescription="@string/notification_guts_bundle_feedback"
+ android:src="@*android:drawable/ic_feedback"
+ android:paddingTop="@*android:dimen/notification_2025_margin"
+ android:tint="@androidprv:color/materialColorPrimary"/>
+
+ <!-- Optional link to app. Only appears if the channel is not disabled and the app
+ asked for it -->
+ <ImageButton
+ android:id="@+id/app_settings"
+ android:layout_width="@dimen/notification_2025_guts_button_size"
+ android:layout_height="@dimen/notification_2025_guts_button_size"
+ android:visibility="gone"
+ android:background="@drawable/ripple_drawable"
+ android:contentDescription="@string/notification_app_settings"
+ android:src="@drawable/ic_info"
+ android:paddingTop="@*android:dimen/notification_2025_margin"
+ android:tint="@androidprv:color/materialColorPrimary"/>
+
+ <!-- System notification settings -->
+ <ImageButton
+ android:id="@+id/info"
+ android:layout_width="@dimen/notification_2025_guts_button_size"
+ android:layout_height="@dimen/notification_2025_guts_button_size"
+ android:contentDescription="@string/notification_more_settings"
+ android:background="@drawable/ripple_drawable"
+ android:src="@drawable/ic_settings"
+ android:padding="@*android:dimen/notification_2025_margin"
+ android:tint="@androidprv:color/materialColorPrimary" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/inline_controls"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@*android:dimen/notification_2025_margin"
+ android:layout_marginTop="@*android:dimen/notification_2025_margin"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:orientation="vertical">
+
+ <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings-->
+ <TextView
+ android:id="@+id/non_configurable_text"
+ android:text="@string/notification_unblockable_desc"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+ <!-- Non configurable app/channel text. appears instead of @+id/interruptiveness_settings-->
+ <TextView
+ android:id="@+id/non_configurable_call_text"
+ android:text="@string/notification_unblockable_call_desc"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+ <!-- Non configurable multichannel text. appears instead of @+id/interruptiveness_settings-->
+ <TextView
+ android:id="@+id/non_configurable_multichannel_text"
+ android:text="@string/notification_multichannel_desc"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@*android:style/TextAppearance.DeviceDefault.Notification" />
+
+ <LinearLayout
+ android:id="@+id/interruptiveness_settings"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="vertical">
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+ android:id="@+id/automatic"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical"
+ android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal"
+ android:gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="@drawable/notification_2025_guts_priority_button_bg"
+ android:orientation="horizontal"
+ android:visibility="gone">
+ <ImageView
+ android:id="@+id/automatic_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="@*android:dimen/notification_2025_margin"
+ android:src="@drawable/ic_notifications_automatic"
+ android:background="@android:color/transparent"
+ android:tint="@color/notification_guts_priority_contents"
+ android:clickable="false"
+ android:focusable="false"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center"
+ >
+ <TextView
+ android:id="@+id/automatic_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="false"
+ android:focusable="false"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+ android:text="@string/notification_automatic_title"/>
+ <TextView
+ android:id="@+id/automatic_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/notification_importance_button_description_top_margin"
+ android:visibility="gone"
+ android:text="@string/notification_channel_summary_automatic"
+ android:clickable="false"
+ android:focusable="false"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+ </LinearLayout>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+ android:id="@+id/alert"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical"
+ android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal"
+ android:gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="@drawable/notification_2025_guts_priority_button_bg"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@+id/alert_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="@*android:dimen/notification_2025_margin"
+ android:src="@drawable/ic_notifications_alert"
+ android:background="@android:color/transparent"
+ android:tint="@color/notification_guts_priority_contents"
+ android:clickable="false"
+ android:focusable="false"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center"
+ >
+ <TextView
+ android:id="@+id/alert_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="false"
+ android:focusable="false"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+ android:text="@string/notification_alert_title"/>
+ <TextView
+ android:id="@+id/alert_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:text="@string/notification_channel_summary_default"
+ android:clickable="false"
+ android:focusable="false"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+ </LinearLayout>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+ <com.android.systemui.statusbar.notification.row.ButtonLinearLayout
+ android:id="@+id/silence"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/notification_importance_button_separation"
+ android:paddingVertical="@dimen/notification_2025_importance_button_padding_vertical"
+ android:paddingHorizontal="@dimen/notification_2025_importance_button_padding_horizontal"
+ android:gravity="center_vertical"
+ android:clickable="true"
+ android:focusable="true"
+ android:background="@drawable/notification_2025_guts_priority_button_bg"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@+id/silence_icon"
+ android:src="@drawable/ic_notifications_silence"
+ android:background="@android:color/transparent"
+ android:tint="@color/notification_guts_priority_contents"
+ android:layout_gravity="center"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingEnd="@*android:dimen/notification_2025_margin"
+ android:clickable="false"
+ android:focusable="false"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:gravity="center"
+ >
+ <TextView
+ android:id="@+id/silence_label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:clickable="false"
+ android:focusable="false"
+ android:layout_toEndOf="@id/silence_icon"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceButton"
+ android:text="@string/notification_silence_title"/>
+ <TextView
+ android:id="@+id/silence_summary"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:text="@string/notification_channel_summary_low"
+ android:clickable="false"
+ android:focusable="false"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.NotificationImportanceDetail"/>
+ </LinearLayout>
+ </com.android.systemui.statusbar.notification.row.ButtonLinearLayout>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/bottom_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@*android:dimen/notification_2025_margin"
+ android:minHeight="@dimen/notification_2025_guts_button_size"
+ android:gravity="center_vertical"
+ >
+ <TextView
+ android:id="@+id/turn_off_notifications"
+ android:text="@string/inline_turn_off_notifications"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="32dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="@*android:dimen/notification_2025_margin"
+ android:gravity="center"
+ android:minWidth="@dimen/notification_2025_min_tap_target_size"
+ android:minHeight="@dimen/notification_2025_min_tap_target_size"
+ android:maxWidth="200dp"
+ style="@style/TextAppearance.NotificationInfo.Button"
+ android:textSize="@*android:dimen/notification_2025_action_text_size"/>
+ <TextView
+ android:id="@+id/done"
+ android:text="@string/inline_ok_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dp"
+ android:paddingBottom="@*android:dimen/notification_2025_margin"
+ android:gravity="center"
+ android:minWidth="@dimen/notification_2025_min_tap_target_size"
+ android:minHeight="@dimen/notification_2025_min_tap_target_size"
+ android:maxWidth="125dp"
+ style="@style/TextAppearance.NotificationInfo.Button"
+ android:textSize="@*android:dimen/notification_2025_action_text_size"/>
+ </LinearLayout>
+ </LinearLayout>
+</com.android.systemui.statusbar.notification.row.NotificationInfo>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index b273886e286e..4995858f95a4 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -1125,4 +1125,7 @@
<!-- Configuration to swipe to open glanceable hub -->
<bool name="config_swipeToOpenGlanceableHub">false</bool>
+
+ <!-- Whether or not to show the UMO on the glanceable hub when media is playing. -->
+ <bool name="config_showUmoOnHub">false</bool>
</resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 640e1fa79530..7c370d3bc064 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -390,6 +390,12 @@
<!-- Extra space for guts bundle feedback button -->
<dimen name="notification_guts_bundle_feedback_size">48dp</dimen>
+ <!-- Size of icon buttons in notification info. -->
+ <!-- 24dp for the icon itself + 16dp * 2 for top and bottom padding -->
+ <dimen name="notification_2025_guts_button_size">56dp</dimen>
+
+ <dimen name="notification_2025_min_tap_target_size">48dp</dimen>
+
<dimen name="notification_importance_toggle_size">48dp</dimen>
<dimen name="notification_importance_button_separation">8dp</dimen>
<dimen name="notification_importance_drawable_padding">8dp</dimen>
@@ -402,6 +408,10 @@
<dimen name="notification_importance_button_description_top_margin">12dp</dimen>
<dimen name="rect_button_radius">8dp</dimen>
+ <!-- Padding for importance selection buttons in notification info, 2025 redesign version -->
+ <dimen name="notification_2025_importance_button_padding_vertical">12dp</dimen>
+ <dimen name="notification_2025_importance_button_padding_horizontal">16dp</dimen>
+
<!-- The minimum height for the snackbar shown after the snooze option has been chosen. -->
<dimen name="snooze_snackbar_min_height">56dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 084495f4b196..6ff1240c5e60 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3176,8 +3176,8 @@
<string name="controls_media_settings_button">Settings</string>
<!-- Description for media control's playing media item, including information for the media's title, the artist, and source app [CHAR LIMIT=NONE]-->
<string name="controls_media_playing_item_description"><xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> is playing from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string>
- <!-- Content description for media controls progress bar [CHAR_LIMIT=NONE] -->
- <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1 hour 2 minutes 30 seconds">%1$s</xliff:g> of <xliff:g id="total_time" example="4 hours 5 seconds">%2$s</xliff:g></string>
+ <!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] -->
+ <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string>
<!-- Placeholder title to inform user that an app has posted media controls [CHAR_LIMIT=NONE] -->
<string name="controls_media_empty_title"><xliff:g id="app_name" example="Foo Music App">%1$s</xliff:g> is running</string>
@@ -4178,4 +4178,7 @@
<string name="qs_edit_mode_reset_dialog_content">
All Quick Settings tiles will reset to the device’s original settings
</string>
+
+ <!-- Template that joins disabled message with the label for the voice over. [CHAR LIMIT=NONE] -->
+ <string name="volume_slider_disabled_message_template"><xliff:g example="Notification" id="stream_name">%1$s</xliff:g>, <xliff:g example="Disabled because ring is muted" id="disabled_message">%2$s</xliff:g></string>
</resources>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
index 4a4cb7a232c5..8f8bcf273af1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java
@@ -239,7 +239,6 @@ public class KeyguardPatternView extends KeyguardInputView
R.dimen.keyguard_pattern_activated_dot_size));
mLockPatternView.setPathWidth(
getResources().getDimensionPixelSize(R.dimen.keyguard_pattern_stroke_width));
- mLockPatternView.setKeepDotActivated(true);
}
mEcaView = findViewById(R.id.keyguard_selector_fade_container);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
index 7fb66640b29f..f6df42575bbd 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -36,6 +36,7 @@ import com.android.internal.widget.LockPatternView.Cell;
import com.android.internal.widget.LockscreenCredential;
import com.android.keyguard.EmergencyButtonController.EmergencyButtonCallback;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.systemui.Flags;
import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer;
import com.android.systemui.classifier.FalsingClassifier;
import com.android.systemui.classifier.FalsingCollector;
@@ -237,8 +238,12 @@ public class KeyguardPatternViewController
super.onViewAttached();
mLockPatternView.setOnPatternListener(new UnlockPatternListener());
mLockPatternView.setSaveEnabled(false);
- mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled(
- mSelectedUserInteractor.getSelectedUserId()));
+ boolean visiblePatternEnabled = mLockPatternUtils.isVisiblePatternEnabled(
+ mSelectedUserInteractor.getSelectedUserId());
+ mLockPatternView.setInStealthMode(!visiblePatternEnabled);
+ if (Flags.bouncerUiRevamp2()) {
+ mLockPatternView.setKeepDotActivated(visiblePatternEnabled);
+ }
mLockPatternView.setOnTouchListener((v, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
mFalsingCollector.avoidGesture();
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
index 208adc22a3e0..5f7dca8d649a 100644
--- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageInstallerMonitor.kt
@@ -64,15 +64,14 @@ constructor(
synchronized(sessions) {
sessions.putAll(
packageInstaller.allSessions
- .filter { !TextUtils.isEmpty(it.appPackageName) }
- .map { session -> session.toModel() }
+ .mapNotNull { session -> session.toModel() }
.associateBy { it.sessionId }
)
updateInstallerSessionsFlow()
}
packageInstaller.registerSessionCallback(
this@PackageInstallerMonitor,
- bgHandler
+ bgHandler,
)
} else {
synchronized(sessions) {
@@ -130,7 +129,7 @@ constructor(
if (session == null) {
sessions.remove(sessionId)
} else {
- sessions[sessionId] = session.toModel()
+ session.toModel()?.apply { sessions[sessionId] = this }
}
updateInstallerSessionsFlow()
}
@@ -144,7 +143,11 @@ constructor(
companion object {
const val TAG = "PackageInstallerMonitor"
- private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession {
+ private fun PackageInstaller.SessionInfo.toModel(): PackageInstallSession? {
+ if (TextUtils.isEmpty(this.appPackageName)) {
+ return null
+ }
+
return PackageInstallSession(
sessionId = this.sessionId,
packageName = this.appPackageName,
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/colors/SurfaceEffectColors.kt b/packages/SystemUI/src/com/android/systemui/common/shared/colors/SurfaceEffectColors.kt
index 5e8c21f9abf5..4451f07318ef 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/colors/SurfaceEffectColors.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/colors/SurfaceEffectColors.kt
@@ -16,23 +16,27 @@
package com.android.systemui.common.shared.colors
-import android.content.res.Resources
+import android.content.Context
object SurfaceEffectColors {
@JvmStatic
- fun surfaceEffect0(r: Resources): Int {
- return r.getColor(com.android.internal.R.color.surface_effect_0)
+ fun surfaceEffect0(context: Context): Int {
+ return context.resources.getColor(
+ com.android.internal.R.color.surface_effect_0, context.theme)
}
@JvmStatic
- fun surfaceEffect1(r: Resources): Int {
- return r.getColor(com.android.internal.R.color.surface_effect_1)
+ fun surfaceEffect1(context: Context): Int {
+ return context.resources.getColor(
+ com.android.internal.R.color.surface_effect_1, context.theme)
}
@JvmStatic
- fun surfaceEffect2(r: Resources): Int {
- return r.getColor(com.android.internal.R.color.surface_effect_2)
+ fun surfaceEffect2(context: Context): Int {
+ return context.resources.getColor(
+ com.android.internal.R.color.surface_effect_2, context.theme)
}
@JvmStatic
- fun surfaceEffect3(r: Resources): Int {
- return r.getColor(com.android.internal.R.color.surface_effect_3)
+ fun surfaceEffect3(context: Context): Int {
+ return context.resources.getColor(
+ com.android.internal.R.color.surface_effect_3, context.theme)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
index 48a6d9de380c..7765d0017c4e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalOngoingContentStartable.kt
@@ -16,7 +16,9 @@
package com.android.systemui.communal
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.CoreStartable
+import com.android.systemui.communal.dagger.CommunalModule.Companion.SHOW_UMO
import com.android.systemui.communal.data.repository.CommunalMediaRepository
import com.android.systemui.communal.data.repository.CommunalSmartspaceRepository
import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -24,8 +26,8 @@ import com.android.systemui.communal.domain.interactor.CommunalSettingsInteracto
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
+import javax.inject.Named
import kotlinx.coroutines.CoroutineScope
-import com.android.app.tracing.coroutines.launchTraced as launch
@SysUISingleton
class CommunalOngoingContentStartable
@@ -36,6 +38,7 @@ constructor(
private val communalMediaRepository: CommunalMediaRepository,
private val communalSettingsInteractor: CommunalSettingsInteractor,
private val communalSmartspaceRepository: CommunalSmartspaceRepository,
+ @Named(SHOW_UMO) private val showUmoOnHub: Boolean,
) : CoreStartable {
override fun start() {
@@ -46,10 +49,14 @@ constructor(
bgScope.launch {
communalInteractor.isCommunalEnabled.collect { enabled ->
if (enabled) {
- communalMediaRepository.startListening()
+ if (showUmoOnHub) {
+ communalMediaRepository.startListening()
+ }
communalSmartspaceRepository.startListening()
} else {
- communalMediaRepository.stopListening()
+ if (showUmoOnHub) {
+ communalMediaRepository.stopListening()
+ }
communalSmartspaceRepository.stopListening()
}
}
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 ff741625a3cc..bb3be531aa8a 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -105,6 +105,7 @@ interface CommunalModule {
const val LOGGABLE_PREFIXES = "loggable_prefixes"
const val LAUNCHER_PACKAGE = "launcher_package"
const val SWIPE_TO_HUB = "swipe_to_hub"
+ const val SHOW_UMO = "show_umo"
@Provides
@Communal
@@ -150,5 +151,11 @@ interface CommunalModule {
fun provideSwipeToHub(@Main resources: Resources): Boolean {
return resources.getBoolean(R.bool.config_swipeToOpenGlanceableHub)
}
+
+ @Provides
+ @Named(SHOW_UMO)
+ fun provideShowUmo(@Main resources: Resources): Boolean {
+ return resources.getBoolean(R.bool.config_showUmoOnHub)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 26501596aa1a..15a4722d3911 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -29,8 +29,6 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.notification.collection.SortBySectionTimeFlag
import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod
import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression
import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
@@ -52,8 +50,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha
NotificationMinimalism.token dependsOn NotificationThrottleHun.token
ModesEmptyShadeFix.token dependsOn modesUi
- PromotedNotificationUiAod.token dependsOn PromotedNotificationUi.token
-
// SceneContainer dependencies
SceneContainerFlag.getFlagDependencies().forEach { (alpha, beta) -> alpha dependsOn beta }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
index 74d471c7c1d6..e119ec94f8c8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt
@@ -101,7 +101,7 @@ constructor(
if (Flags.notificationShadeBlur()) {
transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
} else {
- emptyFlow()
+ transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
},
flowWhenShadeIsNotExpanded =
transitionAnimation.sharedFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
index 3c126aa23fef..f14a5a282e88 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToPrimaryBouncerTransitionViewModel.kt
@@ -51,7 +51,7 @@ constructor(
if (Flags.notificationShadeBlur()) {
transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
} else {
- emptyFlow()
+ transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
},
flowWhenShadeIsNotExpanded =
transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
index 5a111aa519fc..4a39421a3737 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt
@@ -32,7 +32,6 @@ import com.android.systemui.scene.shared.model.Overlays
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.emptyFlow
/**
* Breaks down PRIMARY BOUNCER->LOCKSCREEN transition into discrete steps for corresponding views to
@@ -81,7 +80,7 @@ constructor(
if (Flags.notificationShadeBlur()) {
transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
} else {
- emptyFlow()
+ transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
},
flowWhenShadeIsNotExpanded =
transitionAnimation.sharedFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt
index 0f0e7b6faa66..31b20a7ea828 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToOccludedTransitionViewModel.kt
@@ -27,7 +27,6 @@ import com.android.systemui.keyguard.ui.transitions.BlurConfig
import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.emptyFlow
@SysUISingleton
class PrimaryBouncerToOccludedTransitionViewModel
@@ -51,7 +50,7 @@ constructor(
if (Flags.notificationShadeBlur()) {
transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
} else {
- emptyFlow()
+ transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
},
flowWhenShadeIsNotExpanded =
transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx),
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt
index c9716be52408..34f7c4dcaec0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/binder/SeekBarObserver.kt
@@ -18,9 +18,6 @@ package com.android.systemui.media.controls.ui.binder
import android.animation.Animator
import android.animation.ObjectAnimator
-import android.icu.text.MeasureFormat
-import android.icu.util.Measure
-import android.icu.util.MeasureUnit
import android.text.format.DateUtils
import androidx.annotation.UiThread
import androidx.lifecycle.Observer
@@ -31,11 +28,8 @@ import com.android.systemui.media.controls.ui.drawable.SquigglyProgress
import com.android.systemui.media.controls.ui.view.MediaViewHolder
import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
import com.android.systemui.res.R
-import java.util.Locale
private const val TAG = "SeekBarObserver"
-private const val MIN_IN_SEC = 60
-private const val HOUR_IN_SEC = MIN_IN_SEC * 60
/**
* Observer for changes from SeekBarViewModel.
@@ -133,9 +127,10 @@ open class SeekBarObserver(private val holder: MediaViewHolder) :
}
holder.seekBar.setMax(data.duration)
- val totalTimeDescription = formatTimeContentDescription(data.duration)
+ val totalTimeString =
+ DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS)
if (data.scrubbing) {
- holder.scrubbingTotalTimeView.text = formatTimeLabel(data.duration)
+ holder.scrubbingTotalTimeView.text = totalTimeString
}
data.elapsedTime?.let {
@@ -153,62 +148,20 @@ open class SeekBarObserver(private val holder: MediaViewHolder) :
}
}
- val elapsedTimeDescription = formatTimeContentDescription(it)
+ val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS)
if (data.scrubbing) {
- holder.scrubbingElapsedTimeView.text = formatTimeLabel(it)
+ holder.scrubbingElapsedTimeView.text = elapsedTimeString
}
holder.seekBar.contentDescription =
holder.seekBar.context.getString(
R.string.controls_media_seekbar_description,
- elapsedTimeDescription,
- totalTimeDescription,
+ elapsedTimeString,
+ totalTimeString
)
}
}
- /** Returns a time string suitable for display, e.g. "12:34" */
- private fun formatTimeLabel(milliseconds: Int): CharSequence {
- return DateUtils.formatElapsedTime(milliseconds / DateUtils.SECOND_IN_MILLIS)
- }
-
- /**
- * Returns a time string suitable for content description, e.g. "12 minutes 34 seconds"
- *
- * Follows same logic as Chronometer#formatDuration
- */
- private fun formatTimeContentDescription(milliseconds: Int): CharSequence {
- var seconds = milliseconds / DateUtils.SECOND_IN_MILLIS
-
- val hours =
- if (seconds >= HOUR_IN_SEC) {
- seconds / HOUR_IN_SEC
- } else {
- 0
- }
- seconds -= hours * HOUR_IN_SEC
-
- val minutes =
- if (seconds >= MIN_IN_SEC) {
- seconds / MIN_IN_SEC
- } else {
- 0
- }
- seconds -= minutes * MIN_IN_SEC
-
- val measures = arrayListOf<Measure>()
- if (hours > 0) {
- measures.add(Measure(hours, MeasureUnit.HOUR))
- }
- if (minutes > 0) {
- measures.add(Measure(minutes, MeasureUnit.MINUTE))
- }
- measures.add(Measure(seconds, MeasureUnit.SECOND))
-
- return MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
- .formatMeasures(*measures.toTypedArray())
- }
-
@VisibleForTesting
open fun buildResetAnimator(targetTime: Int): Animator {
val animator =
@@ -216,7 +169,7 @@ open class SeekBarObserver(private val holder: MediaViewHolder) :
holder.seekBar,
"progress",
holder.seekBar.progress,
- targetTime + RESET_ANIMATION_DURATION_MS,
+ targetTime + RESET_ANIMATION_DURATION_MS
)
animator.setAutoCancel(true)
animator.duration = RESET_ANIMATION_DURATION_MS.toLong()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
index 609541ba1ab6..c70a854a2ca0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractor.kt
@@ -20,6 +20,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.UserHandle
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.Dumpable
import com.android.systemui.ProtoDumpable
import com.android.systemui.dagger.SysUISingleton
@@ -62,7 +63,6 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
-import com.android.app.tracing.coroutines.launchTraced as launch
import kotlinx.coroutines.withContext
/**
@@ -245,7 +245,6 @@ constructor(
processExistingTile(
tileSpec,
specsToTiles.getValue(tileSpec),
- userChanged,
newUser,
) ?: createTile(tileSpec)
} else {
@@ -378,7 +377,6 @@ constructor(
private fun processExistingTile(
tileSpec: TileSpec,
tileOrNotInstalled: TileOrNotInstalled,
- userChanged: Boolean,
user: Int,
): QSTile? {
return when (tileOrNotInstalled) {
@@ -386,6 +384,10 @@ constructor(
is TileOrNotInstalled.Tile -> {
val qsTile = tileOrNotInstalled.tile
when {
+ qsTile.isDestroyed -> {
+ logger.logTileDestroyedIgnored(tileSpec)
+ null
+ }
!qsTile.isAvailable -> {
logger.logTileDestroyed(
tileSpec,
@@ -399,10 +401,11 @@ constructor(
qsTile !is CustomTile -> {
// The tile is not a custom tile. Make sure they are reset to the correct
// user
- if (userChanged) {
+ if (qsTile.currentTileUser != user) {
qsTile.userSwitch(user)
logger.logTileUserChanged(tileSpec, user)
}
+
qsTile
}
qsTile.user == user -> {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
index e237ca9f462b..21a8ec604f08 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt
@@ -60,7 +60,7 @@ constructor(
bool1 = usesDefault
int1 = user
},
- { "Parsed tiles (default=$bool1, user=$int1): $str1" }
+ { "Parsed tiles (default=$bool1, user=$int1): $str1" },
)
}
@@ -77,7 +77,7 @@ constructor(
str2 = reconciledTiles.toString()
int1 = user
},
- { "Tiles restored and reconciled for user: $int1\nWas: $str1\nSet to: $str2" }
+ { "Tiles restored and reconciled for user: $int1\nWas: $str1\nSet to: $str2" },
)
}
@@ -94,7 +94,7 @@ constructor(
str2 = newList.toString()
int1 = userId
},
- { "Processing $str1 for user $int1\nNew list: $str2" }
+ { "Processing $str1 for user $int1\nNew list: $str2" },
)
}
@@ -107,7 +107,16 @@ constructor(
str1 = spec.toString()
str2 = reason.readable
},
- { "Tile $str1 destroyed. Reason: $str2" }
+ { "Tile $str1 destroyed. Reason: $str2" },
+ )
+ }
+
+ fun logTileDestroyedIgnored(spec: TileSpec) {
+ tileListLogBuffer.log(
+ TILE_LIST_TAG,
+ LogLevel.DEBUG,
+ { str1 = spec.toString() },
+ { "Tile $str1 ignored as it was already destroyed." },
)
}
@@ -117,7 +126,7 @@ constructor(
TILE_LIST_TAG,
LogLevel.DEBUG,
{ str1 = spec.toString() },
- { "Tile $str1 created" }
+ { "Tile $str1 created" },
)
}
@@ -127,7 +136,7 @@ constructor(
TILE_LIST_TAG,
LogLevel.VERBOSE,
{ str1 = spec.toString() },
- { "Tile $str1 not found in factory" }
+ { "Tile $str1 not found in factory" },
)
}
@@ -140,7 +149,7 @@ constructor(
str1 = spec.toString()
int1 = user
},
- { "User changed to $int1 for tile $str1" }
+ { "User changed to $int1 for tile $str1" },
)
}
@@ -156,7 +165,7 @@ constructor(
str1 = tiles.toString()
int1 = user
},
- { "Tiles kept for not installed packages for user $int1: $str1" }
+ { "Tiles kept for not installed packages for user $int1: $str1" },
)
}
@@ -168,7 +177,7 @@ constructor(
str1 = tiles.toString()
int1 = userId
},
- { "Auto add tiles parsed for user $int1: $str1" }
+ { "Auto add tiles parsed for user $int1: $str1" },
)
}
@@ -180,7 +189,7 @@ constructor(
str1 = tiles.toString()
int1 = userId
},
- { "Auto-add tiles reconciled for user $int1: $str1" }
+ { "Auto-add tiles reconciled for user $int1: $str1" },
)
}
@@ -193,7 +202,7 @@ constructor(
int2 = position
str1 = spec.toString()
},
- { "Tile $str1 auto added for user $int1 at position $int2" }
+ { "Tile $str1 auto added for user $int1 at position $int2" },
)
}
@@ -205,7 +214,7 @@ constructor(
int1 = userId
str1 = spec.toString()
},
- { "Tile $str1 auto removed for user $int1" }
+ { "Tile $str1 auto removed for user $int1" },
)
}
@@ -217,7 +226,7 @@ constructor(
int1 = userId
str1 = spec.toString()
},
- { "Tile $str1 unmarked as auto-added for user $int1" }
+ { "Tile $str1 unmarked as auto-added for user $int1" },
)
}
@@ -226,7 +235,7 @@ constructor(
RESTORE_TAG,
LogLevel.DEBUG,
{ int1 = userId },
- { "Restored from single intent after user setup complete for user $int1" }
+ { "Restored from single intent after user setup complete for user $int1" },
)
}
@@ -243,7 +252,7 @@ constructor(
"Restored settings data for user $int1\n" +
"\tRestored tiles: $str1\n" +
"\tRestored auto added tiles: $str2"
- }
+ },
)
}
@@ -258,7 +267,7 @@ constructor(
str1 = restoreProcessorClassName
str2 = step.name
},
- { "Restore $str2 processed by $str1" }
+ { "Restore $str2 processed by $str1" },
)
}
@@ -273,6 +282,6 @@ constructor(
enum class RestorePreprocessorStep {
PREPROCESSING,
- POSTPROCESSING
+ POSTPROCESSING,
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
index 1d1e9911884c..c6fc868b3dc8 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -74,6 +74,8 @@ import com.android.systemui.qs.logging.QSLogger;
import java.io.PrintWriter;
import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* Base quick-settings tile, extend this to create a new tile.
@@ -127,6 +129,8 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
private int mIsFullQs;
private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
+ private final AtomicBoolean mIsDestroyed = new AtomicBoolean(false);
+ private final AtomicInteger mCurrentTileUser = new AtomicInteger();
/**
* Provides a new {@link TState} of the appropriate type to use between this tile and the
@@ -203,6 +207,7 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
mMetricsLogger = metricsLogger;
mStatusBarStateController = statusBarStateController;
mActivityStarter = activityStarter;
+ mCurrentTileUser.set(host.getUserId());
resetStates();
mUiHandler.post(() -> mLifecycle.setCurrentState(CREATED));
@@ -352,11 +357,19 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
}
public void userSwitch(int newUserId) {
+ mCurrentTileUser.set(newUserId);
mHandler.obtainMessage(H.USER_SWITCH, newUserId, 0).sendToTarget();
postStale();
}
+ @Override
+ public int getCurrentTileUser() {
+ return mCurrentTileUser.get();
+ }
+
public void destroy() {
+ // We mark it as soon as we start the destroy process, as nothing can interrupt it.
+ mIsDestroyed.set(true);
mHandler.sendEmptyMessage(H.DESTROY);
}
@@ -365,7 +378,7 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
*
* Should be called upon creation of the tile, before performing other operations
*/
- public void initialize() {
+ public final void initialize() {
mHandler.sendEmptyMessage(H.INITIALIZE);
}
@@ -525,6 +538,11 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
});
}
+ @Override
+ public final boolean isDestroyed() {
+ return mIsDestroyed.get();
+ }
+
protected void checkIfRestrictionEnforcedByAdminOnly(State state, String userRestriction) {
EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
userRestriction, mHost.getUserId());
@@ -799,7 +817,7 @@ public abstract class QSTileImpl<TState extends State> implements QSTile, Lifecy
*/
@Override
public void dump(PrintWriter pw, String[] args) {
- pw.println(this.getClass().getSimpleName() + ":");
+ pw.print(this.getClass().getSimpleName() + ":");
pw.print(" "); pw.println(getState().toString());
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
index f80b8fb8cb1f..e48e943dd3f4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt
@@ -99,7 +99,7 @@ constructor(
}
override fun getDetailsViewModel(): TileDetailsViewModel {
- return internetDetailsViewModelFactory.create { longClick(null) }
+ return internetDetailsViewModelFactory.create()
}
override fun handleUpdateState(state: QSTile.BooleanState, arg: Any?) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
index e8c4274474e0..8ad4e16291c2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/QSTileUserActionInteractor.kt
@@ -28,17 +28,4 @@ interface QSTileUserActionInteractor<DATA_TYPE> {
* It's safe to run long running computations inside this function.
*/
@WorkerThread suspend fun handleInput(input: QSTileInput<DATA_TYPE>)
-
- /**
- * Provides the [TileDetailsViewModel] for constructing the corresponding details view.
- *
- * This property is defined here to reuse the business logic. For example, reusing the user
- * long-click as the go-to-settings callback in the details view.
- * Subclasses can override this property to provide a specific [TileDetailsViewModel]
- * implementation.
- *
- * @return The [TileDetailsViewModel] instance, or null if not implemented.
- */
- val detailsViewModel: TileDetailsViewModel?
- get() = null
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
index 8c75cf001441..7f475f31b940 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelFactory.kt
@@ -19,6 +19,7 @@ package com.android.systemui.qs.tiles.base.viewmodel
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tiles.base.analytics.QSTileAnalytics
import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor
@@ -70,9 +71,7 @@ sealed interface QSTileViewModelFactory<T> {
* Creates [QSTileViewModelImpl] based on the interactors obtained from [QSTileComponent].
* Reference of that [QSTileComponent] is then stored along the view model.
*/
- fun create(
- tileSpec: TileSpec,
- ): QSTileViewModel {
+ fun create(tileSpec: TileSpec): QSTileViewModel {
val config = qsTileConfigProvider.getConfig(tileSpec.spec)
val component =
customTileComponentBuilder.qsTileConfigModule(QSTileConfigModule(config)).build()
@@ -90,6 +89,7 @@ sealed interface QSTileViewModelFactory<T> {
backgroundDispatcher,
uiBackgroundDispatcher,
component.coroutineScope(),
+ /* tileDetailsViewModel= */ null,
)
}
}
@@ -127,6 +127,7 @@ sealed interface QSTileViewModelFactory<T> {
userActionInteractor: QSTileUserActionInteractor<T>,
tileDataInteractor: QSTileDataInteractor<T>,
mapper: QSTileDataToStateMapper<T>,
+ tileDetailsViewModel: TileDetailsViewModel? = null,
): QSTileViewModelImpl<T> =
QSTileViewModelImpl(
qsTileConfigProvider.getConfig(tileSpec.spec),
@@ -142,6 +143,7 @@ sealed interface QSTileViewModelFactory<T> {
backgroundDispatcher,
uiBackgroundDispatcher,
coroutineScopeFactory.create(),
+ tileDetailsViewModel,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
index 30bf5b309a2e..3866c17b655f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImpl.kt
@@ -83,6 +83,7 @@ class QSTileViewModelImpl<DATA_TYPE>(
private val backgroundDispatcher: CoroutineDispatcher,
uiBackgroundDispatcher: CoroutineDispatcher,
private val tileScope: CoroutineScope,
+ override val tileDetailsViewModel: TileDetailsViewModel? = null,
) : QSTileViewModel, Dumpable {
private val users: MutableStateFlow<UserHandle> =
@@ -96,6 +97,9 @@ class QSTileViewModelImpl<DATA_TYPE>(
private val tileData: SharedFlow<DATA_TYPE?> = createTileDataFlow()
+ override val currentTileUser: Int
+ get() = users.value.identifier
+
override val state: StateFlow<QSTileState?> =
tileData
.map { data ->
@@ -114,9 +118,6 @@ class QSTileViewModelImpl<DATA_TYPE>(
.flowOn(backgroundDispatcher)
.stateIn(tileScope, SharingStarted.WhileSubscribed(), true)
- override val detailsViewModel: TileDetailsViewModel?
- get() = userActionInteractor().detailsViewModel
-
override fun forceUpdate() {
tileScope.launch(context = backgroundDispatcher) { forceUpdates.emit(Unit) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
index 0ed56f62ee6c..6709fd2bb508 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
@@ -16,9 +16,11 @@
package com.android.systemui.qs.tiles.dialog
+import android.content.Intent
+import android.provider.Settings
import com.android.systemui.plugins.qs.TileDetailsViewModel
+import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
import com.android.systemui.statusbar.connectivity.AccessPointController
-import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -27,10 +29,13 @@ class InternetDetailsViewModel
constructor(
private val accessPointController: AccessPointController,
val contentManagerFactory: InternetDetailsContentManager.Factory,
- @Assisted private val onLongClick: () -> Unit,
+ private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
) : TileDetailsViewModel() {
override fun clickOnSettingsButton() {
- onLongClick()
+ qsTileIntentUserActionHandler.handle(
+ /* expandable= */ null,
+ Intent(Settings.ACTION_WIFI_SETTINGS),
+ )
}
override fun getTitle(): String {
@@ -58,7 +63,7 @@ constructor(
}
@AssistedFactory
- interface Factory {
- fun create(onLongClick: () -> Unit): InternetDetailsViewModel
+ fun interface Factory {
+ fun create(): InternetDetailsViewModel
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
index 0adc41313bae..8d4a24e0c2cf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacy.java
@@ -400,6 +400,9 @@ public class InternetDialogDelegateLegacy implements
mInternetDialogTitle.setText(internetContent.mInternetDialogTitleString);
mInternetDialogSubTitle.setText(internetContent.mInternetDialogSubTitle);
+ if (!internetContent.mIsWifiEnabled) {
+ setProgressBarVisible(false);
+ }
mAirplaneModeButton.setVisibility(
internetContent.mIsAirplaneModeEnabled ? View.VISIBLE : View.GONE);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt
index 0ed46e73958d..5f692f2f5a73 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/di/QSTileComponent.kt
@@ -16,6 +16,7 @@
package com.android.systemui.qs.tiles.impl.di
+import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
index 8e48fe492e13..0431e36fef6a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt
@@ -18,13 +18,10 @@ package com.android.systemui.qs.tiles.impl.internet.domain.interactor
import android.content.Intent
import android.provider.Settings
-import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.interactor.QSTileInput
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
-import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
import com.android.systemui.qs.tiles.dialog.InternetDialogManager
import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel
import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
@@ -41,7 +38,6 @@ constructor(
private val internetDialogManager: InternetDialogManager,
private val accessPointController: AccessPointController,
private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
- private val internetDetailsViewModelFactory: InternetDetailsViewModel.Factory,
) : QSTileUserActionInteractor<InternetTileModel> {
override suspend fun handleInput(input: QSTileInput<InternetTileModel>): Unit =
@@ -58,16 +54,12 @@ constructor(
}
}
is QSTileUserAction.LongClick -> {
- handleLongClick(action.expandable)
+ qsTileIntentUserActionHandler.handle(
+ action.expandable,
+ Intent(Settings.ACTION_WIFI_SETTINGS),
+ )
}
else -> {}
}
}
-
- override val detailsViewModel: TileDetailsViewModel =
- internetDetailsViewModelFactory.create { handleLongClick(null) }
-
- private fun handleLongClick(expandable: Expandable?) {
- qsTileIntentUserActionHandler.handle(expandable, Intent(Settings.ACTION_WIFI_SETTINGS))
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
index e8b9926e5cea..7a533883444e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModel.kt
@@ -39,9 +39,12 @@ interface QSTileViewModel {
val isAvailable: StateFlow<Boolean>
/** Specifies the [TileDetailsViewModel] for constructing the corresponding details view. */
- val detailsViewModel: TileDetailsViewModel?
+ val tileDetailsViewModel: TileDetailsViewModel?
get() = null
+ /** Returns the current user for this tile */
+ val currentTileUser: Int
+
/**
* Notifies about the user change. Implementations should avoid using 3rd party userId sources
* and use this value instead. This is to maintain consistent and concurrency-free behaviour
@@ -65,8 +68,6 @@ interface QSTileViewModel {
fun destroy()
}
-/**
- * Returns the immediate state of the tile or null if the state haven't been collected yet.
- */
+/** Returns the immediate state of the tile or null if the state haven't been collected yet. */
val QSTileViewModel.currentState: QSTileState?
get() = state.value
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
index 30d1f05771d7..e607eae8f38d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt
@@ -19,7 +19,6 @@ package com.android.systemui.qs.tiles.viewmodel
import android.content.Context
import android.os.UserHandle
import android.util.Log
-import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.logging.InstanceId
import com.android.systemui.Dumpable
import com.android.systemui.animation.Expandable
@@ -156,8 +155,12 @@ constructor(
qsTileViewModel.onUserChanged(UserHandle.of(currentUser))
}
+ override fun getCurrentTileUser(): Int {
+ return qsTileViewModel.currentTileUser
+ }
+
override fun getDetailsViewModel(): TileDetailsViewModel? {
- return qsTileViewModel.detailsViewModel
+ return qsTileViewModel.tileDetailsViewModel
}
@Deprecated(
@@ -213,6 +216,10 @@ constructor(
qsTileViewModel.destroy()
}
+ override fun isDestroyed(): Boolean {
+ return !(tileAdapterJob?.isActive ?: false)
+ }
+
override fun getState(): QSTile.AdapterState =
qsTileViewModel.currentState?.let { mapState(context, it, qsTileViewModel.config) }
?: QSTile.AdapterState()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/StubQSTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/StubQSTileViewModel.kt
index 00b7e61eb1c6..bdd5c73779cf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/StubQSTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/StubQSTileViewModel.kt
@@ -37,4 +37,7 @@ object StubQSTileViewModel : QSTileViewModel {
override fun onActionPerformed(userAction: QSTileUserAction) = error("Don't call stubs")
override fun destroy() = error("Don't call stubs")
+
+ override val currentTileUser: Int
+ get() = error("Don't call stubs")
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
index 7dc2ae71b63e..e44701dba87c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java
@@ -580,7 +580,8 @@ public class CommandQueue extends IStatusBar.Stub implements
/**
* @see IStatusBar#immersiveModeChanged
*/
- default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {}
+ default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode,
+ int windowType) {}
/**
* @see IStatusBar#moveFocusedTaskToDesktop(int)
@@ -876,11 +877,13 @@ public class CommandQueue extends IStatusBar.Stub implements
}
@Override
- public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode,
+ int windowType) {
synchronized (mLock) {
final SomeArgs args = SomeArgs.obtain();
args.argi1 = rootDisplayAreaId;
args.argi2 = isImmersiveMode ? 1 : 0;
+ args.argi3 = windowType;
mHandler.obtainMessage(MSG_IMMERSIVE_CHANGED, args).sendToTarget();
}
}
@@ -2030,8 +2033,10 @@ public class CommandQueue extends IStatusBar.Stub implements
args = (SomeArgs) msg.obj;
int rootDisplayAreaId = args.argi1;
boolean isImmersiveMode = args.argi2 != 0;
+ int windowType = args.argi3;
for (int i = 0; i < mCallbacks.size(); i++) {
- mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+ mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode,
+ windowType);
}
break;
case MSG_ENTER_DESKTOP: {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
index fed3f6e81130..97e62d79b374 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java
@@ -23,6 +23,8 @@ import static android.app.StatusBarManager.DISABLE_HOME;
import static android.app.StatusBarManager.DISABLE_RECENT;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
+import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
@@ -208,7 +210,8 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca
}
@Override
- public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
+ public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode,
+ int windowType) {
mHandler.removeMessages(H.SHOW);
if (isImmersiveMode) {
if (DEBUG) Log.d(TAG, "immersiveModeChanged() sConfirmed=" + sConfirmed);
@@ -221,7 +224,9 @@ public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Ca
&& mCanSystemBarsBeShownByUser
&& !mNavBarEmpty
&& !UserManager.isDeviceInDemoMode(mDisplayContext)
- && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) {
+ && (mLockTaskState != LOCK_TASK_MODE_LOCKED)
+ && windowType != TYPE_PRESENTATION
+ && windowType != TYPE_PRIVATE_PRESENTATION) {
final Message msg = mHandler.obtainMessage(
H.SHOW);
msg.arg1 = rootDisplayAreaId;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
index 1009028345de..48f0245fd5db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
@@ -34,6 +34,7 @@ import com.android.systemui.qs.tiles.InternetTileNewImpl
import com.android.systemui.qs.tiles.NfcTile
import com.android.systemui.qs.tiles.base.interactor.QSTileAvailabilityInteractor
import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
+import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
import com.android.systemui.qs.tiles.impl.airplane.domain.AirplaneModeMapper
import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileDataInteractor
import com.android.systemui.qs.tiles.impl.airplane.domain.interactor.AirplaneModeTileUserActionInteractor
@@ -162,13 +163,15 @@ interface ConnectivityModule {
factory: QSTileViewModelFactory.Static<AirplaneModeTileModel>,
mapper: AirplaneModeMapper,
stateInteractor: AirplaneModeTileDataInteractor,
- userActionInteractor: AirplaneModeTileUserActionInteractor
+ userActionInteractor: AirplaneModeTileUserActionInteractor,
+ internetDetailsViewModelFactory: InternetDetailsViewModel.Factory
): QSTileViewModel =
factory.create(
TileSpec.create(AIRPLANE_MODE_TILE_SPEC),
userActionInteractor,
stateInteractor,
mapper,
+ internetDetailsViewModelFactory.create(),
)
@Provides
@@ -226,13 +229,15 @@ interface ConnectivityModule {
factory: QSTileViewModelFactory.Static<InternetTileModel>,
mapper: InternetTileMapper,
stateInteractor: InternetTileDataInteractor,
- userActionInteractor: InternetTileUserActionInteractor
+ userActionInteractor: InternetTileUserActionInteractor,
+ internetDetailsViewModelFactory: InternetDetailsViewModel.Factory
): QSTileViewModel =
factory.create(
TileSpec.create(INTERNET_TILE_SPEC),
userActionInteractor,
stateInteractor,
mapper,
+ internetDetailsViewModelFactory.create(),
)
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index 25deec375c03..d09546fe80ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -391,7 +391,7 @@ public class FooterView extends StackScrollerDecorView {
if (!notificationFooterBackgroundTintOptimization()) {
if (notificationShadeBlur()) {
Color backgroundColor = Color.valueOf(
- SurfaceEffectColors.surfaceEffect1(getResources()));
+ SurfaceEffectColors.surfaceEffect1(getContext()));
scHigh = ColorUtils.setAlphaComponent(backgroundColor.toArgb(), 0xFF);
// Apply alpha on background drawables.
int backgroundAlpha = (int) (backgroundColor.alpha() * 0xFF);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 4ed9dcee072e..a081ad5bb82c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -129,7 +129,7 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
private void updateColors() {
if (notificationRowTransparency()) {
- mNormalColor = SurfaceEffectColors.surfaceEffect1(getResources());
+ mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext());
} else {
mNormalColor = mContext.getColor(
com.android.internal.R.color.materialColorSurfaceContainerHigh);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
index 6bfc9f07ffc4..4bd6053ea23c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
@@ -21,7 +21,6 @@ import android.app.INotificationManager
import android.app.NotificationChannel
import android.app.NotificationChannel.DEFAULT_CHANNEL_ID
import android.app.NotificationChannelGroup
-import android.app.NotificationManager.IMPORTANCE_NONE
import android.app.NotificationManager.Importance
import android.content.Context
import android.graphics.Color
@@ -40,7 +39,7 @@ import android.widget.TextView
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.res.R
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.shade.ShadeDisplayAware
+import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
import javax.inject.Inject
private const val TAG = "ChannelDialogController"
@@ -59,9 +58,9 @@ private const val TAG = "ChannelDialogController"
*/
@SysUISingleton
class ChannelEditorDialogController @Inject constructor(
- @ShadeDisplayAware private val context: Context,
+ private val shadeDialogContextInteractor: ShadeDialogContextInteractor,
private val noMan: INotificationManager,
- private val dialogBuilder: ChannelEditorDialog.Builder
+ private val dialogBuilder: ChannelEditorDialog.Builder,
) {
private var prepared = false
@@ -272,7 +271,7 @@ class ChannelEditorDialogController @Inject constructor(
}
private fun initDialog() {
- dialogBuilder.setContext(context)
+ dialogBuilder.setContext(shadeDialogContextInteractor.context)
dialog = dialogBuilder.build()
dialog.window?.requestFeature(Window.FEATURE_NO_TITLE)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
index 33c36d8c4c76..c0bc13270664 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationBackgroundView.java
@@ -88,7 +88,7 @@ public class NotificationBackgroundView extends View implements Dumpable,
mDarkColoredStatefulColors = getResources().getColorStateList(
R.color.notification_state_color_dark);
if (notificationRowTransparency()) {
- mNormalColor = SurfaceEffectColors.surfaceEffect1(getResources());
+ mNormalColor = SurfaceEffectColors.surfaceEffect1(getContext());
} else {
mNormalColor = mContext.getColor(
com.android.internal.R.color.materialColorSurfaceContainerHigh);
@@ -321,7 +321,7 @@ public class NotificationBackgroundView extends View implements Dumpable,
new PorterDuffColorFilter(
isColorized()
? ColorUtils.setAlphaComponent(mTintColor, (int) (255 * 0.9f))
- : SurfaceEffectColors.surfaceEffect1(getResources()),
+ : SurfaceEffectColors.surfaceEffect1(getContext()),
PorterDuff.Mode.SRC)); // SRC operator discards the drawable's color+alpha
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
index ab382df13d10..e89a76fd5a69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.notification.row;
-import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS;
+import static android.app.Flags.notificationsRedesignTemplates;
import static android.view.HapticFeedbackConstants.CLOCK_TICK;
import static com.android.systemui.SwipeHelper.SWIPED_FAR_ENOUGH_SIZE_FRACTION;
@@ -706,8 +706,11 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
static NotificationMenuItem createInfoItem(Context context) {
Resources res = context.getResources();
String infoDescription = res.getString(R.string.notification_menu_gear_description);
+ int layoutId = notificationsRedesignTemplates()
+ ? R.layout.notification_2025_info
+ : R.layout.notification_info;
NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
- R.layout.notification_info, null, false);
+ layoutId, null, false);
return new NotificationMenuItem(context, infoDescription, infoContent,
R.drawable.ic_settings);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
index da988589184f..9bd5a5bd903f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt
@@ -291,7 +291,7 @@ constructor(
* currently being swiped. From the center outwards, the multipliers apply to the neighbors
* of the swiped view.
*/
- private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.18f, 0.28f, 0.5f, 0.28f, 0.18f)
+ private val MAGNETIC_TRANSLATION_MULTIPLIERS = listOf(0.04f, 0.12f, 0.5f, 0.12f, 0.04f)
const val MAGNETIC_REDUCTION = 0.65f
@@ -299,7 +299,7 @@ constructor(
private const val DETACH_STIFFNESS = 800f
private const val DETACH_DAMPING_RATIO = 0.95f
private const val SNAP_BACK_STIFFNESS = 550f
- private const val SNAP_BACK_DAMPING_RATIO = 0.52f
+ private const val SNAP_BACK_DAMPING_RATIO = 0.6f
// Maximum value of corner roundness that gets applied during the pre-detach dragging
private const val MAX_PRE_DETACH_ROUNDNESS = 0.8f
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
index a0e3fbd2f3d1..8b0f7c4d2451 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -29,18 +29,13 @@ import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SliderDefaults
-import androidx.compose.material3.SliderState
-import androidx.compose.material3.VerticalSlider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
@@ -49,16 +44,17 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.theme.PlatformTheme
import com.android.compose.ui.graphics.painter.DrawablePainter
+import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
-import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
import com.android.systemui.volume.dialog.sliders.ui.compose.VolumeDialogSliderTrack
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
-import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
+import com.android.systemui.volume.ui.slider.AccessibilityParams
+import com.android.systemui.volume.ui.slider.Haptics
+import com.android.systemui.volume.ui.slider.Slider
import javax.inject.Inject
-import kotlin.math.round
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
@@ -90,7 +86,7 @@ constructor(
}
}
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun VolumeDialogSlider(
viewModel: VolumeDialogSliderViewModel,
@@ -108,59 +104,8 @@ private fun VolumeDialogSlider(
)
val collectedSliderStateModel by viewModel.state.collectAsStateWithLifecycle(null)
val sliderStateModel = collectedSliderStateModel ?: return
-
- val steps = with(sliderStateModel.valueRange) { endInclusive - start - 1 }.toInt()
-
val interactionSource = remember { MutableInteractionSource() }
- val hapticsViewModel: SliderHapticsViewModel? =
- hapticsViewModelFactory?.let {
- rememberViewModel(traceName = "SliderHapticsViewModel") {
- it.create(
- interactionSource,
- sliderStateModel.valueRange,
- Orientation.Vertical,
- VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(
- sliderStateModel.valueRange
- ),
- VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
- )
- }
- }
- val sliderState =
- remember(steps, sliderStateModel.valueRange) {
- SliderState(
- value = sliderStateModel.value,
- valueRange = sliderStateModel.valueRange,
- steps = steps,
- )
- .also { sliderState ->
- sliderState.onValueChangeFinished = {
- viewModel.onSliderChangeFinished(sliderState.value)
- hapticsViewModel?.onValueChangeEnded()
- }
- sliderState.onValueChange = { newValue ->
- sliderState.value = newValue
- hapticsViewModel?.addVelocityDataPoint(newValue)
- overscrollViewModel.setSlider(
- value = sliderState.value,
- min = sliderState.valueRange.start,
- max = sliderState.valueRange.endInclusive,
- )
- viewModel.setStreamVolume(newValue, true)
- }
- }
- }
-
- var lastDiscreteStep by remember { mutableFloatStateOf(round(sliderStateModel.value)) }
- LaunchedEffect(sliderStateModel.value) {
- val value = sliderStateModel.value
- sliderState.value = value
- if (value != lastDiscreteStep) {
- lastDiscreteStep = value
- hapticsViewModel?.onValueChange(value)
- }
- }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect {
when (it) {
@@ -171,24 +116,33 @@ private fun VolumeDialogSlider(
}
}
- VerticalSlider(
- state = sliderState,
- enabled = !sliderStateModel.isDisabled,
- reverseDirection = true,
+ Slider(
+ value = sliderStateModel.value,
+ valueRange = sliderStateModel.valueRange,
+ onValueChanged = { value ->
+ overscrollViewModel.setSlider(
+ value = value,
+ min = sliderStateModel.valueRange.start,
+ max = sliderStateModel.valueRange.endInclusive,
+ )
+ viewModel.setStreamVolume(value, true)
+ },
+ onValueChangeFinished = { viewModel.onSliderChangeFinished(it) },
+ isEnabled = !sliderStateModel.isDisabled,
+ isReverseDirection = true,
+ isVertical = true,
colors = colors,
interactionSource = interactionSource,
- modifier =
- modifier.pointerInput(Unit) {
- coroutineScope {
- val currentContext = currentCoroutineContext()
- awaitPointerEventScope {
- while (currentContext.isActive) {
- viewModel.onTouchEvent(awaitPointerEvent())
- }
- }
- }
- },
- track = {
+ haptics =
+ hapticsViewModelFactory?.let {
+ Haptics.Enabled(
+ hapticsViewModelFactory = it,
+ hapticFilter = SliderHapticFeedbackFilter(),
+ orientation = Orientation.Vertical,
+ )
+ } ?: Haptics.Disabled,
+ stepDistance = 1f,
+ track = { sliderState ->
VolumeDialogSliderTrack(
sliderState,
colors = colors,
@@ -201,6 +155,19 @@ private fun VolumeDialogSlider(
},
)
},
+ accessibilityParams =
+ AccessibilityParams(label = "", currentStateDescription = "", disabledMessage = ""),
+ modifier =
+ modifier.pointerInput(Unit) {
+ coroutineScope {
+ val currentContext = currentCoroutineContext()
+ awaitPointerEventScope {
+ while (currentContext.isActive) {
+ viewModel.onTouchEvent(awaitPointerEvent())
+ }
+ }
+ }
+ },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
index 3efb2b464a1d..3d98ebacc7ca 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioSharingStreamSliderViewModel.kt
@@ -116,8 +116,8 @@ constructor(
override val isEnabled: Boolean
get() = true
- override val a11yStep: Int
- get() = 1
+ override val a11yStep: Float
+ get() = 1f
override val disabledMessage: String?
get() = null
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index f9d776bc3aaf..9d32285fecb3 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -165,7 +165,7 @@ constructor(
label = label,
disabledMessage = disabledMessage,
isEnabled = isEnabled,
- a11yStep = volumeRange.step,
+ a11yStep = volumeRange.step.toFloat(),
a11yClickDescription =
if (isAffectedByMute) {
context.getString(
@@ -307,7 +307,7 @@ constructor(
override val label: String,
override val disabledMessage: String?,
override val isEnabled: Boolean,
- override val a11yStep: Int,
+ override val a11yStep: Float,
override val a11yClickDescription: String?,
override val a11yStateDescription: String?,
override val isMutable: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
index d74a433ad86c..a6c809186ca5 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
@@ -86,7 +86,7 @@ constructor(
icon = Icon.Resource(R.drawable.ic_cast, null),
label = context.getString(R.string.media_device_cast),
isEnabled = true,
- a11yStep = 1,
+ a11yStep = 1f,
)
}
@@ -96,7 +96,7 @@ constructor(
override val icon: Icon,
override val label: String,
override val isEnabled: Boolean,
- override val a11yStep: Int,
+ override val a11yStep: Float,
) : SliderState {
override val hapticFilter: SliderHapticFeedbackFilter
get() = SliderHapticFeedbackFilter()
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
index f1353713799d..4bc237bd36f5 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
@@ -36,7 +36,7 @@ sealed interface SliderState {
* A11y slider controls works by adjusting one step up or down. The default slider step isn't
* enough to trigger rounding to the correct value.
*/
- val a11yStep: Int
+ val a11yStep: Float
val a11yClickDescription: String?
val a11yStateDescription: String?
val disabledMessage: String?
@@ -49,7 +49,7 @@ sealed interface SliderState {
override val icon: Icon? = null
override val label: String = ""
override val disabledMessage: String? = null
- override val a11yStep: Int = 0
+ override val a11yStep: Float = 0f
override val a11yClickDescription: String? = null
override val a11yStateDescription: String? = null
override val isEnabled: Boolean = true
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt
new file mode 100644
index 000000000000..d3562e2a4235
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/ui/slider/Slider.kt
@@ -0,0 +1,265 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+
+package com.android.systemui.volume.ui.slider
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderColors
+import androidx.compose.material3.SliderDefaults
+import androidx.compose.material3.SliderState
+import androidx.compose.material3.VerticalSlider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.disabled
+import androidx.compose.ui.semantics.progressBarRangeInfo
+import androidx.compose.ui.semantics.setProgress
+import androidx.compose.ui.semantics.stateDescription
+import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
+import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.res.R
+import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
+import kotlin.math.round
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+private val defaultSpring =
+ SpringSpec<Float>(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessHigh)
+private val defaultTrack: @Composable (SliderState) -> Unit =
+ @Composable { SliderDefaults.Track(it) }
+
+@Composable
+fun Slider(
+ value: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ onValueChanged: (Float) -> Unit,
+ onValueChangeFinished: ((Float) -> Unit)?,
+ stepDistance: Float,
+ isEnabled: Boolean,
+ accessibilityParams: AccessibilityParams,
+ modifier: Modifier = Modifier,
+ colors: SliderColors = SliderDefaults.colors(),
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ haptics: Haptics = Haptics.Disabled,
+ isVertical: Boolean = false,
+ isReverseDirection: Boolean = false,
+ track: (@Composable (SliderState) -> Unit)? = null,
+) {
+ require(stepDistance > 0) { "stepDistance must be positive" }
+ val coroutineScope = rememberCoroutineScope()
+ val snappedValue = snapValue(value, valueRange, stepDistance)
+ val hapticsViewModel = haptics.createViewModel(snappedValue, valueRange, interactionSource)
+
+ val animatable = remember { Animatable(snappedValue) }
+ var animationJob: Job? by remember { mutableStateOf(null) }
+ val sliderState =
+ remember(valueRange) { SliderState(value = snappedValue, valueRange = valueRange) }
+ val valueChange: (Float) -> Unit = { newValue ->
+ hapticsViewModel?.onValueChange(newValue)
+ val snappedNewValue = snapValue(newValue, valueRange, stepDistance)
+ if (animatable.targetValue != snappedNewValue) {
+ onValueChanged(snappedNewValue)
+ animationJob?.cancel()
+ animationJob =
+ coroutineScope.launch {
+ animatable.animateTo(
+ targetValue = snappedNewValue,
+ animationSpec = defaultSpring,
+ )
+ }
+ }
+ }
+ val semantics =
+ accessibilityParams.createSemantics(
+ animatable.targetValue,
+ valueRange,
+ valueChange,
+ isEnabled,
+ stepDistance,
+ )
+
+ LaunchedEffect(snappedValue) {
+ if (!animatable.isRunning && animatable.targetValue != snappedValue) {
+ animationJob?.cancel()
+ animationJob =
+ coroutineScope.launch {
+ animatable.animateTo(targetValue = snappedValue, animationSpec = defaultSpring)
+ }
+ }
+ }
+
+ sliderState.onValueChangeFinished = {
+ hapticsViewModel?.onValueChangeEnded()
+ onValueChangeFinished?.invoke(animatable.targetValue)
+ }
+ sliderState.onValueChange = valueChange
+ sliderState.value = animatable.value
+
+ if (isVertical) {
+ VerticalSlider(
+ state = sliderState,
+ enabled = isEnabled,
+ reverseDirection = isReverseDirection,
+ interactionSource = interactionSource,
+ colors = colors,
+ track = track ?: defaultTrack,
+ modifier = modifier.clearAndSetSemantics(semantics),
+ )
+ } else {
+ Slider(
+ state = sliderState,
+ enabled = isEnabled,
+ interactionSource = interactionSource,
+ colors = colors,
+ track = track ?: defaultTrack,
+ modifier = modifier.clearAndSetSemantics(semantics),
+ )
+ }
+}
+
+private fun snapValue(
+ value: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ stepDistance: Float,
+): Float {
+ if (stepDistance == 0f) {
+ return value
+ }
+ val coercedValue = value.coerceIn(valueRange)
+ return Math.round(coercedValue / stepDistance) * stepDistance
+}
+
+@Composable
+private fun AccessibilityParams.createSemantics(
+ value: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ onValueChanged: (Float) -> Unit,
+ isEnabled: Boolean,
+ stepDistance: Float,
+): SemanticsPropertyReceiver.() -> Unit {
+ val semanticsContentDescription =
+ disabledMessage
+ ?.takeIf { !isEnabled }
+ ?.let { message ->
+ stringResource(R.string.volume_slider_disabled_message_template, label, message)
+ } ?: label
+ return {
+ contentDescription = semanticsContentDescription
+ if (isEnabled) {
+ currentStateDescription?.let { stateDescription = it }
+ progressBarRangeInfo = ProgressBarRangeInfo(value, valueRange)
+ } else {
+ disabled()
+ }
+ setProgress { targetValue ->
+ val targetDirection =
+ when {
+ targetValue > value -> 1
+ targetValue < value -> -1
+ else -> 0
+ }
+
+ val newValue =
+ (value + targetDirection * stepDistance).coerceIn(
+ valueRange.start,
+ valueRange.endInclusive,
+ )
+ onValueChanged(newValue)
+ true
+ }
+ }
+}
+
+@Composable
+private fun Haptics.createViewModel(
+ value: Float,
+ valueRange: ClosedFloatingPointRange<Float>,
+ interactionSource: MutableInteractionSource,
+): SliderHapticsViewModel? {
+ return when (this) {
+ is Haptics.Disabled -> null
+ is Haptics.Enabled -> {
+ hapticsViewModelFactory.let {
+ rememberViewModel(traceName = "SliderHapticsViewModel") {
+ it.create(
+ interactionSource,
+ valueRange,
+ orientation,
+ VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(
+ valueRange,
+ hapticFilter,
+ ),
+ VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
+ )
+ }
+ .also { hapticsViewModel ->
+ var lastDiscreteStep by remember { mutableFloatStateOf(value) }
+ LaunchedEffect(value) {
+ snapshotFlow { value }
+ .map { round(it) }
+ .filter { it != lastDiscreteStep }
+ .distinctUntilChanged()
+ .collect { discreteStep ->
+ lastDiscreteStep = discreteStep
+ hapticsViewModel.onValueChange(discreteStep)
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+data class AccessibilityParams(
+ val label: String,
+ val currentStateDescription: String?,
+ val disabledMessage: String?,
+)
+
+sealed interface Haptics {
+ data object Disabled : Haptics
+
+ data class Enabled(
+ val hapticsViewModelFactory: SliderHapticsViewModel.Factory,
+ val hapticFilter: SliderHapticFeedbackFilter,
+ val orientation: Orientation,
+ ) : Haptics
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java
index 3d0a8f6cd236..ebbe023d0d24 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateLegacyTest.java
@@ -878,4 +878,18 @@ public class InternetDialogDelegateLegacyTest extends SysuiTestCase {
mMobileDataLayout.setVisibility(mobileDataVisible ? View.VISIBLE : View.GONE);
mConnectedWifi.setVisibility(connectedWifiVisible ? View.VISIBLE : View.GONE);
}
+
+ @Test
+ public void updateDialog_wifiIsDisabled_turnOffProgressBar() {
+ when(mInternetDetailsContentController.isWifiEnabled()).thenReturn(false);
+ mInternetDialogDelegateLegacy.mIsProgressBarVisible = true;
+
+ mInternetDialogDelegateLegacy.updateDialog(false);
+
+ mBgExecutor.runAllReady();
+ mInternetDialogDelegateLegacy.mDataInternetContent.observe(
+ mInternetDialogDelegateLegacy.mLifecycleOwner, i -> {
+ assertThat(mInternetDialogDelegateLegacy.mIsProgressBarVisible).isFalse();
+ });
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
index c5b19ab5862c..0b2fea53d811 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogControllerTest.kt
@@ -31,13 +31,13 @@ import android.testing.TestableLooper
import android.view.View
import com.android.systemui.SysuiTestCase
+import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.runner.RunWith
import org.junit.Test
import org.mockito.Answers
-import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.Mockito
@@ -66,11 +66,14 @@ class ChannelEditorDialogControllerTest : SysuiTestCase() {
@Mock
private lateinit var dialog: ChannelEditorDialog
+ private val shadeDialogContextInteractor = FakeShadeDialogContextInteractor(mContext)
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
`when`(dialogBuilder.build()).thenReturn(dialog)
- controller = ChannelEditorDialogController(mContext, mockNoMan, dialogBuilder)
+ controller =
+ ChannelEditorDialogController(shadeDialogContextInteractor, mockNoMan, dialogBuilder)
channel1 = NotificationChannel(TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_DEFAULT)
channel2 = NotificationChannel(TEST_CHANNEL2, TEST_CHANNEL_NAME2, IMPORTANCE_NONE)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
index e4806e62a9e5..e4806e62a9e5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index 9e914ad0a660..9e914ad0a660 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index 804e7d635107..804e7d635107 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
index 4714969af508..c7ea6db926d1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeQSTile.kt
@@ -22,7 +22,7 @@ import com.android.systemui.plugins.qs.QSTile
class FakeQSTile(var user: Int, var available: Boolean = true) : QSTile {
private var tileSpec: String? = null
- var destroyed = false
+ private var destroyed = false
var hasDetailsViewModel: Boolean = true
private var state = QSTile.State()
val callbacks = mutableListOf<QSTile.Callback>()
@@ -64,6 +64,10 @@ class FakeQSTile(var user: Int, var available: Boolean = true) : QSTile {
user = currentUser
}
+ override fun getCurrentTileUser(): Int {
+ return user
+ }
+
override fun getMetricsCategory(): Int {
return 0
}
@@ -76,6 +80,10 @@ class FakeQSTile(var user: Int, var available: Boolean = true) : QSTile {
destroyed = true
}
+ override fun isDestroyed(): Boolean {
+ return destroyed
+ }
+
override fun getTileLabel(): CharSequence {
return ""
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
index 4978558ff8a2..f038fdd3a1cd 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
@@ -26,7 +26,7 @@ import com.android.systemui.qs.tiles.impl.custom.packageManagerAdapterFacade
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.fakeSystemClock
-val Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by
+var Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by
Kosmos.Fixture {
TileLifecycleManager.Factory { intent, userHandle ->
TileLifecycleManager(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLoggerKosmos.kt
index 7d52f5d8aa34..c1531835b136 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLoggerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLoggerKosmos.kt
@@ -17,7 +17,14 @@
package com.android.systemui.qs.pipeline.shared.logging
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.log.logcatLogBuffer
/** mock */
-var Kosmos.qsLogger: QSPipelineLogger by Kosmos.Fixture { mock<QSPipelineLogger>() }
+var Kosmos.qsLogger: QSPipelineLogger by
+ Kosmos.Fixture {
+ QSPipelineLogger(
+ logcatLogBuffer(QSPipelineLogger.TILE_LIST_TAG),
+ logcatLogBuffer(QSPipelineLogger.AUTO_ADD_TAG),
+ logcatLogBuffer(QSPipelineLogger.RESTORE_TAG),
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
index bc1c60c33d71..c0584903db2d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/base/interactor/FakeQSTileUserActionInteractor.kt
@@ -33,7 +33,4 @@ class FakeQSTileUserActionInteractor<T> : QSTileUserActionInteractor<T> {
override suspend fun handleInput(input: QSTileInput<T>) {
mutex.withLock { mutableInputs.add(input) }
}
-
- override var detailsViewModel: TileDetailsViewModel? =
- FakeTileDetailsViewModel("FakeQSTileUserActionInteractor")
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt
index 6787b8ebb37f..c223be44a70c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/di/NewQSTileFactoryKosmos.kt
@@ -19,6 +19,7 @@ package com.android.systemui.qs.tiles.di
import android.os.UserHandle
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.instanceIdSequenceFake
+import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.shared.model.TileCategory
import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
@@ -56,7 +57,11 @@ val Kosmos.customTileViewModelFactory: QSTileViewModelFactory.Component by
override val config: QSTileConfig = config
override val isAvailable: StateFlow<Boolean> = MutableStateFlow(true)
- override fun onUserChanged(user: UserHandle) {}
+ override var currentTileUser = currentTilesInteractor.userId.value
+
+ override fun onUserChanged(user: UserHandle) {
+ currentTileUser = user.identifier
+ }
override fun forceUpdate() {}
@@ -68,7 +73,7 @@ val Kosmos.customTileViewModelFactory: QSTileViewModelFactory.Component by
}
}
-val Kosmos.newQSTileFactory by
+var Kosmos.newQSTileFactory by
Kosmos.Fixture {
NewQSTileFactory(
qSTileConfigProvider,
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
index 3ebef02284d6..b6167665c985 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
@@ -23,13 +23,10 @@ import static org.junit.Assume.assumeTrue;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.os.Bundle;
import android.platform.test.annotations.RavenwoodTestRunnerInitializing;
import android.platform.test.annotations.internal.InnerRunner;
import android.util.Log;
-import androidx.test.platform.app.InstrumentationRegistry;
-
import com.android.ravenwood.common.RavenwoodCommonUtils;
import org.junit.rules.TestRule;
@@ -285,11 +282,6 @@ public final class RavenwoodAwareTestRunner extends RavenwoodAwareTestRunnerBase
private boolean onBefore(Description description, Scope scope, Order order) {
Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order);
- if (scope == Scope.Instance && order == Order.Outer) {
- // Start of a test method.
- mState.enterTestMethod(description);
- }
-
final var classDescription = getDescription();
// Class-level annotations are checked by the runner already, so we only check
@@ -299,6 +291,12 @@ public final class RavenwoodAwareTestRunner extends RavenwoodAwareTestRunnerBase
return false;
}
}
+
+ if (scope == Scope.Instance && order == Order.Outer) {
+ // Start of a test method.
+ mState.enterTestMethod(description);
+ }
+
return true;
}
@@ -314,8 +312,7 @@ public final class RavenwoodAwareTestRunner extends RavenwoodAwareTestRunnerBase
if (scope == Scope.Instance && order == Order.Outer) {
// End of a test method.
- mState.exitTestMethod();
-
+ mState.exitTestMethod(description);
}
// If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error.
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
index 70bc52bdaa12..705186edba00 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
@@ -81,12 +81,15 @@ public final class RavenwoodRunnerState {
RavenwoodRuntimeEnvironmentController.exitTestClass();
}
+ /** Called when a test method is about to start */
public void enterTestMethod(Description description) {
mMethodDescription = description;
- RavenwoodRuntimeEnvironmentController.initForMethod();
+ RavenwoodRuntimeEnvironmentController.enterTestMethod(description);
}
- public void exitTestMethod() {
+ /** Called when a test method finishes */
+ public void exitTestMethod(Description description) {
+ RavenwoodRuntimeEnvironmentController.exitTestMethod(description);
mMethodDescription = null;
}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index f205d238c693..d935626c34df 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -51,6 +51,7 @@ import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.HandlerThread;
import android.os.Looper;
+import android.os.Message;
import android.os.Process_ravenwood;
import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
@@ -74,6 +75,7 @@ import com.android.ravenwood.common.SneakyThrow;
import com.android.server.LocalServices;
import com.android.server.compat.PlatformCompat;
+import org.junit.AssumptionViolatedException;
import org.junit.internal.management.ManagementFactory;
import org.junit.runner.Description;
@@ -81,6 +83,7 @@ import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -93,6 +96,7 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
+import java.util.stream.Collectors;
/**
* Responsible for initializing and the environment.
@@ -107,32 +111,60 @@ public class RavenwoodRuntimeEnvironmentController {
@SuppressWarnings("UnusedVariable")
private static final PrintStream sStdErr = System.err;
- private static final String MAIN_THREAD_NAME = "RavenwoodMain";
+ private static final String MAIN_THREAD_NAME = "Ravenwood:Main";
+ private static final String TESTS_THREAD_NAME = "Ravenwood:Test";
+
private static final String LIBRAVENWOOD_INITIALIZER_NAME = "ravenwood_initializer";
private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
private static final String ANDROID_LOG_TAGS = "ANDROID_LOG_TAGS";
private static final String RAVENWOOD_ANDROID_LOG_TAGS = "RAVENWOOD_" + ANDROID_LOG_TAGS;
+ static volatile Thread sTestThread;
+ static volatile Thread sMainThread;
+
/**
* When enabled, attempt to dump all thread stacks just before we hit the
* overall Tradefed timeout, to aid in debugging deadlocks.
+ *
+ * Note, this timeout will _not_ stop the test, as there isn't really a clean way to do it.
+ * It'll merely print stacktraces.
*/
private static final boolean ENABLE_TIMEOUT_STACKS =
- "1".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));
+ !"0".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));
+
+ private static final boolean TOLERATE_LOOPER_ASSERTS =
+ !"0".equals(System.getenv("RAVENWOOD_TOLERATE_LOOPER_ASSERTS"));
+
+ static final int DEFAULT_TIMEOUT_SECONDS = 10;
+ private static final int TIMEOUT_MILLIS = getTimeoutSeconds() * 1000;
+
+ static int getTimeoutSeconds() {
+ var e = System.getenv("RAVENWOOD_TIMEOUT_SECONDS");
+ if (e == null || e.isEmpty()) {
+ return DEFAULT_TIMEOUT_SECONDS;
+ }
+ return Integer.parseInt(e);
+ }
- private static final int TIMEOUT_MILLIS = 9_000;
private static final ScheduledExecutorService sTimeoutExecutor =
- Executors.newScheduledThreadPool(1);
+ Executors.newScheduledThreadPool(1, (Runnable r) -> {
+ Thread t = Executors.defaultThreadFactory().newThread(r);
+ t.setName("Ravenwood:TimeoutMonitor");
+ t.setDaemon(true);
+ return t;
+ });
- private static ScheduledFuture<?> sPendingTimeout;
+ private static volatile ScheduledFuture<?> sPendingTimeout;
/**
* When enabled, attempt to detect uncaught exceptions from background threads.
*/
private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION =
- "1".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION"));
+ !"0".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION"));
+
+ private static final boolean DIE_ON_UNCAUGHT_EXCEPTION = true;
/**
* When set, an unhandled exception was discovered (typically on a background thread), and we
@@ -141,12 +173,6 @@ public class RavenwoodRuntimeEnvironmentController {
private static final AtomicReference<Throwable> sPendingUncaughtException =
new AtomicReference<>();
- private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler =
- (thread, throwable) -> {
- // Remember the first exception we discover
- sPendingUncaughtException.compareAndSet(null, throwable);
- };
-
// TODO: expose packCallingIdentity function in libbinder and use it directly
// See: packCallingIdentity in frameworks/native/libs/binder/IPCThreadState.cpp
private static long packBinderIdentityToken(
@@ -187,6 +213,8 @@ public class RavenwoodRuntimeEnvironmentController {
* Initialize the global environment.
*/
public static void globalInitOnce() {
+ sTestThread = Thread.currentThread();
+ Thread.currentThread().setName(TESTS_THREAD_NAME);
synchronized (sInitializationLock) {
if (!sInitialized) {
// globalInitOnce() is called from class initializer, which cause
@@ -194,6 +222,7 @@ public class RavenwoodRuntimeEnvironmentController {
sInitialized = true;
// This is the first call.
+ final long start = System.currentTimeMillis();
try {
globalInitInner();
} catch (Throwable th) {
@@ -202,6 +231,9 @@ public class RavenwoodRuntimeEnvironmentController {
sExceptionFromGlobalInit = th;
SneakyThrow.sneakyThrow(th);
}
+ final long end = System.currentTimeMillis();
+ // TODO Show user/system time too
+ Log.e(TAG, "globalInit() took " + (end - start) + "ms");
} else {
// Subsequent calls. If the first call threw, just throw the same error, to prevent
// the test from running.
@@ -220,7 +252,8 @@ public class RavenwoodRuntimeEnvironmentController {
RavenwoodCommonUtils.log(TAG, "globalInitInner()");
if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
- Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler);
+ Thread.setDefaultUncaughtExceptionHandler(
+ RavenwoodRuntimeEnvironmentController::reportUncaughtExceptions);
}
// Some process-wide initialization:
@@ -304,6 +337,7 @@ public class RavenwoodRuntimeEnvironmentController {
ActivityManager.init$ravenwood(SYSTEM.getIdentifier());
final var main = new HandlerThread(MAIN_THREAD_NAME);
+ sMainThread = main;
main.start();
Looper.setMainLooperForTest(main.getLooper());
@@ -350,9 +384,20 @@ public class RavenwoodRuntimeEnvironmentController {
var systemServerContext =
new RavenwoodContext(ANDROID_PACKAGE_NAME, main, systemResourcesLoader);
- sInstrumentation = new Instrumentation();
- sInstrumentation.basicInit(instContext, targetContext, null);
- InstrumentationRegistry.registerInstance(sInstrumentation, Bundle.EMPTY);
+ var instArgs = Bundle.EMPTY;
+ RavenwoodUtils.runOnMainThreadSync(() -> {
+ try {
+ // TODO We should get the instrumentation class name from the build file or
+ // somewhere.
+ var InstClass = Class.forName("android.app.Instrumentation");
+ sInstrumentation = (Instrumentation) InstClass.getConstructor().newInstance();
+ sInstrumentation.basicInit(instContext, targetContext, null);
+ sInstrumentation.onCreate(instArgs);
+ } catch (Exception e) {
+ SneakyThrow.sneakyThrow(e);
+ }
+ });
+ InstrumentationRegistry.registerInstance(sInstrumentation, instArgs);
RavenwoodSystemServer.init(systemServerContext);
@@ -399,22 +444,46 @@ public class RavenwoodRuntimeEnvironmentController {
SystemProperties.clearChangeCallbacksForTest();
- if (ENABLE_TIMEOUT_STACKS) {
- sPendingTimeout = sTimeoutExecutor.schedule(
- RavenwoodRuntimeEnvironmentController::dumpStacks,
- TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
- }
- if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
- maybeThrowPendingUncaughtException(false);
- }
+ maybeThrowPendingUncaughtException();
}
/**
- * Partially reset and initialize before each test method invocation
+ * Called when a test method is about to be started.
*/
- public static void initForMethod() {
+ public static void enterTestMethod(Description description) {
// TODO(b/375272444): this is a hacky workaround to ensure binder identity
Binder.restoreCallingIdentity(sCallingIdentity);
+
+ scheduleTimeout();
+ }
+
+ /**
+ * Called when a test method finished.
+ */
+ public static void exitTestMethod(Description description) {
+ cancelTimeout();
+ maybeThrowPendingUncaughtException();
+ }
+
+ private static void scheduleTimeout() {
+ if (!ENABLE_TIMEOUT_STACKS) {
+ return;
+ }
+ cancelTimeout();
+
+ sPendingTimeout = sTimeoutExecutor.schedule(
+ RavenwoodRuntimeEnvironmentController::onTestTimedOut,
+ TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ }
+
+ private static void cancelTimeout() {
+ if (!ENABLE_TIMEOUT_STACKS) {
+ return;
+ }
+ var pt = sPendingTimeout;
+ if (pt != null) {
+ pt.cancel(false);
+ }
}
private static void initializeCompatIds() {
@@ -473,15 +542,36 @@ public class RavenwoodRuntimeEnvironmentController {
}
/**
+ * Return if an exception is benign and okay to continue running the main looper even
+ * if we detect it.
+ */
+ private static boolean isThrowableBenign(Throwable th) {
+ return th instanceof AssertionError || th instanceof AssumptionViolatedException;
+ }
+
+ static void dispatchMessage(Message msg) {
+ try {
+ msg.getTarget().dispatchMessage(msg);
+ } catch (Throwable th) {
+ var desc = String.format("Detected %s on looper thread %s", th.getClass().getName(),
+ Thread.currentThread());
+ sStdErr.println(desc);
+ if (TOLERATE_LOOPER_ASSERTS && isThrowableBenign(th)) {
+ sStdErr.printf("*** Continuing the test because it's %s ***\n",
+ th.getClass().getSimpleName());
+ var e = new Exception(desc, th);
+ sPendingUncaughtException.compareAndSet(null, e);
+ return;
+ }
+ throw th;
+ }
+ }
+
+ /**
* A callback when a test class finishes its execution, mostly only for debugging.
*/
public static void exitTestClass() {
- if (ENABLE_TIMEOUT_STACKS) {
- sPendingTimeout.cancel(false);
- }
- if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
- maybeThrowPendingUncaughtException(true);
- }
+ maybeThrowPendingUncaughtException();
}
public static void logTestRunner(String label, Description description) {
@@ -491,35 +581,70 @@ public class RavenwoodRuntimeEnvironmentController {
+ "(" + description.getTestClass().getName() + ")");
}
- private static void dumpStacks() {
- final PrintStream out = System.err;
- out.println("-----BEGIN ALL THREAD STACKS-----");
- final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
- for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) {
- out.println();
- Thread t = stack.getKey();
- out.println(t.toString() + " ID=" + t.getId());
- for (StackTraceElement e : stack.getValue()) {
- out.println("\tat " + e);
- }
+ private static void maybeThrowPendingUncaughtException() {
+ final Throwable pending = sPendingUncaughtException.getAndSet(null);
+ if (pending != null) {
+ throw new IllegalStateException("Found an uncaught exception", pending);
}
- out.println("-----END ALL THREAD STACKS-----");
}
/**
- * If there's a pending uncaught exception, consume and throw it now. Typically used to
- * report an exception on a background thread as a failure for the currently running test.
+ * Prints the stack trace from all threads.
*/
- private static void maybeThrowPendingUncaughtException(boolean duringReset) {
- final Throwable pending = sPendingUncaughtException.getAndSet(null);
- if (pending != null) {
- if (duringReset) {
- throw new IllegalStateException(
- "Found an uncaught exception during this test", pending);
- } else {
- throw new IllegalStateException(
- "Found an uncaught exception before this test started", pending);
+ private static void onTestTimedOut() {
+ sStdErr.println("********* SLOW TEST DETECTED ********");
+ dumpStacks(null, null);
+ }
+
+ private static final Object sDumpStackLock = new Object();
+
+ /**
+ * Prints the stack trace from all threads.
+ */
+ private static void dumpStacks(
+ @Nullable Thread exceptionThread, @Nullable Throwable throwable) {
+ cancelTimeout();
+ synchronized (sDumpStackLock) {
+ final PrintStream out = sStdErr;
+ out.println("-----BEGIN ALL THREAD STACKS-----");
+
+ var stacks = Thread.getAllStackTraces();
+ var threads = stacks.keySet().stream().sorted(
+ Comparator.comparingLong(Thread::getId)).collect(Collectors.toList());
+
+ // Put the test and the main thread at the top.
+ var testThread = sTestThread;
+ var mainThread = sMainThread;
+ if (mainThread != null) {
+ threads.remove(mainThread);
+ threads.add(0, mainThread);
+ }
+ if (testThread != null) {
+ threads.remove(testThread);
+ threads.add(0, testThread);
+ }
+ // Put the exception thread at the top.
+ // Also inject the stacktrace from the exception.
+ if (exceptionThread != null) {
+ threads.remove(exceptionThread);
+ threads.add(0, exceptionThread);
+ stacks.put(exceptionThread, throwable.getStackTrace());
+ }
+ for (var th : threads) {
+ out.println();
+
+ out.print("Thread");
+ if (th == exceptionThread) {
+ out.print(" [** EXCEPTION THREAD **]");
+ }
+ out.print(": " + th.getName() + " / " + th);
+ out.println();
+
+ for (StackTraceElement e : stacks.get(th)) {
+ out.println("\tat " + e);
+ }
}
+ out.println("-----END ALL THREAD STACKS-----");
}
}
@@ -545,13 +670,17 @@ public class RavenwoodRuntimeEnvironmentController {
() -> Class.forName("org.mockito.Matchers"));
}
- // TODO: use the real UiAutomation class instead of a mock
- private static UiAutomation createMockUiAutomation() {
- sAdoptedPermissions = Collections.emptySet();
- var mock = mock(UiAutomation.class, inv -> {
+ static <T> T makeDefaultThrowMock(Class<T> clazz) {
+ return mock(clazz, inv -> {
HostTestUtils.onThrowMethodCalled();
return null;
});
+ }
+
+ // TODO: use the real UiAutomation class instead of a mock
+ private static UiAutomation createMockUiAutomation() {
+ sAdoptedPermissions = Collections.emptySet();
+ var mock = makeDefaultThrowMock(UiAutomation.class);
doAnswer(inv -> {
sAdoptedPermissions = UiAutomation.ALL_PERMISSIONS;
return null;
@@ -586,6 +715,23 @@ public class RavenwoodRuntimeEnvironmentController {
}
}
+ private static void reportUncaughtExceptions(Thread th, Throwable e) {
+ sStdErr.printf("Uncaught exception detected: %s: %s\n",
+ th, RavenwoodCommonUtils.getStackTraceString(e));
+
+ doBugreport(th, e, DIE_ON_UNCAUGHT_EXCEPTION);
+ }
+
+ private static void doBugreport(
+ @Nullable Thread exceptionThread, @Nullable Throwable throwable,
+ boolean killSelf) {
+ // TODO: Print more information
+ dumpStacks(exceptionThread, throwable);
+ if (killSelf) {
+ System.exit(13);
+ }
+ }
+
private static void dumpJavaProperties() {
Log.v(TAG, "JVM properties:");
dumpMap(System.getProperties());
@@ -601,7 +747,6 @@ public class RavenwoodRuntimeEnvironmentController {
Log.v(TAG, " " + key + "=" + map.get(key));
}
}
-
private static void dumpOtherInfo() {
Log.v(TAG, "Other key information:");
var jloc = Locale.getDefault();
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
index 70c161c1f19a..819d93a9c336 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
@@ -26,6 +26,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
@@ -45,6 +46,9 @@ public class RavenwoodSystemProperties {
/** The default values. */
static final Map<String, String> sDefaultValues = new HashMap<>();
+ static final Set<String> sReadableKeys = new HashSet<>();
+ static final Set<String> sWritableKeys = new HashSet<>();
+
private static final String[] PARTITIONS = {
"bootimage",
"odm",
@@ -88,9 +92,24 @@ public class RavenwoodSystemProperties {
ravenwoodProps.forEach((key, origValue) -> {
final String value;
- // If a value starts with "$$$", then this is a reference to the device-side value.
if (origValue.startsWith("$$$")) {
+ // If a value starts with "$$$", then:
+ // - If it's "$$$r", the key is allowed to read.
+ // - If it's "$$$w", the key is allowed to write.
+ // - Otherwise, it's a reference to the device-side value.
+ // In case of $$$r and $$$w, if the key ends with a '.', then it'll be treaded
+ // as a prefix match.
var deviceKey = origValue.substring(3);
+ if ("r".equals(deviceKey)) {
+ sReadableKeys.add(key);
+ Log.v(TAG, key + " (readable)");
+ return;
+ } else if ("w".equals(deviceKey)) {
+ sWritableKeys.add(key);
+ Log.v(TAG, key + " (writable)");
+ return;
+ }
+
var deviceValue = deviceProps.get(deviceKey);
if (deviceValue == null) {
throw new RuntimeException("Failed to initialize system properties. Key '"
@@ -131,50 +150,38 @@ public class RavenwoodSystemProperties {
sDefaultValues.forEach(RavenwoodRuntimeNative::setSystemProperty);
}
- private static boolean isKeyReadable(String key) {
- // All writable keys are also readable
- if (isKeyWritable(key)) return true;
+ private static boolean checkAllowedInner(String key, Set<String> allowed) {
+ if (allowed.contains(key)) {
+ return true;
+ }
- final String root = getKeyRoot(key);
+ // Also search for a prefix match.
+ for (var k : allowed) {
+ if (k.endsWith(".") && key.startsWith(k)) {
+ return true;
+ }
+ }
+ return false;
+ }
- // This set is carefully curated to help identify situations where a test may
- // accidentally depend on a default value of an obscure property whose owner hasn't
- // decided how Ravenwood should behave.
- if (root.startsWith("boot.")) return true;
- if (root.startsWith("build.")) return true;
- if (root.startsWith("product.")) return true;
- if (root.startsWith("soc.")) return true;
- if (root.startsWith("system.")) return true;
+ private static boolean checkAllowed(String key, Set<String> allowed) {
+ return checkAllowedInner(key, allowed) || checkAllowedInner(getKeyRoot(key), allowed);
+ }
+ private static boolean isKeyReadable(String key) {
// All core values should be readable
- if (sDefaultValues.containsKey(key)) return true;
-
- // Hardcoded allowlist
- return switch (key) {
- case "gsm.version.baseband",
- "no.such.thing",
- "qemu.sf.lcd_density",
- "ro.bootloader",
- "ro.hardware",
- "ro.hw_timeout_multiplier",
- "ro.odm.build.media_performance_class",
- "ro.sf.lcd_density",
- "ro.treble.enabled",
- "ro.vndk.version",
- "ro.icu.data.path" -> true;
- default -> false;
- };
+ if (sDefaultValues.containsKey(key)) {
+ return true;
+ }
+ if (checkAllowed(key, sReadableKeys)) {
+ return true;
+ }
+ // All writable keys are also readable
+ return isKeyWritable(key);
}
private static boolean isKeyWritable(String key) {
- final String root = getKeyRoot(key);
-
- if (root.startsWith("debug.")) return true;
-
- // For PropertyInvalidatedCache
- if (root.startsWith("cache_key.")) return true;
-
- return false;
+ return checkAllowed(key, sWritableKeys);
}
static boolean isKeyAccessible(String key, boolean write) {
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
index 19c1bffaebcd..3e2c4051b792 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
@@ -15,7 +15,20 @@
*/
package android.platform.test.ravenwood;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.ReflectedMethod.reflectMethod;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+
import com.android.ravenwood.common.RavenwoodCommonUtils;
+import com.android.ravenwood.common.SneakyThrow;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
/**
* Utilities for writing (bivalent) ravenwood tests.
@@ -47,4 +60,129 @@ public class RavenwoodUtils {
public static void loadJniLibrary(String libname) {
RavenwoodCommonUtils.loadJniLibrary(libname);
}
+
+ private class MainHandlerHolder {
+ static Handler sMainHandler = new Handler(Looper.getMainLooper());
+ }
+
+ /**
+ * Returns the main thread handler.
+ */
+ public static Handler getMainHandler() {
+ return MainHandlerHolder.sMainHandler;
+ }
+
+ /**
+ * Run a Callable on Handler and wait for it to complete.
+ */
+ @Nullable
+ public static <T> T runOnHandlerSync(@NonNull Handler h, @NonNull Callable<T> c) {
+ var result = new AtomicReference<T>();
+ var thrown = new AtomicReference<Throwable>();
+ var latch = new CountDownLatch(1);
+ h.post(() -> {
+ try {
+ result.set(c.call());
+ } catch (Throwable th) {
+ thrown.set(th);
+ }
+ latch.countDown();
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Interrupted while waiting on the Runnable", e);
+ }
+ var th = thrown.get();
+ if (th != null) {
+ SneakyThrow.sneakyThrow(th);
+ }
+ return result.get();
+ }
+
+
+ /**
+ * Run a Runnable on Handler and wait for it to complete.
+ */
+ @Nullable
+ public static void runOnHandlerSync(@NonNull Handler h, @NonNull Runnable r) {
+ runOnHandlerSync(h, () -> {
+ r.run();
+ return null;
+ });
+ }
+
+ /**
+ * Run a Callable on main thread and wait for it to complete.
+ */
+ @Nullable
+ public static <T> T runOnMainThreadSync(@NonNull Callable<T> c) {
+ return runOnHandlerSync(getMainHandler(), c);
+ }
+
+ /**
+ * Run a Runnable on main thread and wait for it to complete.
+ */
+ @Nullable
+ public static void runOnMainThreadSync(@NonNull Runnable r) {
+ runOnHandlerSync(getMainHandler(), r);
+ }
+
+ public static class MockitoHelper {
+ private MockitoHelper() {
+ }
+
+ /**
+ * Allow verifyZeroInteractions to work on ravenwood. It was replaced with a different
+ * method on. (Maybe we should do it in Ravenizer.)
+ */
+ public static void verifyZeroInteractions(Object... mocks) {
+ if (RavenwoodRule.isOnRavenwood()) {
+ // Mockito 4 or later
+ reflectMethod("org.mockito.Mockito", "verifyNoInteractions", Object[].class)
+ .callStatic(new Object[]{mocks});
+ } else {
+ // Mockito 2
+ reflectMethod("org.mockito.Mockito", "verifyZeroInteractions", Object[].class)
+ .callStatic(new Object[]{mocks});
+ }
+ }
+ }
+
+
+ /**
+ * Wrap the given {@link Supplier} to become memoized.
+ *
+ * The underlying {@link Supplier} will only be invoked once, and that result will be cached
+ * and returned for any future requests.
+ */
+ static <T> Supplier<T> memoize(ThrowingSupplier<T> supplier) {
+ return new Supplier<>() {
+ private T mInstance;
+
+ @Override
+ public T get() {
+ synchronized (this) {
+ if (mInstance == null) {
+ mInstance = create();
+ }
+ return mInstance;
+ }
+ }
+
+ private T create() {
+ try {
+ return supplier.get();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ }
+
+ /** Used by {@link #memoize(ThrowingSupplier)} */
+ public interface ThrowingSupplier<T> {
+ /** */
+ T get() throws Exception;
+ }
}
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
index a967a3fff0d7..893b354d4645 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
@@ -26,10 +26,12 @@ import java.io.FileInputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
+import java.util.Objects;
import java.util.function.Supplier;
public class RavenwoodCommonUtils {
@@ -329,4 +331,70 @@ public class RavenwoodCommonUtils {
public static <T> T withDefault(@Nullable T value, @Nullable T def) {
return value != null ? value : def;
}
+
+ /**
+ * Utility for calling a method with reflections. Used to call a method by name.
+ * Note, this intentionally does _not_ support non-public methods, as we generally
+ * shouldn't violate java visibility in ravenwood.
+ *
+ * @param <TTHIS> class owning the method.
+ */
+ public static class ReflectedMethod<TTHIS> {
+ private final Class<TTHIS> mThisClass;
+ private final Method mMethod;
+
+ private ReflectedMethod(Class<TTHIS> thisClass, Method method) {
+ mThisClass = thisClass;
+ mMethod = method;
+ }
+
+ /** Factory method. */
+ @SuppressWarnings("unchecked")
+ public static <TTHIS> ReflectedMethod<TTHIS> reflectMethod(
+ @NonNull Class<TTHIS> clazz, @NonNull String methodName,
+ @NonNull Class<?>... argTypes) {
+ try {
+ return new ReflectedMethod(clazz, clazz.getMethod(methodName, argTypes));
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Factory method. */
+ @SuppressWarnings("unchecked")
+ public static <TTHIS> ReflectedMethod<TTHIS> reflectMethod(
+ @NonNull String className, @NonNull String methodName,
+ @NonNull Class<?>... argTypes) {
+ try {
+ return reflectMethod((Class<TTHIS>) Class.forName(className), methodName, argTypes);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Call the instance method */
+ @SuppressWarnings("unchecked")
+ public <RET> RET call(@NonNull TTHIS thisObject, @NonNull Object... args) {
+ try {
+ return (RET) mMethod.invoke(Objects.requireNonNull(thisObject), args);
+ } catch (InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Call the static method */
+ @SuppressWarnings("unchecked")
+ public <RET> RET callStatic(@NonNull Object... args) {
+ try {
+ return (RET) mMethod.invoke(null, args);
+ } catch (InvocationTargetException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ /** Handy method to create an array */
+ public static <T> T[] arr(@NonNull T... objects) {
+ return objects;
+ }
}
diff --git a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
index 7ab9cda378b7..855a4ff21671 100644
--- a/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/Log_ravenwood.java
@@ -21,7 +21,6 @@ import android.util.Log.Level;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.RuntimeInit;
import com.android.ravenwood.RavenwoodRuntimeNative;
-import com.android.ravenwood.common.RavenwoodCommonUtils;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
@@ -164,7 +163,7 @@ public class Log_ravenwood {
* Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so
* that we don't end up in a recursive loop.
*/
- private static PrintStream getRealOut() {
+ public static PrintStream getRealOut() {
if (RuntimeInit.sOut$ravenwood != null) {
return RuntimeInit.sOut$ravenwood;
} else {
diff --git a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
index eaadac6a8b92..50cfd3bbe863 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
@@ -57,4 +57,12 @@ public class VMRuntime {
public int getTargetSdkVersion() {
return RavenwoodRuntimeState.sTargetSdkLevel;
}
+
+ /** Ignored on ravenwood. */
+ public void registerNativeAllocation(long bytes) {
+ }
+
+ /** Ignored on ravenwood. */
+ public void registerNativeFree(long bytes) {
+ }
}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
index cf1a5138cbc6..985e00e8641d 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
@@ -97,6 +97,9 @@ public class NativeAllocationRegistry {
if (referent == null) {
throw new IllegalArgumentException("referent is null");
}
+ if (mFreeFunction == 0) {
+ return () -> {}; // do nothing
+ }
if (nativePtr == 0) {
throw new IllegalArgumentException("nativePtr is null");
}
diff --git a/ravenwood/scripts/add-annotations.sh b/ravenwood/scripts/add-annotations.sh
index 3e86037d7c7b..8c394f51d8c4 100755
--- a/ravenwood/scripts/add-annotations.sh
+++ b/ravenwood/scripts/add-annotations.sh
@@ -35,7 +35,7 @@ set -e
# We add this line to each methods found.
# Note, if we used a single @, that'd be handled as an at file. Use
# the double-at instead.
-annotation="@@android.platform.test.annotations.DisabledOnRavenwood"
+annotation="@@android.platform.test.annotations.DisabledOnRavenwood(reason = \"bulk-disabled by script\")"
while getopts "t:" opt; do
case "$opt" in
t)
diff --git a/ravenwood/tests/coretest/Android.bp b/ravenwood/tests/coretest/Android.bp
index 9dd7cc683719..182a7cf3d3de 100644
--- a/ravenwood/tests/coretest/Android.bp
+++ b/ravenwood/tests/coretest/Android.bp
@@ -33,3 +33,34 @@ android_ravenwood_test {
},
auto_gen_config: true,
}
+
+// Same as RavenwoodCoreTest, but it excludes tests using platform-parametric-runner-lib,
+// because that modules has too many dependencies and slow to build incrementally.
+android_ravenwood_test {
+ name: "RavenwoodCoreTest-light",
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+
+ // This library should be removed by Ravenizer
+ "mockito-target-minus-junit4",
+ ],
+ libs: [
+ // We access internal private classes
+ "ravenwood-junit-impl",
+ ],
+ srcs: [
+ "test/**/*.java",
+ "test/**/*.kt",
+ ],
+
+ exclude_srcs: [
+ "test/com/android/ravenwoodtest/runnercallbacktests/*",
+ ],
+ ravenizer: {
+ strip_mockito: true,
+ },
+ auto_gen_config: true,
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java
new file mode 100644
index 000000000000..68387d76b675
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodMainThreadTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.ravenwoodtest.coretest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.ravenwood.RavenwoodUtils;
+
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+public class RavenwoodMainThreadTest {
+ private static final boolean RUN_UNSAFE_TESTS =
+ "1".equals(System.getenv("RAVENWOOD_RUN_UNSAFE_TESTS"));
+
+ @Test
+ public void testRunOnMainThread() {
+ AtomicReference<Thread> thr = new AtomicReference<>();
+ RavenwoodUtils.runOnMainThreadSync(() -> {
+ thr.set(Thread.currentThread());
+ });
+ var th = thr.get();
+ assertThat(th).isNotNull();
+ assertThat(th).isNotEqualTo(Thread.currentThread());
+ }
+
+ /**
+ * Sleep a long time on the main thread. This test would then "pass", but Ravenwood
+ * should show the stack traces.
+ *
+ * This is "unsafe" because this test is slow.
+ */
+ @Test
+ public void testUnsafeMainThreadHang() {
+ assumeTrue(RUN_UNSAFE_TESTS);
+
+ // The test should time out.
+ RavenwoodUtils.runOnMainThreadSync(() -> {
+ try {
+ Thread.sleep(30_000);
+ } catch (InterruptedException e) {
+ fail("Interrupted");
+ }
+ });
+ }
+
+ /**
+ * AssertionError on the main thread would be swallowed and reported "normally".
+ * (Other kinds of exceptions would be caught by the unhandled exception handler, and kills
+ * the process)
+ *
+ * This is "unsafe" only because this feature can be disabled via the env var.
+ */
+ @Test
+ public void testUnsafeAssertFailureOnMainThread() {
+ assumeTrue(RUN_UNSAFE_TESTS);
+
+ assertThrows(AssertionError.class, () -> {
+ RavenwoodUtils.runOnMainThreadSync(() -> {
+ fail();
+ });
+ });
+ }
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java
new file mode 100644
index 000000000000..421fb50e0c9a
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodReflectorTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.ravenwoodtest.coretest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.ravenwood.common.RavenwoodCommonUtils.ReflectedMethod;
+
+import org.junit.Test;
+
+/**
+ * Tests for {@link ReflectedMethod}.
+ */
+public class RavenwoodReflectorTest {
+ /** test target */
+ public class Target {
+ private final int mVar;
+
+ /** test target */
+ public Target(int var) {
+ mVar = var;
+ }
+
+ /** test target */
+ public int foo(int x) {
+ return x + mVar;
+ }
+
+ /** test target */
+ public static int bar(int x) {
+ return x + 1;
+ }
+ }
+
+ /** Test for a non-static method call */
+ @Test
+ public void testNonStatic() {
+ var obj = new Target(5);
+
+ var m = ReflectedMethod.reflectMethod(Target.class, "foo", int.class);
+ assertThat((int) m.call(obj, 2)).isEqualTo(7);
+ }
+
+ /** Test for a static method call */
+ @Test
+ public void testStatic() {
+ var m = ReflectedMethod.reflectMethod(Target.class, "bar", int.class);
+ assertThat((int) m.callStatic(1)).isEqualTo(2);
+ }
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java
new file mode 100644
index 000000000000..454f5a9576d9
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodSystemPropertiesTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.coretest;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import android.os.SystemProperties;
+
+import org.junit.Test;
+
+public class RavenwoodSystemPropertiesTest {
+ @Test
+ public void testRead() {
+ assertThat(SystemProperties.get("ro.board.first_api_level")).isEqualTo("1");
+ }
+
+ @Test
+ public void testWrite() {
+ SystemProperties.set("debug.xxx", "5");
+ assertThat(SystemProperties.get("debug.xxx")).isEqualTo("5");
+ }
+
+ private static void assertException(String expectedMessage, Runnable r) {
+ try {
+ r.run();
+ fail("Excepted exception with message '" + expectedMessage + "' but wasn't thrown");
+ } catch (RuntimeException e) {
+ if (e.getMessage().contains(expectedMessage)) {
+ return;
+ }
+ fail("Excepted exception with message '" + expectedMessage + "' but was '"
+ + e.getMessage() + "'");
+ }
+ }
+
+
+ @Test
+ public void testReadDisallowed() {
+ assertException("Read access to system property 'nonexisitent' denied", () -> {
+ SystemProperties.get("nonexisitent");
+ });
+ }
+
+ @Test
+ public void testWriteDisallowed() {
+ assertException("failed to set system property \"ro.board.first_api_level\" ", () -> {
+ SystemProperties.set("ro.board.first_api_level", "2");
+ });
+ }
+}
diff --git a/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java b/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
index 30abaa2e7d38..b1a40f082656 100644
--- a/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
+++ b/ravenwood/tests/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
@@ -16,28 +16,27 @@
package com.android.ravenwoodtest;
import android.platform.test.annotations.IgnoreUnderRavenwood;
-import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
-import org.junit.Rule;
+import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class RavenwoodMinimumTest {
- @Rule
- public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
- .setProcessApp()
- .build();
-
@Test
public void testSimple() {
Assert.assertTrue(android.os.Process.isApplicationUid(android.os.Process.myUid()));
}
@Test
+ public void testAssumeNot() {
+ Assume.assumeFalse(android.os.Process.isApplicationUid(android.os.Process.myUid()));
+ }
+
+ @Test
@IgnoreUnderRavenwood
public void testIgnored() {
throw new RuntimeException("Shouldn't be executed under ravenwood");
diff --git a/ravenwood/texts/ravenwood-build.prop b/ravenwood/texts/ravenwood-build.prop
index 37c50f11f73f..512b459113da 100644
--- a/ravenwood/texts/ravenwood-build.prop
+++ b/ravenwood/texts/ravenwood-build.prop
@@ -8,9 +8,41 @@ ro.soc.manufacturer=Android
ro.soc.model=Ravenwood
ro.debuggable=1
-# For the graphics stack
-ro.hwui.max_texture_allocation_size=104857600
persist.sys.locale=en-US
+ro.product.locale=en-US
+
+ro.hwui.max_texture_allocation_size=104857600
+
+# Allowlist control:
+# This set is carefully curated to help identify situations where a test may
+# accidentally depend on a default value of an obscure property whose owner hasn't
+# decided how Ravenwood should behave.
+
+boot.=$$$r
+build.=$$$r
+product.=$$$r
+soc.=$$$r
+system.=$$$r
+wm.debug.=$$$r
+wm.extensions.=$$$r
+
+gsm.version.baseband=$$$r
+no.such.thing=$$$r
+qemu.sf.lcd_density=$$$r
+ro.bootloader=$$$r
+ro.hardware=$$$r
+ro.hw_timeout_multiplier=$$$r
+ro.odm.build.media_performance_class=$$$r
+ro.sf.lcd_density=$$$r
+ro.treble.enabled=$$$r
+ro.vndk.version=$$$r
+ro.icu.data.path=$$$r
+
+# Writable keys
+debug.=$$$w
+
+# For PropertyInvalidatedCache
+cache_key.=$$$w
# The ones starting with "ro.product" or "ro.build" will be copied to all "partitions" too.
# See RavenwoodSystemProperties.
diff --git a/ravenwood/texts/ravenwood-services-jarjar-rules.txt b/ravenwood/texts/ravenwood-services-jarjar-rules.txt
index 8fdd3408f74d..64a0e2548e2e 100644
--- a/ravenwood/texts/ravenwood-services-jarjar-rules.txt
+++ b/ravenwood/texts/ravenwood-services-jarjar-rules.txt
@@ -5,7 +5,7 @@ rule com.android.server.pm.pkg.AndroidPackageSplit @0
# Rename all other service internals so that tests can continue to statically
# link services code when owners aren't ready to support on Ravenwood
-rule com.android.server.** repackaged.@0
+rule com.android.server.** repackaged.services.@0
# TODO: support AIDL generated Parcelables via hoststubgen
-rule android.hardware.power.stats.** repackaged.@0
+rule android.hardware.power.stats.** repackaged.services.@0
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index bd34f33226a1..c182c2618fdf 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -149,7 +149,6 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
OperationStorage mOperationStorage;
List<PackageInfo> mPackages;
- PackageInfo mCurrentPackage;
boolean mUpdateSchedule;
CountDownLatch mLatch;
FullBackupJob mJob; // if a scheduled job needs to be finished afterwards
@@ -207,10 +206,9 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
for (String pkg : whichPackages) {
try {
PackageManager pm = backupManagerService.getPackageManager();
- PackageInfo info = pm.getPackageInfoAsUser(pkg,
+ PackageInfo packageInfo = pm.getPackageInfoAsUser(pkg,
PackageManager.GET_SIGNING_CERTIFICATES, mUserId);
- mCurrentPackage = info;
- if (!mBackupEligibilityRules.appIsEligibleForBackup(info.applicationInfo)) {
+ if (!mBackupEligibilityRules.appIsEligibleForBackup(packageInfo.applicationInfo)) {
// Cull any packages that have indicated that backups are not permitted,
// that run as system-domain uids but do not define their own backup agents,
// as well as any explicit mention of the 'special' shared-storage agent
@@ -220,13 +218,13 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
}
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_INELIGIBLE,
- mCurrentPackage,
+ packageInfo,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ /* extras= */ null);
BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
BackupManager.ERROR_BACKUP_NOT_ALLOWED);
continue;
- } else if (!mBackupEligibilityRules.appGetsFullBackup(info)) {
+ } else if (!mBackupEligibilityRules.appGetsFullBackup(packageInfo)) {
// Cull any packages that are found in the queue but now aren't supposed
// to get full-data backup operations.
if (DEBUG) {
@@ -235,13 +233,13 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
}
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_KEY_VALUE_PARTICIPANT,
- mCurrentPackage,
+ packageInfo,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ /* extras= */ null);
BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
BackupManager.ERROR_BACKUP_NOT_ALLOWED);
continue;
- } else if (mBackupEligibilityRules.appIsStopped(info.applicationInfo)) {
+ } else if (mBackupEligibilityRules.appIsStopped(packageInfo.applicationInfo)) {
// Cull any packages in the 'stopped' state: they've either just been
// installed or have explicitly been force-stopped by the user. In both
// cases we do not want to launch them for backup.
@@ -250,21 +248,21 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
}
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_STOPPED,
- mCurrentPackage,
+ packageInfo,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ /* extras= */ null);
BackupObserverUtils.sendBackupOnPackageResult(mBackupObserver, pkg,
BackupManager.ERROR_BACKUP_NOT_ALLOWED);
continue;
}
- mPackages.add(info);
+ mPackages.add(packageInfo);
} catch (NameNotFoundException e) {
Slog.i(TAG, "Requested package " + pkg + " not found; ignoring");
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_NOT_FOUND,
- mCurrentPackage,
+ /* pkg= */ null,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ /* extras= */ null);
}
}
@@ -352,10 +350,11 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
} else {
monitoringEvent = BackupManagerMonitor.LOG_EVENT_ID_DEVICE_NOT_PROVISIONED;
}
- mBackupManagerMonitorEventSender
- .monitorEvent(monitoringEvent, null,
- BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- null);
+ mBackupManagerMonitorEventSender.monitorEvent(
+ monitoringEvent,
+ /* pkg= */ null,
+ BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
+ /* extras= */ null);
mUpdateSchedule = false;
backupRunStatus = BackupManager.ERROR_BACKUP_NOT_ALLOWED;
return;
@@ -367,8 +366,9 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
backupRunStatus = BackupManager.ERROR_TRANSPORT_ABORTED;
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_PACKAGE_TRANSPORT_NOT_PRESENT,
- mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
- null);
+ /* pkg= */ null,
+ BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
+ /* extras= */ null);
return;
}
@@ -461,9 +461,10 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
}
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_ERROR_PREFLIGHT,
- mCurrentPackage,
+ currentPackage,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- mBackupManagerMonitorEventSender.putMonitoringExtra(null,
+ BackupManagerMonitorEventSender.putMonitoringExtra(
+ /* extras= */ null,
BackupManagerMonitor.EXTRA_LOG_PREFLIGHT_ERROR,
preflightResult));
backupPackageStatus = (int) preflightResult;
@@ -496,9 +497,9 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
+ ": " + totalRead + " of " + quota);
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_QUOTA_HIT_PREFLIGHT,
- mCurrentPackage,
+ currentPackage,
BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
- null);
+ /* extras= */ null);
mBackupRunner.sendQuotaExceeded(totalRead, quota);
}
}
@@ -645,9 +646,9 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
Slog.w(TAG, "Exception trying full transport backup", e);
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_EXCEPTION_FULL_BACKUP,
- mCurrentPackage,
+ /* pkg= */ null,
BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY,
- mBackupManagerMonitorEventSender.putMonitoringExtra(null,
+ BackupManagerMonitorEventSender.putMonitoringExtra(/* extras= */ null,
BackupManagerMonitor.EXTRA_LOG_EXCEPTION_FULL_BACKUP,
Log.getStackTraceString(e)));
@@ -966,9 +967,6 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
}
}
-
- // BackupRestoreTask interface: specifically, timeout detection
-
@Override
public void execute() { /* intentionally empty */ }
@@ -981,7 +979,9 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
mBackupManagerMonitorEventSender.monitorEvent(
BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_CANCEL,
- mCurrentPackage, BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT, null);
+ mTarget,
+ BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
+ /* extras= */ null);
mIsCancelled = true;
// Cancel tasks spun off by this task.
mUserBackupManagerService.handleCancel(mEphemeralToken, cancelAll);
diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
index c4519b1173eb..33668a6d5314 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorEventSender.java
@@ -71,6 +71,7 @@ public class BackupManagerMonitorEventSender {
mMonitor = monitor;
}
+ @Nullable
public IBackupManagerMonitor getMonitor() {
return mMonitor;
}
@@ -87,9 +88,9 @@ public class BackupManagerMonitorEventSender {
*/
public void monitorEvent(
int id,
- PackageInfo pkg,
+ @Nullable PackageInfo pkg,
int category,
- Bundle extras) {
+ @Nullable Bundle extras) {
try {
Bundle bundle = new Bundle();
bundle.putInt(BackupManagerMonitor.EXTRA_LOG_EVENT_ID, id);
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index b6fe0ad37078..e46bbe2871cd 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -160,6 +160,7 @@ import com.android.server.memory.ZramMaintenance;
import com.android.server.pm.Installer;
import com.android.server.pm.UserManagerInternal;
import com.android.server.storage.AppFuseBridge;
+import com.android.server.storage.ImmutableVolumeInfo;
import com.android.server.storage.StorageSessionController;
import com.android.server.storage.StorageSessionController.ExternalStorageServiceException;
import com.android.server.storage.WatchedVolumeInfo;
@@ -777,7 +778,7 @@ class StorageManagerService extends IStorageManager.Stub
break;
}
case H_VOLUME_UNMOUNT: {
- final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj;
+ final ImmutableVolumeInfo vol = (ImmutableVolumeInfo) msg.obj;
unmount(vol);
break;
}
@@ -898,8 +899,14 @@ class StorageManagerService extends IStorageManager.Stub
for (int i = 0; i < size; i++) {
final WatchedVolumeInfo vol = mVolumes.valueAt(i);
if (vol.getMountUserId() == userId) {
+ // Capture the volume before we set mount user id to null,
+ // so that StorageSessionController remove the session from
+ // the correct user (old mount user id)
+ final ImmutableVolumeInfo volToUnmount
+ = vol.getClonedImmutableVolumeInfo();
vol.setMountUserId(UserHandle.USER_NULL);
- mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget();
+ mHandler.obtainMessage(H_VOLUME_UNMOUNT, volToUnmount)
+ .sendToTarget();
}
}
}
@@ -1295,7 +1302,12 @@ class StorageManagerService extends IStorageManager.Stub
}
private void maybeRemountVolumes(int userId) {
- List<WatchedVolumeInfo> volumesToRemount = new ArrayList<>();
+ // We need to keep 2 lists
+ // 1. List of volumes before we set the mount user Id so that
+ // StorageSessionController is able to remove the session from the correct user (old one)
+ // 2. List of volumes to mount which should have the up to date info
+ List<ImmutableVolumeInfo> volumesToUnmount = new ArrayList<>();
+ List<WatchedVolumeInfo> volumesToMount = new ArrayList<>();
synchronized (mLock) {
for (int i = 0; i < mVolumes.size(); i++) {
final WatchedVolumeInfo vol = mVolumes.valueAt(i);
@@ -1303,16 +1315,19 @@ class StorageManagerService extends IStorageManager.Stub
&& vol.getMountUserId() != mCurrentUserId) {
// If there's a visible secondary volume mounted,
// we need to update the currentUserId and remount
+ // But capture the volume with the old user id first to use it in unmounting
+ volumesToUnmount.add(vol.getClonedImmutableVolumeInfo());
vol.setMountUserId(mCurrentUserId);
- volumesToRemount.add(vol);
+ volumesToMount.add(vol);
}
}
}
- for (WatchedVolumeInfo vol : volumesToRemount) {
- Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: " + vol);
- mHandler.obtainMessage(H_VOLUME_UNMOUNT, vol).sendToTarget();
- mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
+ for (int i = 0; i < volumesToMount.size(); i++) {
+ Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: "
+ + volumesToUnmount.get(i));
+ mHandler.obtainMessage(H_VOLUME_UNMOUNT, volumesToUnmount.get(i)).sendToTarget();
+ mHandler.obtainMessage(H_VOLUME_MOUNT, volumesToMount.get(i)).sendToTarget();
}
}
@@ -2430,10 +2445,10 @@ class StorageManagerService extends IStorageManager.Stub
super.unmount_enforcePermission();
final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId);
- unmount(vol);
+ unmount(vol.getClonedImmutableVolumeInfo());
}
- private void unmount(WatchedVolumeInfo vol) {
+ private void unmount(ImmutableVolumeInfo vol) {
try {
try {
if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
@@ -2444,7 +2459,7 @@ class StorageManagerService extends IStorageManager.Stub
}
extendWatchdogTimeout("#unmount might be slow");
mVold.unmount(vol.getId());
- mStorageSessionController.onVolumeUnmount(vol.getImmutableVolumeInfo());
+ mStorageSessionController.onVolumeUnmount(vol);
} catch (Exception e) {
Slog.wtf(TAG, e);
}
diff --git a/services/core/java/com/android/server/SystemTimeZone.java b/services/core/java/com/android/server/SystemTimeZone.java
index dd07081bda12..c8810f672320 100644
--- a/services/core/java/com/android/server/SystemTimeZone.java
+++ b/services/core/java/com/android/server/SystemTimeZone.java
@@ -133,6 +133,7 @@ public final class SystemTimeZone {
boolean timeZoneChanged = false;
synchronized (SystemTimeZone.class) {
String currentTimeZoneId = getTimeZoneId();
+ @TimeZoneConfidence int currentConfidence = getTimeZoneConfidence();
if (currentTimeZoneId == null || !currentTimeZoneId.equals(timeZoneId)) {
SystemProperties.set(TIME_ZONE_SYSTEM_PROPERTY, timeZoneId);
if (DEBUG) {
@@ -145,6 +146,8 @@ public final class SystemTimeZone {
String logMsg = "Time zone or confidence set: "
+ " (new) timeZoneId=" + timeZoneId
+ ", (new) confidence=" + confidence
+ + ", (old) timeZoneId=" + currentTimeZoneId
+ + ", (old) confidence=" + currentConfidence
+ ", logInfo=" + logInfo;
addDebugLogEntry(logMsg);
}
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index bd7a0ac55117..b75b7ddf8181 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -2816,13 +2816,11 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub {
if (!checkNotifyPermission("notifyEmergencyNumberList()")) {
return;
}
- if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
- if (!mContext.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_TELEPHONY_CALLING)) {
- // TelephonyManager.getEmergencyNumberList() throws an exception if
- // FEATURE_TELEPHONY_CALLING is not defined.
- return;
- }
+ if (!mContext.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_TELEPHONY_CALLING)) {
+ // TelephonyManager.getEmergencyNumberList() throws an exception if
+ // FEATURE_TELEPHONY_CALLING is not defined.
+ return;
}
synchronized (mRecords) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 8b701f0e2069..b0b34d0ab9c4 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -19471,7 +19471,7 @@ public class ActivityManagerService extends IActivityManager.Stub
/**
* @hide
*/
- @EnforcePermission("android.permission.INTERACT_ACROSS_USERS_FULL")
+ @EnforcePermission(INTERACT_ACROSS_USERS_FULL)
public IBinder refreshIntentCreatorToken(Intent intent) {
refreshIntentCreatorToken_enforcePermission();
IBinder binder = intent.getCreatorToken();
diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
index 36035bdcddbc..78beb18263a7 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
@@ -832,7 +832,9 @@ class BroadcastQueueImpl extends BroadcastQueue {
// If this receiver is going to be skipped, skip it now itself and don't even enqueue
// it.
- final String skipReason = mSkipPolicy.shouldSkipMessage(r, receiver);
+ final String skipReason = Flags.avoidNoteOpAtEnqueue()
+ ? mSkipPolicy.shouldSkipAtEnqueueMessage(r, receiver)
+ : mSkipPolicy.shouldSkipMessage(r, receiver);
if (skipReason != null) {
setDeliveryState(null, null, r, i, receiver, BroadcastRecord.DELIVERY_SKIPPED,
"skipped by policy at enqueue: " + skipReason);
diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
index d2af84cf3d30..b0d5994cc60b 100644
--- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
+++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java
@@ -71,10 +71,20 @@ public class BroadcastSkipPolicy {
* {@code null} if it can proceed.
*/
public @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target) {
+ return shouldSkipMessage(r, target, false /* preflight */);
+ }
+
+ public @Nullable String shouldSkipAtEnqueueMessage(@NonNull BroadcastRecord r,
+ @NonNull Object target) {
+ return shouldSkipMessage(r, target, true /* preflight */);
+ }
+
+ private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r, @NonNull Object target,
+ boolean preflight) {
if (target instanceof BroadcastFilter) {
- return shouldSkipMessage(r, (BroadcastFilter) target);
+ return shouldSkipMessage(r, (BroadcastFilter) target, preflight);
} else {
- return shouldSkipMessage(r, (ResolveInfo) target);
+ return shouldSkipMessage(r, (ResolveInfo) target, preflight);
}
}
@@ -86,7 +96,7 @@ public class BroadcastSkipPolicy {
* {@code null} if it can proceed.
*/
private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r,
- @NonNull ResolveInfo info) {
+ @NonNull ResolveInfo info, boolean preflight) {
final BroadcastOptions brOptions = r.options;
final ComponentName component = new ComponentName(
info.activityInfo.applicationInfo.packageName,
@@ -134,15 +144,23 @@ public class BroadcastSkipPolicy {
+ " requires " + info.activityInfo.permission;
}
} else if (info.activityInfo.permission != null) {
- final int opCode = AppOpsManager.permissionToOpCode(info.activityInfo.permission);
- if (opCode != AppOpsManager.OP_NONE && mService.getAppOpsManager().noteOpNoThrow(opCode,
- r.callingUid, r.callerPackage, r.callerFeatureId,
- "Broadcast delivered to " + info.activityInfo.name)
- != AppOpsManager.MODE_ALLOWED) {
- return "Appop Denial: broadcasting "
- + broadcastDescription(r, component)
- + " requires appop " + AppOpsManager.permissionToOp(
- info.activityInfo.permission);
+ final String op = AppOpsManager.permissionToOp(info.activityInfo.permission);
+ if (op != null) {
+ final int mode;
+ if (preflight) {
+ mode = mService.getAppOpsManager().checkOpNoThrow(op,
+ r.callingUid, r.callerPackage, r.callerFeatureId);
+ } else {
+ mode = mService.getAppOpsManager().noteOpNoThrow(op,
+ r.callingUid, r.callerPackage, r.callerFeatureId,
+ "Broadcast delivered to " + info.activityInfo.name);
+ }
+ if (mode != AppOpsManager.MODE_ALLOWED) {
+ return "Appop Denial: broadcasting "
+ + broadcastDescription(r, component)
+ + " requires appop " + AppOpsManager.permissionToOp(
+ info.activityInfo.permission);
+ }
}
}
@@ -250,8 +268,8 @@ public class BroadcastSkipPolicy {
perm = PackageManager.PERMISSION_DENIED;
}
- int appOp = AppOpsManager.permissionToOpCode(excludedPermission);
- if (appOp != AppOpsManager.OP_NONE) {
+ final String appOp = AppOpsManager.permissionToOp(excludedPermission);
+ if (appOp != null) {
// When there is an app op associated with the permission,
// skip when both the permission and the app op are
// granted.
@@ -259,7 +277,7 @@ public class BroadcastSkipPolicy {
mService.getAppOpsManager().checkOpNoThrow(appOp,
info.activityInfo.applicationInfo.uid,
info.activityInfo.packageName)
- == AppOpsManager.MODE_ALLOWED)) {
+ == AppOpsManager.MODE_ALLOWED)) {
return "Skipping delivery to " + info.activityInfo.packageName
+ " due to excluded permission " + excludedPermission;
}
@@ -292,9 +310,10 @@ public class BroadcastSkipPolicy {
createAttributionSourcesForResolveInfo(info);
for (int i = 0; i < r.requiredPermissions.length; i++) {
String requiredPermission = r.requiredPermissions[i];
- perm = hasPermissionForDataDelivery(
+ perm = hasPermission(
requiredPermission,
"Broadcast delivered to " + info.activityInfo.name,
+ preflight,
attributionSources)
? PackageManager.PERMISSION_GRANTED
: PackageManager.PERMISSION_DENIED;
@@ -308,10 +327,14 @@ public class BroadcastSkipPolicy {
}
}
}
- if (r.appOp != AppOpsManager.OP_NONE) {
- if (!noteOpForManifestReceiver(r.appOp, r, info, component)) {
+ if (r.appOp != AppOpsManager.OP_NONE && AppOpsManager.isValidOp(r.appOp)) {
+ final String op = AppOpsManager.opToPublicName(r.appOp);
+ final boolean appOpAllowed = preflight
+ ? checkOpForManifestReceiver(r.appOp, op, r, info, component)
+ : noteOpForManifestReceiver(r.appOp, op, r, info, component);
+ if (!appOpAllowed) {
return "Skipping delivery to " + info.activityInfo.packageName
- + " due to required appop " + r.appOp;
+ + " due to required appop " + AppOpsManager.opToName(r.appOp);
}
}
@@ -338,7 +361,7 @@ public class BroadcastSkipPolicy {
* {@code null} if it can proceed.
*/
private @Nullable String shouldSkipMessage(@NonNull BroadcastRecord r,
- @NonNull BroadcastFilter filter) {
+ @NonNull BroadcastFilter filter, boolean preflight) {
if (r.options != null && !r.options.testRequireCompatChange(filter.owningUid)) {
return "Compat change filtered: broadcasting " + r.intent.toString()
+ " to uid " + filter.owningUid + " due to compat change "
@@ -372,18 +395,25 @@ public class BroadcastSkipPolicy {
+ " requires " + filter.requiredPermission
+ " due to registered receiver " + filter;
} else {
- final int opCode = AppOpsManager.permissionToOpCode(filter.requiredPermission);
- if (opCode != AppOpsManager.OP_NONE
- && mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid,
- r.callerPackage, r.callerFeatureId, "Broadcast sent to protected receiver")
- != AppOpsManager.MODE_ALLOWED) {
- return "Appop Denial: broadcasting "
- + r.intent.toString()
- + " from " + r.callerPackage + " (pid="
- + r.callingPid + ", uid=" + r.callingUid + ")"
- + " requires appop " + AppOpsManager.permissionToOp(
- filter.requiredPermission)
- + " due to registered receiver " + filter;
+ final String op = AppOpsManager.permissionToOp(filter.requiredPermission);
+ if (op != null) {
+ final int mode;
+ if (preflight) {
+ mode = mService.getAppOpsManager().checkOpNoThrow(op,
+ r.callingUid, r.callerPackage, r.callerFeatureId);
+ } else {
+ mode = mService.getAppOpsManager().noteOpNoThrow(op, r.callingUid,
+ r.callerPackage, r.callerFeatureId,
+ "Broadcast sent to protected receiver");
+ }
+ if (mode != AppOpsManager.MODE_ALLOWED) {
+ return "Appop Denial: broadcasting "
+ + r.intent
+ + " from " + r.callerPackage + " (pid="
+ + r.callingPid + ", uid=" + r.callingUid + ")"
+ + " requires appop " + op
+ + " due to registered receiver " + filter;
+ }
}
}
}
@@ -433,9 +463,10 @@ public class BroadcastSkipPolicy {
.build();
for (int i = 0; i < r.requiredPermissions.length; i++) {
String requiredPermission = r.requiredPermissions[i];
- final int perm = hasPermissionForDataDelivery(
+ final int perm = hasPermission(
requiredPermission,
"Broadcast delivered to registered receiver " + filter.receiverId,
+ preflight,
attributionSource)
? PackageManager.PERMISSION_GRANTED
: PackageManager.PERMISSION_DENIED;
@@ -471,8 +502,8 @@ public class BroadcastSkipPolicy {
final int perm = checkComponentPermission(excludedPermission,
filter.receiverList.pid, filter.receiverList.uid, -1, true);
- int appOp = AppOpsManager.permissionToOpCode(excludedPermission);
- if (appOp != AppOpsManager.OP_NONE) {
+ final String appOp = AppOpsManager.permissionToOp(excludedPermission);
+ if (appOp != null) {
// When there is an app op associated with the permission,
// skip when both the permission and the app op are
// granted.
@@ -480,14 +511,13 @@ public class BroadcastSkipPolicy {
mService.getAppOpsManager().checkOpNoThrow(appOp,
filter.receiverList.uid,
filter.packageName)
- == AppOpsManager.MODE_ALLOWED)) {
+ == AppOpsManager.MODE_ALLOWED)) {
return "Appop Denial: receiving "
- + r.intent.toString()
+ + r.intent
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
+ ", uid=" + filter.receiverList.uid + ")"
- + " excludes appop " + AppOpsManager.permissionToOp(
- excludedPermission)
+ + " excludes appop " + appOp
+ " due to sender " + r.callerPackage
+ " (uid " + r.callingUid + ")";
}
@@ -496,7 +526,7 @@ public class BroadcastSkipPolicy {
// skip when permission is granted.
if (perm == PackageManager.PERMISSION_GRANTED) {
return "Permission Denial: receiving "
- + r.intent.toString()
+ + r.intent
+ " to " + filter.receiverList.app
+ " (pid=" + filter.receiverList.pid
+ ", uid=" + filter.receiverList.uid + ")"
@@ -523,19 +553,27 @@ public class BroadcastSkipPolicy {
}
// If the broadcast also requires an app op check that as well.
- if (r.appOp != AppOpsManager.OP_NONE
- && mService.getAppOpsManager().noteOpNoThrow(r.appOp,
- filter.receiverList.uid, filter.packageName, filter.featureId,
- "Broadcast delivered to registered receiver " + filter.receiverId)
- != AppOpsManager.MODE_ALLOWED) {
- return "Appop Denial: receiving "
- + r.intent.toString()
- + " to " + filter.receiverList.app
- + " (pid=" + filter.receiverList.pid
- + ", uid=" + filter.receiverList.uid + ")"
- + " requires appop " + AppOpsManager.opToName(r.appOp)
- + " due to sender " + r.callerPackage
- + " (uid " + r.callingUid + ")";
+ if (r.appOp != AppOpsManager.OP_NONE && AppOpsManager.isValidOp(r.appOp)) {
+ final String op = AppOpsManager.opToPublicName(r.appOp);
+ final int mode;
+ if (preflight) {
+ mode = mService.getAppOpsManager().checkOpNoThrow(op,
+ filter.receiverList.uid, filter.packageName, filter.featureId);
+ } else {
+ mode = mService.getAppOpsManager().noteOpNoThrow(op,
+ filter.receiverList.uid, filter.packageName, filter.featureId,
+ "Broadcast delivered to registered receiver " + filter.receiverId);
+ }
+ if (mode != AppOpsManager.MODE_ALLOWED) {
+ return "Appop Denial: receiving "
+ + r.intent
+ + " to " + filter.receiverList.app
+ + " (pid=" + filter.receiverList.pid
+ + ", uid=" + filter.receiverList.uid + ")"
+ + " requires appop " + AppOpsManager.opToName(r.appOp)
+ + " due to sender " + r.callerPackage
+ + " (uid " + r.callingUid + ")";
+ }
}
// Ensure that broadcasts are only sent to other apps if they are explicitly marked as
@@ -572,14 +610,14 @@ public class BroadcastSkipPolicy {
+ ", uid=" + r.callingUid + ") to " + component.flattenToShortString();
}
- private boolean noteOpForManifestReceiver(int appOp, BroadcastRecord r, ResolveInfo info,
- ComponentName component) {
+ private boolean noteOpForManifestReceiver(int opCode, String appOp, BroadcastRecord r,
+ ResolveInfo info, ComponentName component) {
if (ArrayUtils.isEmpty(info.activityInfo.attributionTags)) {
- return noteOpForManifestReceiverInner(appOp, r, info, component, null);
+ return noteOpForManifestReceiverInner(opCode, appOp, r, info, component, null);
} else {
// Attribution tags provided, noteOp each tag
for (String tag : info.activityInfo.attributionTags) {
- if (!noteOpForManifestReceiverInner(appOp, r, info, component, tag)) {
+ if (!noteOpForManifestReceiverInner(opCode, appOp, r, info, component, tag)) {
return false;
}
}
@@ -587,8 +625,8 @@ public class BroadcastSkipPolicy {
}
}
- private boolean noteOpForManifestReceiverInner(int appOp, BroadcastRecord r, ResolveInfo info,
- ComponentName component, String tag) {
+ private boolean noteOpForManifestReceiverInner(int opCode, String appOp, BroadcastRecord r,
+ ResolveInfo info, ComponentName component, String tag) {
if (mService.getAppOpsManager().noteOpNoThrow(appOp,
info.activityInfo.applicationInfo.uid,
info.activityInfo.packageName,
@@ -598,7 +636,37 @@ public class BroadcastSkipPolicy {
Slog.w(TAG, "Appop Denial: receiving "
+ r.intent + " to "
+ component.flattenToShortString()
- + " requires appop " + AppOpsManager.opToName(appOp)
+ + " requires appop " + AppOpsManager.opToName(opCode)
+ + " due to sender " + r.callerPackage
+ + " (uid " + r.callingUid + ")");
+ return false;
+ }
+ return true;
+ }
+
+ private boolean checkOpForManifestReceiver(int opCode, String appOp, BroadcastRecord r,
+ ResolveInfo info, ComponentName component) {
+ if (ArrayUtils.isEmpty(info.activityInfo.attributionTags)) {
+ return checkOpForManifestReceiverInner(opCode, appOp, r, info, component, null);
+ } else {
+ // Attribution tags provided, noteOp each tag
+ for (String tag : info.activityInfo.attributionTags) {
+ if (!checkOpForManifestReceiverInner(opCode, appOp, r, info, component, tag)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ private boolean checkOpForManifestReceiverInner(int opCode, String appOp, BroadcastRecord r,
+ ResolveInfo info, ComponentName component, String tag) {
+ if (mService.getAppOpsManager().checkOpNoThrow(appOp, info.activityInfo.applicationInfo.uid,
+ info.activityInfo.packageName, tag) != AppOpsManager.MODE_ALLOWED) {
+ Slog.w(TAG, "Appop Denial: receiving "
+ + r.intent + " to "
+ + component.flattenToShortString()
+ + " requires appop " + AppOpsManager.opToName(opCode)
+ " due to sender " + r.callerPackage
+ " (uid " + r.callingUid + ")");
return false;
@@ -694,9 +762,10 @@ public class BroadcastSkipPolicy {
return mPermissionManager;
}
- private boolean hasPermissionForDataDelivery(
+ private boolean hasPermission(
@NonNull String permission,
@NonNull String message,
+ boolean preflight,
@NonNull AttributionSource... attributionSources) {
final PermissionManager permissionManager = getPermissionManager();
if (permissionManager == null) {
@@ -704,9 +773,14 @@ public class BroadcastSkipPolicy {
}
for (AttributionSource attributionSource : attributionSources) {
- final int permissionCheckResult =
- permissionManager.checkPermissionForDataDelivery(
- permission, attributionSource, message);
+ final int permissionCheckResult;
+ if (preflight) {
+ permissionCheckResult = permissionManager.checkPermissionForPreflight(
+ permission, attributionSource);
+ } else {
+ permissionCheckResult = permissionManager.checkPermissionForDataDelivery(
+ permission, attributionSource, message);
+ }
if (permissionCheckResult != PackageManager.PERMISSION_GRANTED) {
return false;
}
diff --git a/services/core/java/com/android/server/am/broadcasts_flags.aconfig b/services/core/java/com/android/server/am/broadcasts_flags.aconfig
index 7f169db7dcec..68e21a35a531 100644
--- a/services/core/java/com/android/server/am/broadcasts_flags.aconfig
+++ b/services/core/java/com/android/server/am/broadcasts_flags.aconfig
@@ -15,4 +15,15 @@ flag {
description: "Limit the scope of receiver priorities to within a process"
is_fixed_read_only: true
bug: "369487976"
+}
+
+flag {
+ name: "avoid_note_op_at_enqueue"
+ namespace: "backstage_power"
+ description: "Avoid triggering noteOp while enqueueing a broadcast"
+ is_fixed_read_only: true
+ bug: "268016162"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
} \ No newline at end of file
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 3cb2125f7820..0f1228f44b0d 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -1164,9 +1164,11 @@ public class AudioService extends IAudioService.Stub
@GuardedBy("mAccessibilityServiceUidsLock")
private int[] mAccessibilityServiceUids;
+ // Input Method
+ private final Object mInputMethodServiceUidLock = new Object();
// Uid of the active input method service to check if caller is the one or not.
+ @GuardedBy("mInputMethodServiceUidLock")
private int mInputMethodServiceUid = android.os.Process.INVALID_UID;
- private final Object mInputMethodServiceUidLock = new Object();
private int mEncodedSurroundMode;
private String mEnabledSurroundFormats;
@@ -11405,7 +11407,7 @@ public class AudioService extends IAudioService.Stub
/** see {@link AudioManager#getFocusDuckedUidsForTest()} */
@Override
- @EnforcePermission("android.permission.QUERY_AUDIO_STATE")
+ @EnforcePermission(QUERY_AUDIO_STATE)
public @NonNull List<Integer> getFocusDuckedUidsForTest() {
super.getFocusDuckedUidsForTest_enforcePermission();
return mPlaybackMonitor.getFocusDuckedUids();
@@ -11432,7 +11434,7 @@ public class AudioService extends IAudioService.Stub
* @see AudioManager#getFocusFadeOutDurationForTest()
* @return the fade out duration, in ms
*/
- @EnforcePermission("android.permission.QUERY_AUDIO_STATE")
+ @EnforcePermission(QUERY_AUDIO_STATE)
public long getFocusFadeOutDurationForTest() {
super.getFocusFadeOutDurationForTest_enforcePermission();
return mMediaFocusControl.getFocusFadeOutDurationForTest();
@@ -11445,7 +11447,7 @@ public class AudioService extends IAudioService.Stub
* @return the time gap after a fade out completion on focus loss, and fade in start, in ms
*/
@Override
- @EnforcePermission("android.permission.QUERY_AUDIO_STATE")
+ @EnforcePermission(QUERY_AUDIO_STATE)
public long getFocusUnmuteDelayAfterFadeOutForTest() {
super.getFocusUnmuteDelayAfterFadeOutForTest_enforcePermission();
return mMediaFocusControl.getFocusUnmuteDelayAfterFadeOutForTest();
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
index 940bcb4c6ba1..f40d0dd18213 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubEndpointBroker.java
@@ -43,7 +43,10 @@ import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import java.util.Collection;
+import java.util.HashSet;
import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
/**
* A class that represents a broker for the endpoint registered by the client app. This class
@@ -111,6 +114,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
private final boolean mRemoteInitiated;
+ /**
+ * The set of seq # for pending reliable messages started by this endpoint for this session.
+ */
+ private final Set<Integer> mPendingSequenceNumbers = new HashSet<>();
+
SessionInfo(HubEndpointInfo remoteEndpointInfo, boolean remoteInitiated) {
mRemoteEndpointInfo = remoteEndpointInfo;
mRemoteInitiated = remoteInitiated;
@@ -131,6 +139,24 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
public boolean isActive() {
return mSessionState == SessionState.ACTIVE;
}
+
+ public boolean isReliableMessagePending(int sequenceNumber) {
+ return mPendingSequenceNumbers.contains(sequenceNumber);
+ }
+
+ public void setReliableMessagePending(int sequenceNumber) {
+ mPendingSequenceNumbers.add(sequenceNumber);
+ }
+
+ public void setReliableMessageCompleted(int sequenceNumber) {
+ mPendingSequenceNumbers.remove(sequenceNumber);
+ }
+
+ public void forEachPendingReliableMessage(Consumer<Integer> consumer) {
+ for (int sequenceNumber : mPendingSequenceNumbers) {
+ consumer.accept(sequenceNumber);
+ }
+ }
}
/** A map between a session ID which maps to its current state. */
@@ -208,10 +234,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
try {
mSessionInfoMap.put(sessionId, new SessionInfo(destination, false));
mHubInterface.openEndpointSession(
- sessionId,
- halEndpointInfo.id,
- mHalEndpointInfo.id,
- serviceDescriptor);
+ sessionId, halEndpointInfo.id, mHalEndpointInfo.id, serviceDescriptor);
} catch (RemoteException | IllegalArgumentException | UnsupportedOperationException e) {
Log.e(TAG, "Exception while calling HAL openEndpointSession", e);
cleanupSessionResources(sessionId);
@@ -286,34 +309,42 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
public void sendMessage(
int sessionId, HubMessage message, IContextHubTransactionCallback callback) {
super.sendMessage_enforcePermission();
- Message halMessage = ContextHubServiceUtil.createHalMessage(message);
- if (!isSessionActive(sessionId)) {
- throw new SecurityException(
- "sendMessage called on inactive session (id= " + sessionId + ")");
- }
-
- if (callback == null) {
- try {
- mHubInterface.sendMessageToEndpoint(sessionId, halMessage);
- } catch (RemoteException e) {
- Log.w(TAG, "Exception while sending message on session " + sessionId, e);
+ synchronized (mOpenSessionLock) {
+ SessionInfo info = mSessionInfoMap.get(sessionId);
+ if (info == null) {
+ throw new IllegalArgumentException(
+ "sendMessage for invalid session id=" + sessionId);
}
- } else {
- ContextHubServiceTransaction transaction =
- mTransactionManager.createSessionMessageTransaction(
- mHubInterface, sessionId, halMessage, mPackageName, callback);
- try {
- mTransactionManager.addTransaction(transaction);
- } catch (IllegalStateException e) {
- Log.e(
- TAG,
- "Unable to add a transaction in sendMessageToEndpoint "
- + "(session ID = "
- + sessionId
- + ")",
- e);
- transaction.onTransactionComplete(
- ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE);
+ if (!info.isActive()) {
+ throw new SecurityException(
+ "sendMessage called on inactive session (id= " + sessionId + ")");
+ }
+
+ Message halMessage = ContextHubServiceUtil.createHalMessage(message);
+ if (callback == null) {
+ try {
+ mHubInterface.sendMessageToEndpoint(sessionId, halMessage);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Exception while sending message on session " + sessionId, e);
+ }
+ } else {
+ ContextHubServiceTransaction transaction =
+ mTransactionManager.createSessionMessageTransaction(
+ mHubInterface, sessionId, halMessage, mPackageName, callback);
+ try {
+ mTransactionManager.addTransaction(transaction);
+ info.setReliableMessagePending(transaction.getMessageSequenceNumber());
+ } catch (IllegalStateException e) {
+ Log.e(
+ TAG,
+ "Unable to add a transaction in sendMessageToEndpoint "
+ + "(session ID = "
+ + sessionId
+ + ")",
+ e);
+ transaction.onTransactionComplete(
+ ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE);
+ }
}
}
}
@@ -393,7 +424,9 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
int id = mSessionInfoMap.keyAt(i);
int count = i + 1;
sb.append(
- " " + count + ". id="
+ " "
+ + count
+ + ". id="
+ id
+ ", remote:"
+ mSessionInfoMap.get(id).getRemoteEndpointInfo());
@@ -461,13 +494,24 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
/* package */ void onMessageReceived(int sessionId, HubMessage message) {
byte code = onMessageReceivedInternal(sessionId, message);
if (code != ErrorCode.OK && message.isResponseRequired()) {
- sendMessageDeliveryStatus(
- sessionId, message.getMessageSequenceNumber(), code);
+ sendMessageDeliveryStatus(sessionId, message.getMessageSequenceNumber(), code);
}
}
/* package */ void onMessageDeliveryStatusReceived(
int sessionId, int sequenceNumber, byte errorCode) {
+ synchronized (mOpenSessionLock) {
+ SessionInfo info = mSessionInfoMap.get(sessionId);
+ if (info == null || !info.isActive()) {
+ Log.w(TAG, "Received delivery status for invalid session: id=" + sessionId);
+ return;
+ }
+ if (!info.isReliableMessagePending(sequenceNumber)) {
+ Log.w(TAG, "Received delivery status for unknown seq: " + sequenceNumber);
+ return;
+ }
+ info.setReliableMessageCompleted(sequenceNumber);
+ }
mTransactionManager.onMessageDeliveryResponse(sequenceNumber, errorCode == ErrorCode.OK);
}
@@ -492,7 +536,6 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
onCloseEndpointSession(id, Reason.HUB_RESET);
}
}
- // TODO(b/390029594): Cancel any ongoing reliable communication transactions
}
private Optional<Byte> onEndpointSessionOpenRequestInternal(
@@ -515,9 +558,11 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
mSessionInfoMap.put(sessionId, new SessionInfo(initiator, true));
}
- boolean success = invokeCallback(
- (consumer) ->
- consumer.onSessionOpenRequest(sessionId, initiator, serviceDescriptor));
+ boolean success =
+ invokeCallback(
+ (consumer) ->
+ consumer.onSessionOpenRequest(
+ sessionId, initiator, serviceDescriptor));
return success ? Optional.empty() : Optional.of(Reason.UNSPECIFIED);
}
@@ -590,8 +635,15 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
private boolean cleanupSessionResources(int sessionId) {
synchronized (mOpenSessionLock) {
SessionInfo info = mSessionInfoMap.get(sessionId);
- if (info != null && !info.isRemoteInitiated()) {
- mEndpointManager.returnSessionId(sessionId);
+ if (info != null) {
+ if (!info.isRemoteInitiated()) {
+ mEndpointManager.returnSessionId(sessionId);
+ }
+ info.forEachPendingReliableMessage(
+ (sequenceNumber) -> {
+ mTransactionManager.onMessageDeliveryResponse(
+ sequenceNumber, /* success= */ false);
+ });
mSessionInfoMap.remove(sessionId);
}
return info != null;
@@ -646,10 +698,7 @@ public class ContextHubEndpointBroker extends IContextHubEndpoint.Stub
try {
mWakeLock.release();
} catch (RuntimeException e) {
- Log.e(
- TAG,
- "Releasing the wakelock for all acquisitions fails - ",
- e);
+ Log.e(TAG, "Releasing the wakelock for all acquisitions fails - ", e);
break;
}
}
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
index a430a82fc13b..6a1db0223db5 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
@@ -29,6 +29,7 @@ import android.os.SystemClock;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import java.time.Duration;
import java.util.ArrayDeque;
@@ -165,52 +166,61 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Creates a transaction for loading a nanoapp.
*
- * @param contextHubId the ID of the hub to load the nanoapp to
- * @param nanoAppBinary the binary of the nanoapp to load
+ * @param contextHubId the ID of the hub to load the nanoapp to
+ * @param nanoAppBinary the binary of the nanoapp to load
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createLoadTransaction(
- int contextHubId, NanoAppBinary nanoAppBinary,
- IContextHubTransactionCallback onCompleteCallback, String packageName) {
+ int contextHubId,
+ NanoAppBinary nanoAppBinary,
+ IContextHubTransactionCallback onCompleteCallback,
+ String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_LOAD_NANOAPP,
- nanoAppBinary.getNanoAppId(), packageName) {
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_LOAD_NANOAPP,
+ nanoAppBinary.getNanoAppId(),
+ packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.loadNanoapp(
contextHubId, nanoAppBinary, this.getTransactionId());
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while trying to load nanoapp with ID 0x" +
- Long.toHexString(nanoAppBinary.getNanoAppId()), e);
+ Log.e(
+ TAG,
+ "RemoteException while trying to load nanoapp with ID 0x"
+ + Long.toHexString(nanoAppBinary.getNanoAppId()),
+ e);
return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
}
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
ContextHubStatsLog.write(
ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED,
nanoAppBinary.getNanoAppId(),
nanoAppBinary.getNanoAppVersion(),
ContextHubStatsLog
- .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_LOAD,
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_LOAD,
toStatsTransactionResult(result));
- ContextHubEventLogger.getInstance().logNanoappLoad(
- contextHubId,
- nanoAppBinary.getNanoAppId(),
- nanoAppBinary.getNanoAppVersion(),
- nanoAppBinary.getBinary().length,
- result == ContextHubTransaction.RESULT_SUCCESS);
+ ContextHubEventLogger.getInstance()
+ .logNanoappLoad(
+ contextHubId,
+ nanoAppBinary.getNanoAppId(),
+ nanoAppBinary.getNanoAppVersion(),
+ nanoAppBinary.getBinary().length,
+ result == ContextHubTransaction.RESULT_SUCCESS);
if (result == ContextHubTransaction.RESULT_SUCCESS) {
// NOTE: The legacy JNI code used to do a query right after a load success
// to synchronize the service cache. Instead store the binary that was
// requested to load to update the cache later without doing a query.
mNanoAppStateManager.addNanoAppInstance(
- contextHubId, nanoAppBinary.getNanoAppId(),
+ contextHubId,
+ nanoAppBinary.getNanoAppId(),
nanoAppBinary.getNanoAppVersion());
}
try {
@@ -228,42 +238,51 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Creates a transaction for unloading a nanoapp.
*
- * @param contextHubId the ID of the hub to unload the nanoapp from
- * @param nanoAppId the ID of the nanoapp to unload
+ * @param contextHubId the ID of the hub to unload the nanoapp from
+ * @param nanoAppId the ID of the nanoapp to unload
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createUnloadTransaction(
- int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback,
+ int contextHubId,
+ long nanoAppId,
+ IContextHubTransactionCallback onCompleteCallback,
String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_UNLOAD_NANOAPP,
- nanoAppId, packageName) {
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_UNLOAD_NANOAPP,
+ nanoAppId,
+ packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.unloadNanoapp(
contextHubId, nanoAppId, this.getTransactionId());
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while trying to unload nanoapp with ID 0x" +
- Long.toHexString(nanoAppId), e);
+ Log.e(
+ TAG,
+ "RemoteException while trying to unload nanoapp with ID 0x"
+ + Long.toHexString(nanoAppId),
+ e);
return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
}
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
ContextHubStatsLog.write(
- ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED, nanoAppId,
+ ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED,
+ nanoAppId,
0 /* nanoappVersion */,
ContextHubStatsLog
- .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_UNLOAD,
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_TYPE__TYPE_UNLOAD,
toStatsTransactionResult(result));
- ContextHubEventLogger.getInstance().logNanoappUnload(
- contextHubId,
- nanoAppId,
- result == ContextHubTransaction.RESULT_SUCCESS);
+ ContextHubEventLogger.getInstance()
+ .logNanoappUnload(
+ contextHubId,
+ nanoAppId,
+ result == ContextHubTransaction.RESULT_SUCCESS);
if (result == ContextHubTransaction.RESULT_SUCCESS) {
mNanoAppStateManager.removeNanoAppInstance(contextHubId, nanoAppId);
@@ -283,31 +302,37 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Creates a transaction for enabling a nanoapp.
*
- * @param contextHubId the ID of the hub to enable the nanoapp on
- * @param nanoAppId the ID of the nanoapp to enable
+ * @param contextHubId the ID of the hub to enable the nanoapp on
+ * @param nanoAppId the ID of the nanoapp to enable
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createEnableTransaction(
- int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback,
+ int contextHubId,
+ long nanoAppId,
+ IContextHubTransactionCallback onCompleteCallback,
String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_ENABLE_NANOAPP,
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_ENABLE_NANOAPP,
packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.enableNanoapp(
contextHubId, nanoAppId, this.getTransactionId());
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while trying to enable nanoapp with ID 0x" +
- Long.toHexString(nanoAppId), e);
+ Log.e(
+ TAG,
+ "RemoteException while trying to enable nanoapp with ID 0x"
+ + Long.toHexString(nanoAppId),
+ e);
return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
}
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
try {
onCompleteCallback.onTransactionComplete(result);
} catch (RemoteException e) {
@@ -320,31 +345,37 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Creates a transaction for disabling a nanoapp.
*
- * @param contextHubId the ID of the hub to disable the nanoapp on
- * @param nanoAppId the ID of the nanoapp to disable
+ * @param contextHubId the ID of the hub to disable the nanoapp on
+ * @param nanoAppId the ID of the nanoapp to disable
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createDisableTransaction(
- int contextHubId, long nanoAppId, IContextHubTransactionCallback onCompleteCallback,
+ int contextHubId,
+ long nanoAppId,
+ IContextHubTransactionCallback onCompleteCallback,
String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_DISABLE_NANOAPP,
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_DISABLE_NANOAPP,
packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.disableNanoapp(
contextHubId, nanoAppId, this.getTransactionId());
} catch (RemoteException e) {
- Log.e(TAG, "RemoteException while trying to disable nanoapp with ID 0x" +
- Long.toHexString(nanoAppId), e);
+ Log.e(
+ TAG,
+ "RemoteException while trying to disable nanoapp with ID 0x"
+ + Long.toHexString(nanoAppId),
+ e);
return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
}
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
try {
onCompleteCallback.onTransactionComplete(result);
} catch (RemoteException e) {
@@ -447,18 +478,20 @@ import java.util.concurrent.atomic.AtomicInteger;
/**
* Creates a transaction for querying for a list of nanoapps.
*
- * @param contextHubId the ID of the hub to query
+ * @param contextHubId the ID of the hub to query
* @param onCompleteCallback the client on complete callback
* @return the generated transaction
*/
/* package */ ContextHubServiceTransaction createQueryTransaction(
- int contextHubId, IContextHubTransactionCallback onCompleteCallback,
+ int contextHubId,
+ IContextHubTransactionCallback onCompleteCallback,
String packageName) {
return new ContextHubServiceTransaction(
- mNextAvailableId.getAndIncrement(), ContextHubTransaction.TYPE_QUERY_NANOAPPS,
+ mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_QUERY_NANOAPPS,
packageName) {
@Override
- /* package */ int onTransact() {
+ /* package */ int onTransact() {
try {
return mContextHubProxy.queryNanoapps(contextHubId);
} catch (RemoteException e) {
@@ -468,12 +501,12 @@ import java.util.concurrent.atomic.AtomicInteger;
}
@Override
- /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
onQueryResponse(result, Collections.emptyList());
}
@Override
- /* package */ void onQueryResponse(
+ /* package */ void onQueryResponse(
@ContextHubTransaction.Result int result, List<NanoAppState> nanoAppStateList) {
try {
onCompleteCallback.onQueryResponse(result, nanoAppStateList);
@@ -539,6 +572,14 @@ import java.util.concurrent.atomic.AtomicInteger;
}
}
+ @VisibleForTesting
+ /* package */
+ int numReliableMessageTransactionPending() {
+ synchronized (mReliableMessageLock) {
+ return mReliableMessageTransactionMap.size();
+ }
+ }
+
/**
* Handles a transaction response from a Context Hub.
*
@@ -585,18 +626,21 @@ import java.util.concurrent.atomic.AtomicInteger;
void onMessageDeliveryResponse(int messageSequenceNumber, boolean success) {
if (!Flags.reliableMessageRetrySupportService()) {
TransactionAcceptConditions conditions =
- transaction -> transaction.getTransactionType()
- == ContextHubTransaction.TYPE_RELIABLE_MESSAGE
- && transaction.getMessageSequenceNumber()
- == messageSequenceNumber;
+ transaction ->
+ transaction.getTransactionType()
+ == ContextHubTransaction.TYPE_RELIABLE_MESSAGE
+ && transaction.getMessageSequenceNumber()
+ == messageSequenceNumber;
ContextHubServiceTransaction transaction = getTransactionAndHandleNext(conditions);
if (transaction == null) {
- Log.w(TAG, "Received unexpected message delivery response (expected"
- + " message sequence number = "
- + messageSequenceNumber
- + ", received messageSequenceNumber = "
- + messageSequenceNumber
- + ")");
+ Log.w(
+ TAG,
+ "Received unexpected message delivery response (expected"
+ + " message sequence number = "
+ + messageSequenceNumber
+ + ", received messageSequenceNumber = "
+ + messageSequenceNumber
+ + ")");
return;
}
@@ -640,8 +684,10 @@ import java.util.concurrent.atomic.AtomicInteger;
*/
/* package */
void onQueryResponse(List<NanoAppState> nanoAppStateList) {
- TransactionAcceptConditions conditions = transaction ->
- transaction.getTransactionType() == ContextHubTransaction.TYPE_QUERY_NANOAPPS;
+ TransactionAcceptConditions conditions =
+ transaction ->
+ transaction.getTransactionType()
+ == ContextHubTransaction.TYPE_QUERY_NANOAPPS;
ContextHubServiceTransaction transaction = getTransactionAndHandleNext(conditions);
if (transaction == null) {
Log.w(TAG, "Received unexpected query response");
@@ -968,24 +1014,33 @@ import java.util.concurrent.atomic.AtomicInteger;
private int toStatsTransactionResult(@ContextHubTransaction.Result int result) {
switch (result) {
case ContextHubTransaction.RESULT_SUCCESS:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_SUCCESS;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_SUCCESS;
case ContextHubTransaction.RESULT_FAILED_BAD_PARAMS:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BAD_PARAMS;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BAD_PARAMS;
case ContextHubTransaction.RESULT_FAILED_UNINITIALIZED:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNINITIALIZED;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNINITIALIZED;
case ContextHubTransaction.RESULT_FAILED_BUSY:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BUSY;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_BUSY;
case ContextHubTransaction.RESULT_FAILED_AT_HUB:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_AT_HUB;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_AT_HUB;
case ContextHubTransaction.RESULT_FAILED_TIMEOUT:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_TIMEOUT;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_TIMEOUT;
case ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_SERVICE_INTERNAL_FAILURE;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_SERVICE_INTERNAL_FAILURE;
case ContextHubTransaction.RESULT_FAILED_HAL_UNAVAILABLE:
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_HAL_UNAVAILABLE;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_HAL_UNAVAILABLE;
case ContextHubTransaction.RESULT_FAILED_UNKNOWN:
default: /* fall through */
- return ContextHubStatsLog.CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNKNOWN;
+ return ContextHubStatsLog
+ .CHRE_CODE_DOWNLOAD_TRANSACTED__TRANSACTION_RESULT__TRANSACTION_RESULT_FAILED_UNKNOWN;
}
}
diff --git a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
index 7a96195528d5..993704988d86 100644
--- a/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
+++ b/services/core/java/com/android/server/net/watchlist/WatchlistReportDbHelper.java
@@ -21,6 +21,7 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Environment;
@@ -204,6 +205,11 @@ class WatchlistReportDbHelper extends SQLiteOpenHelper {
return false;
}
final String clause = WhiteListReportContract.TIMESTAMP + "< " + untilTimestamp;
- return db.delete(WhiteListReportContract.TABLE, clause, null) != 0;
+ try {
+ return db.delete(WhiteListReportContract.TABLE, clause, null) != 0;
+ } catch (SQLiteDatabaseCorruptException e) {
+ Slog.e(TAG, "Error deleting records", e);
+ return false;
+ }
}
}
diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java
index 3f2c2228e453..dd52cce9e927 100644
--- a/services/core/java/com/android/server/notification/ConditionProviders.java
+++ b/services/core/java/com/android/server/notification/ConditionProviders.java
@@ -46,6 +46,7 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
public class ConditionProviders extends ManagedServices {
@@ -202,7 +203,14 @@ public class ConditionProviders extends ManagedServices {
@Override
protected void loadDefaultsFromConfig() {
- String defaultDndAccess = mContext.getResources().getString(
+ for (String dndPackage : getDefaultDndAccessPackages(mContext)) {
+ addDefaultComponentOrPackage(dndPackage);
+ }
+ }
+
+ static List<String> getDefaultDndAccessPackages(Context context) {
+ ArrayList<String> packages = new ArrayList<>();
+ String defaultDndAccess = context.getResources().getString(
R.string.config_defaultDndAccessPackages);
if (defaultDndAccess != null) {
String[] dnds = defaultDndAccess.split(ManagedServices.ENABLED_SERVICES_SEPARATOR);
@@ -210,9 +218,10 @@ public class ConditionProviders extends ManagedServices {
if (TextUtils.isEmpty(dnds[i])) {
continue;
}
- addDefaultComponentOrPackage(dnds[i]);
+ packages.add(dnds[i]);
}
}
+ return packages;
}
@Override
diff --git a/services/core/java/com/android/server/notification/ZenConfigTrimmer.java b/services/core/java/com/android/server/notification/ZenConfigTrimmer.java
new file mode 100644
index 000000000000..d65954d11646
--- /dev/null
+++ b/services/core/java/com/android/server/notification/ZenConfigTrimmer.java
@@ -0,0 +1,109 @@
+/*
+ * 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.notification;
+
+import android.content.Context;
+import android.os.Parcel;
+import android.service.notification.SystemZenRules;
+import android.service.notification.ZenModeConfig;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+class ZenConfigTrimmer {
+
+ private static final String TAG = "ZenConfigTrimmer";
+ private static final int MAXIMUM_PARCELED_SIZE = 150_000; // bytes
+
+ private final HashSet<String> mTrustedPackages;
+
+ ZenConfigTrimmer(Context context) {
+ mTrustedPackages = new HashSet<>();
+ mTrustedPackages.add(SystemZenRules.PACKAGE_ANDROID);
+ mTrustedPackages.addAll(ConditionProviders.getDefaultDndAccessPackages(context));
+ }
+
+ void trimToMaximumSize(ZenModeConfig config) {
+ Map<String, PackageRules> rulesPerPackage = new HashMap<>();
+ for (ZenModeConfig.ZenRule rule : config.automaticRules.values()) {
+ PackageRules pkgRules = rulesPerPackage.computeIfAbsent(rule.pkg, PackageRules::new);
+ pkgRules.mRules.add(rule);
+ }
+
+ int totalSize = 0;
+ for (PackageRules pkgRules : rulesPerPackage.values()) {
+ totalSize += pkgRules.dataSize();
+ }
+
+ if (totalSize > MAXIMUM_PARCELED_SIZE) {
+ List<PackageRules> deletionCandidates = new ArrayList<>();
+ for (PackageRules pkgRules : rulesPerPackage.values()) {
+ if (!mTrustedPackages.contains(pkgRules.mPkg)) {
+ deletionCandidates.add(pkgRules);
+ }
+ }
+ deletionCandidates.sort(Comparator.comparingInt(PackageRules::dataSize).reversed());
+
+ evictPackagesFromConfig(config, deletionCandidates, totalSize);
+ }
+ }
+
+ private static void evictPackagesFromConfig(ZenModeConfig config,
+ List<PackageRules> deletionCandidates, int currentSize) {
+ while (currentSize > MAXIMUM_PARCELED_SIZE && !deletionCandidates.isEmpty()) {
+ PackageRules rulesToDelete = deletionCandidates.removeFirst();
+ Slog.w(TAG, String.format("Evicting %s zen rules from package '%s' (%s bytes)",
+ rulesToDelete.mRules.size(), rulesToDelete.mPkg, rulesToDelete.dataSize()));
+
+ for (ZenModeConfig.ZenRule rule : rulesToDelete.mRules) {
+ config.automaticRules.remove(rule.id);
+ }
+
+ currentSize -= rulesToDelete.dataSize();
+ }
+ }
+
+ private static class PackageRules {
+ private final String mPkg;
+ private final List<ZenModeConfig.ZenRule> mRules;
+ private int mParceledSize = -1;
+
+ PackageRules(String pkg) {
+ mPkg = pkg;
+ mRules = new ArrayList<>();
+ }
+
+ private int dataSize() {
+ if (mParceledSize >= 0) {
+ return mParceledSize;
+ }
+ Parcel parcel = Parcel.obtain();
+ try {
+ parcel.writeParcelableList(mRules, 0);
+ mParceledSize = parcel.dataSize();
+ return mParceledSize;
+ } finally {
+ parcel.recycle();
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 889df512dd60..8b09c2acb96a 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -48,6 +48,7 @@ import static android.service.notification.ZenModeConfig.isImplicitRuleId;
import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.server.notification.Flags.preventZenDeviceEffectsWhileDriving;
+import static com.android.server.notification.Flags.limitZenConfigSize;
import static java.util.Objects.requireNonNull;
@@ -192,6 +193,7 @@ public class ZenModeHelper {
private final ConditionProviders.Config mServiceConfig;
private final SystemUiSystemPropertiesFlags.FlagResolver mFlagResolver;
private final ZenModeEventLogger mZenModeEventLogger;
+ private final ZenConfigTrimmer mConfigTrimmer;
@VisibleForTesting protected int mZenMode;
@VisibleForTesting protected NotificationManager.Policy mConsolidatedPolicy;
@@ -226,6 +228,7 @@ public class ZenModeHelper {
mClock = clock;
addCallback(mMetrics);
mAppOps = context.getSystemService(AppOpsManager.class);
+ mConfigTrimmer = new ZenConfigTrimmer(mContext);
mDefaultConfig = Flags.modesUi()
? ZenModeConfig.getDefaultConfig()
@@ -2061,20 +2064,20 @@ public class ZenModeHelper {
Log.w(TAG, "Invalid config in setConfigLocked; " + config);
return false;
}
+ if (limitZenConfigSize() && (origin == ORIGIN_APP || origin == ORIGIN_USER_IN_APP)) {
+ mConfigTrimmer.trimToMaximumSize(config);
+ }
+
if (config.user != mUser) {
// simply store away for background users
- synchronized (mConfigLock) {
- mConfigs.put(config.user, config);
- }
+ mConfigs.put(config.user, config);
if (DEBUG) Log.d(TAG, "setConfigLocked: store config for user " + config.user);
return true;
}
// handle CPS backed conditions - danger! may modify config
mConditions.evaluateConfig(config, null, false /*processSubscriptions*/);
- synchronized (mConfigLock) {
- mConfigs.put(config.user, config);
- }
+ mConfigs.put(config.user, config);
if (DEBUG) Log.d(TAG, "setConfigLocked reason=" + reason, new Throwable());
ZenLog.traceConfig(origin, reason, triggeringComponent, mConfig, config, callingUid);
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 76cd5c88b388..346d65a06cc9 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -212,6 +212,16 @@ flag {
}
flag {
+ name: "limit_zen_config_size"
+ namespace: "systemui"
+ description: "Enforce a maximum (serialized) size for the Zen configuration"
+ bug: "387498139"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "managed_services_concurrent_multiuser"
namespace: "systemui"
description: "Enables ManagedServices to support Concurrent multi user environment"
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 80c2d415d370..5a6d7a245f56 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2937,28 +2937,20 @@ public class UserManagerService extends IUserManager.Stub {
int flags = UserManager.SWITCHABILITY_STATUS_OK;
- t.traceBegin("TM.isInCall");
- final long identity = Binder.clearCallingIdentity();
- try {
- final TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
- if (com.android.internal.telephony.flags
- .Flags.enforceTelephonyFeatureMappingForPublicApis()) {
- if (mContext.getPackageManager().hasSystemFeature(
- PackageManager.FEATURE_TELECOM)) {
- if (telecomManager != null && telecomManager.isInCall()) {
- flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
- }
- }
- } else {
+ if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM)) {
+ t.traceBegin("TM.isInCall");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ final TelecomManager telecomManager = mContext.getSystemService(
+ TelecomManager.class);
if (telecomManager != null && telecomManager.isInCall()) {
flags |= UserManager.SWITCHABILITY_STATUS_USER_IN_CALL;
}
+ } finally {
+ Binder.restoreCallingIdentity(identity);
}
- } finally {
- Binder.restoreCallingIdentity(identity);
+ t.traceEnd();
}
- t.traceEnd();
-
t.traceBegin("hasUserRestriction-DISALLOW_USER_SWITCH");
if (mLocalService.hasUserRestriction(DISALLOW_USER_SWITCH, userId)) {
flags |= UserManager.SWITCHABILITY_STATUS_USER_SWITCH_DISALLOWED;
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
index fab19b6b8201..1afbb34c5f09 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -160,8 +160,10 @@ public interface StatusBarManagerInternal {
* @param displayId The changed display Id.
* @param rootDisplayAreaId The changed display area Id.
* @param isImmersiveMode {@code true} if the display area get into immersive mode.
+ * @param windowType The window type of the controlling window.
*/
- void immersiveModeChanged(int displayId, int rootDisplayAreaId, boolean isImmersiveMode);
+ void immersiveModeChanged(int displayId, int rootDisplayAreaId, boolean isImmersiveMode,
+ int windowType);
/**
* Show a rotation suggestion that a user may approve to rotate the screen.
diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
index da9d01675984..798c794edaf5 100644
--- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
+++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java
@@ -732,7 +732,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D
@Override
public void immersiveModeChanged(int displayId, int rootDisplayAreaId,
- boolean isImmersiveMode) {
+ boolean isImmersiveMode, int windowType) {
if (mBar == null) {
return;
}
@@ -746,7 +746,7 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D
if (!CLIENT_TRANSIENT) {
// Only call from here when the client transient is not enabled.
try {
- mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode);
+ mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode, windowType);
} catch (RemoteException ex) {
}
}
diff --git a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
index 94e52cd1033a..d4b20fb9bcfc 100644
--- a/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
+++ b/services/core/java/com/android/server/storage/WatchedVolumeInfo.java
@@ -68,6 +68,10 @@ public class WatchedVolumeInfo extends WatchableImpl {
return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo);
}
+ public ImmutableVolumeInfo getClonedImmutableVolumeInfo() {
+ return ImmutableVolumeInfo.fromVolumeInfo(mVolumeInfo.clone());
+ }
+
public StorageVolume buildStorageVolume(Context context, int userId, boolean reportUnmounted) {
return mVolumeInfo.buildStorageVolume(context, userId, reportUnmounted);
}
diff --git a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
index bda3d442956b..621a128a736e 100644
--- a/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
+++ b/services/core/java/com/android/server/vibrator/VendorVibrationSession.java
@@ -51,6 +51,7 @@ import java.util.NoSuchElementException;
final class VendorVibrationSession extends IVibrationSession.Stub
implements VibrationSession, CancellationSignal.OnCancelListener, IBinder.DeathRecipient {
private static final String TAG = "VendorVibrationSession";
+ private static final boolean DEBUG = false;
/** Calls into VibratorManager functionality needed for playing an {@link ExternalVibration}. */
interface VibratorManagerHooks {
@@ -73,8 +74,8 @@ final class VendorVibrationSession extends IVibrationSession.Stub
private final ICancellationSignal mCancellationSignal = CancellationSignal.createTransport();
private final int[] mVibratorIds;
private final long mCreateUptime;
- private final long mCreateTime; // for debugging
- private final IVibrationSessionCallback mCallback;
+ private final long mCreateTime;
+ private final VendorCallbackWrapper mCallback;
private final CallerInfo mCallerInfo;
private final VibratorManagerHooks mManagerHooks;
private final DeviceAdapter mDeviceAdapter;
@@ -88,11 +89,11 @@ final class VendorVibrationSession extends IVibrationSession.Stub
@GuardedBy("mLock")
private boolean mEndedByVendor;
@GuardedBy("mLock")
- private long mStartTime; // for debugging
+ private long mStartTime;
@GuardedBy("mLock")
private long mEndUptime;
@GuardedBy("mLock")
- private long mEndTime; // for debugging
+ private long mEndTime;
@GuardedBy("mLock")
private VibrationStepConductor mConductor;
@@ -103,7 +104,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub
mCreateTime = System.currentTimeMillis();
mVibratorIds = deviceAdapter.getAvailableVibratorIds();
mHandler = handler;
- mCallback = callback;
+ mCallback = new VendorCallbackWrapper(callback, handler);
mCallerInfo = callerInfo;
mManagerHooks = managerHooks;
mDeviceAdapter = deviceAdapter;
@@ -119,7 +120,9 @@ final class VendorVibrationSession extends IVibrationSession.Stub
@Override
public void finishSession() {
- Slog.d(TAG, "Session finish requested, ending vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session finish requested, ending vibration session...");
+ }
// Do not abort session in HAL, wait for ongoing vibration requests to complete.
// This might take a while to end the session, but it can be aborted by cancelSession.
requestEndSession(Status.FINISHED, /* shouldAbort= */ false, /* isVendorRequest= */ true);
@@ -127,7 +130,9 @@ final class VendorVibrationSession extends IVibrationSession.Stub
@Override
public void cancelSession() {
- Slog.d(TAG, "Session cancel requested, aborting vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session cancel requested, aborting vibration session...");
+ }
// Always abort session in HAL while cancelling it.
// This might be triggered after finishSession was already called.
requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true,
@@ -156,7 +161,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub
@Override
public IBinder getCallerToken() {
- return mCallback.asBinder();
+ return mCallback.getBinderToken();
}
@Override
@@ -176,36 +181,30 @@ final class VendorVibrationSession extends IVibrationSession.Stub
@Override
public void onCancel() {
- Slog.d(TAG, "Session cancellation signal received, aborting vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session cancellation signal received, aborting vibration session...");
+ }
requestEndSession(Status.CANCELLED_BY_USER, /* shouldAbort= */ true,
/* isVendorRequest= */ true);
}
@Override
public void binderDied() {
- Slog.d(TAG, "Session binder died, aborting vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session binder died, aborting vibration session...");
+ }
requestEndSession(Status.CANCELLED_BINDER_DIED, /* shouldAbort= */ true,
/* isVendorRequest= */ false);
}
@Override
public boolean linkToDeath() {
- try {
- mCallback.asBinder().linkToDeath(this, 0);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error linking session to token death", e);
- return false;
- }
- return true;
+ return mCallback.linkToDeath(this);
}
@Override
public void unlinkToDeath() {
- try {
- mCallback.asBinder().unlinkToDeath(this, 0);
- } catch (NoSuchElementException e) {
- Slog.wtf(TAG, "Failed to unlink session to token death", e);
- }
+ mCallback.unlinkToDeath(this);
}
@Override
@@ -219,26 +218,37 @@ final class VendorVibrationSession extends IVibrationSession.Stub
@Override
public void notifyVibratorCallback(int vibratorId, long vibrationId, long stepId) {
- Slog.d(TAG, "Vibration callback received for vibration " + vibrationId + " step " + stepId
- + " on vibrator " + vibratorId + ", ignoring...");
+ if (DEBUG) {
+ Slog.d(TAG, "Vibration callback received for vibration " + vibrationId
+ + " step " + stepId + " on vibrator " + vibratorId + ", ignoring...");
+ }
}
@Override
public void notifySyncedVibratorsCallback(long vibrationId) {
- Slog.d(TAG, "Synced vibration callback received for vibration " + vibrationId
- + ", ignoring...");
+ if (DEBUG) {
+ Slog.d(TAG, "Synced vibration callback received for vibration " + vibrationId
+ + ", ignoring...");
+ }
}
@Override
public void notifySessionCallback() {
- Slog.d(TAG, "Session callback received, ending vibration session...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session callback received, ending vibration session...");
+ }
synchronized (mLock) {
// If end was not requested then the HAL has cancelled the session.
- maybeSetEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON,
+ notifyEndRequestLocked(Status.CANCELLED_BY_UNKNOWN_REASON,
/* isVendorRequest= */ false);
maybeSetStatusToRequestedLocked();
clearVibrationConductor();
- mHandler.post(() -> mManagerHooks.onSessionReleased(mSessionId));
+ final Status endStatus = mStatus;
+ mHandler.post(() -> {
+ mManagerHooks.onSessionReleased(mSessionId);
+ // Only trigger client callback after session is released in the manager.
+ mCallback.notifyFinished(endStatus);
+ });
}
}
@@ -271,7 +281,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub
public boolean isEnded() {
synchronized (mLock) {
- return mStatus != Status.RUNNING;
+ return mEndTime > 0;
}
}
@@ -297,19 +307,17 @@ final class VendorVibrationSession extends IVibrationSession.Stub
// Session already ended, skip start callbacks.
isAlreadyEnded = true;
} else {
+ if (DEBUG) {
+ Slog.d(TAG, "Session started at the HAL");
+ }
mStartTime = System.currentTimeMillis();
- // Run client callback in separate thread.
- mHandler.post(() -> {
- try {
- mCallback.onStarted(this);
- } catch (RemoteException e) {
- Slog.e(TAG, "Error notifying vendor session started", e);
- }
- });
+ mCallback.notifyStarted(this);
}
}
if (isAlreadyEnded) {
- Slog.d(TAG, "Session already ended after starting the HAL, aborting...");
+ if (DEBUG) {
+ Slog.d(TAG, "Session already ended after starting the HAL, aborting...");
+ }
mHandler.post(() -> mManagerHooks.endSession(mSessionId, /* shouldAbort= */ true));
}
}
@@ -337,8 +345,10 @@ final class VendorVibrationSession extends IVibrationSession.Stub
public boolean maybeSetVibrationConductor(VibrationStepConductor conductor) {
synchronized (mLock) {
if (mConductor != null) {
- Slog.d(TAG, "Session still dispatching previous vibration, new vibration "
- + conductor.getVibration().id + " ignored");
+ if (DEBUG) {
+ Slog.d(TAG, "Session still dispatching previous vibration, new vibration "
+ + conductor.getVibration().id + " ignored");
+ }
return false;
}
mConductor = conductor;
@@ -347,53 +357,45 @@ final class VendorVibrationSession extends IVibrationSession.Stub
}
private void requestEndSession(Status status, boolean shouldAbort, boolean isVendorRequest) {
- Slog.d(TAG, "Session end request received with status " + status);
- boolean shouldTriggerSessionHook = false;
+ if (DEBUG) {
+ Slog.d(TAG, "Session end request received with status " + status);
+ }
synchronized (mLock) {
- maybeSetEndRequestLocked(status, isVendorRequest);
+ notifyEndRequestLocked(status, isVendorRequest);
if (!isEnded() && isStarted()) {
// Trigger session hook even if it was already triggered, in case a second request
// is aborting the ongoing/ending session. This might cause it to end right away.
// Wait for HAL callback before setting the end status.
- shouldTriggerSessionHook = true;
+ if (DEBUG) {
+ Slog.d(TAG, "Requesting HAL session end with abort=" + shouldAbort);
+ }
+ mHandler.post(() -> mManagerHooks.endSession(mSessionId, shouldAbort));
} else {
- // Session not active in the HAL, set end status right away.
+ // Session not active in the HAL, try to set end status right away.
maybeSetStatusToRequestedLocked();
+ // Use status used to end this session, which might be different from requested.
+ mCallback.notifyFinished(mStatus);
}
}
- if (shouldTriggerSessionHook) {
- Slog.d(TAG, "Requesting HAL session end with abort=" + shouldAbort);
- mHandler.post(() -> mManagerHooks.endSession(mSessionId, shouldAbort));
- }
}
@GuardedBy("mLock")
- private void maybeSetEndRequestLocked(Status status, boolean isVendorRequest) {
+ private void notifyEndRequestLocked(Status status, boolean isVendorRequest) {
if (mEndStatusRequest != null) {
- // End already requested, keep first requested status and time.
+ // End already requested, keep first requested status.
return;
}
- Slog.d(TAG, "Session end request accepted for status " + status);
+ if (DEBUG) {
+ Slog.d(TAG, "Session end request accepted for status " + status);
+ }
mEndStatusRequest = status;
mEndedByVendor = isVendorRequest;
- mEndTime = System.currentTimeMillis();
- mEndUptime = SystemClock.uptimeMillis();
+ mCallback.notifyFinishing();
if (mConductor != null) {
// Vibration is being dispatched when session end was requested, cancel it.
mConductor.notifyCancelled(new Vibration.EndInfo(status),
/* immediate= */ status != Status.FINISHED);
}
- if (isStarted()) {
- // Only trigger "finishing" callback if session started.
- // Run client callback in separate thread.
- mHandler.post(() -> {
- try {
- mCallback.onFinishing();
- } catch (RemoteException e) {
- Slog.e(TAG, "Error notifying vendor session is finishing", e);
- }
- });
- }
}
@GuardedBy("mLock")
@@ -406,40 +408,123 @@ final class VendorVibrationSession extends IVibrationSession.Stub
// No end status was requested, nothing to set.
return;
}
- Slog.d(TAG, "Session end request applied for status " + mEndStatusRequest);
+ if (DEBUG) {
+ Slog.d(TAG, "Session end request applied for status " + mEndStatusRequest);
+ }
mStatus = mEndStatusRequest;
- // Run client callback in separate thread.
- final Status endStatus = mStatus;
- mHandler.post(() -> {
+ mEndTime = System.currentTimeMillis();
+ mEndUptime = SystemClock.uptimeMillis();
+ }
+
+ /**
+ * Wrapper class to handle client callbacks asynchronously.
+ *
+ * <p>This class is also responsible for link/unlink to the client process binder death, and for
+ * making sure the callbacks are only triggered once. The conversion between session status and
+ * the API status code is also defined here.
+ */
+ private static final class VendorCallbackWrapper {
+ private final IVibrationSessionCallback mCallback;
+ private final Handler mHandler;
+
+ private boolean mIsStarted;
+ private boolean mIsFinishing;
+ private boolean mIsFinished;
+
+ VendorCallbackWrapper(@NonNull IVibrationSessionCallback callback,
+ @NonNull Handler handler) {
+ mCallback = callback;
+ mHandler = handler;
+ }
+
+ synchronized IBinder getBinderToken() {
+ return mCallback.asBinder();
+ }
+
+ synchronized boolean linkToDeath(DeathRecipient recipient) {
try {
- mCallback.onFinished(toSessionStatus(endStatus));
+ mCallback.asBinder().linkToDeath(recipient, 0);
} catch (RemoteException e) {
- Slog.e(TAG, "Error notifying vendor session finished", e);
+ Slog.e(TAG, "Error linking session to token death", e);
+ return false;
+ }
+ return true;
+ }
+
+ synchronized void unlinkToDeath(DeathRecipient recipient) {
+ try {
+ mCallback.asBinder().unlinkToDeath(recipient, 0);
+ } catch (NoSuchElementException e) {
+ Slog.wtf(TAG, "Failed to unlink session to token death", e);
+ }
+ }
+
+ synchronized void notifyStarted(IVibrationSession session) {
+ if (mIsStarted) {
+ return;
+ }
+ mIsStarted = true;
+ mHandler.post(() -> {
+ try {
+ mCallback.onStarted(session);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error notifying vendor session started", e);
+ }
+ });
+ }
+
+ synchronized void notifyFinishing() {
+ if (!mIsStarted || mIsFinishing || mIsFinished) {
+ // Ignore if never started or if already finishing or finished.
+ return;
}
- });
- }
-
- @android.os.vibrator.VendorVibrationSession.Status
- private static int toSessionStatus(Status status) {
- // Exhaustive switch to cover all possible internal status.
- return switch (status) {
- case FINISHED
- -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS;
- case IGNORED_UNSUPPORTED
- -> STATUS_UNSUPPORTED;
- case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER,
- CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF,
- CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON
- -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED;
- case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING,
- IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE,
- IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED,
- IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER
- -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED;
- case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING, IGNORED_ERROR_SCHEDULING,
- IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES, FINISHED_UNEXPECTED, RUNNING
- -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR;
- };
+ mIsFinishing = true;
+ mHandler.post(() -> {
+ try {
+ mCallback.onFinishing();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error notifying vendor session is finishing", e);
+ }
+ });
+ }
+
+ synchronized void notifyFinished(Status status) {
+ if (mIsFinished) {
+ return;
+ }
+ mIsFinished = true;
+ mHandler.post(() -> {
+ try {
+ mCallback.onFinished(toSessionStatus(status));
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error notifying vendor session finished", e);
+ }
+ });
+ }
+
+ @android.os.vibrator.VendorVibrationSession.Status
+ private static int toSessionStatus(Status status) {
+ // Exhaustive switch to cover all possible internal status.
+ return switch (status) {
+ case FINISHED
+ -> android.os.vibrator.VendorVibrationSession.STATUS_SUCCESS;
+ case IGNORED_UNSUPPORTED
+ -> STATUS_UNSUPPORTED;
+ case CANCELLED_BINDER_DIED, CANCELLED_BY_APP_OPS, CANCELLED_BY_USER,
+ CANCELLED_SUPERSEDED, CANCELLED_BY_FOREGROUND_USER, CANCELLED_BY_SCREEN_OFF,
+ CANCELLED_BY_SETTINGS_UPDATE, CANCELLED_BY_UNKNOWN_REASON
+ -> android.os.vibrator.VendorVibrationSession.STATUS_CANCELED;
+ case IGNORED_APP_OPS, IGNORED_BACKGROUND, IGNORED_FOR_EXTERNAL, IGNORED_FOR_ONGOING,
+ IGNORED_FOR_POWER, IGNORED_FOR_SETTINGS, IGNORED_FOR_HIGHER_IMPORTANCE,
+ IGNORED_FOR_RINGER_MODE, IGNORED_FROM_VIRTUAL_DEVICE, IGNORED_SUPERSEDED,
+ IGNORED_MISSING_PERMISSION, IGNORED_ON_WIRELESS_CHARGER
+ -> android.os.vibrator.VendorVibrationSession.STATUS_IGNORED;
+ case UNKNOWN, IGNORED_ERROR_APP_OPS, IGNORED_ERROR_CANCELLING,
+ IGNORED_ERROR_SCHEDULING, IGNORED_ERROR_TOKEN, FORWARDED_TO_INPUT_DEVICES,
+ FINISHED_UNEXPECTED, RUNNING
+ -> android.os.vibrator.VendorVibrationSession.STATUS_UNKNOWN_ERROR;
+ };
+ }
}
/**
@@ -499,7 +584,7 @@ final class VendorVibrationSession extends IVibrationSession.Stub
@Override
public void logMetrics(VibratorFrameworkStatsLogger statsLogger) {
if (mStartTime > 0) {
- // Only log sessions that have started.
+ // Only log sessions that have started in the HAL.
statsLogger.logVibrationVendorSessionStarted(mCallerInfo.uid);
statsLogger.logVibrationVendorSessionVibrations(mCallerInfo.uid,
mVibrations.size());
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 124097938ff8..57b82c38c5b8 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2326,13 +2326,16 @@ final class ActivityRecord extends WindowToken {
if (isActivityTypeHome()) {
// The snapshot of home is only used once because it won't be updated while screen
// is on (see {@link TaskSnapshotController#screenTurningOff}).
- mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId);
final Transition transition = mTransitionController.getCollectingTransition();
if (transition != null && (transition.getFlags()
& WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION) == 0) {
+ mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId);
// Only use snapshot of home as starting window when unlocking directly.
return false;
}
+ // Add a reference before removing snapshot from cache.
+ snapshot.addReference(TaskSnapshot.REFERENCE_WRITE_TO_PARCEL);
+ mWmService.mTaskSnapshotController.removeSnapshotCache(task.mTaskId);
}
return createSnapshot(snapshot, typeParameter);
}
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index aed5e140703c..f51e60c101e4 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -144,9 +144,9 @@ public class BackgroundActivityStartController {
.setPendingIntentCreatorBackgroundActivityStartMode(
MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED);
- private ActivityTaskManagerService mService;
+ private final ActivityTaskManagerService mService;
- private ActivityTaskSupervisor mSupervisor;
+ private final ActivityTaskSupervisor mSupervisor;
@GuardedBy("mStrictModeBalCallbacks")
private final SparseArray<ArrayMap<IBinder, IBackgroundActivityLaunchCallback>>
mStrictModeBalCallbacks = new SparseArray<>();
@@ -279,16 +279,24 @@ public class BackgroundActivityStartController {
mSupervisor = supervisor;
}
+ private ActivityTaskManagerService getService() {
+ return mService;
+ }
+
+ private ActivityTaskSupervisor getSupervisor() {
+ return mSupervisor;
+ }
+
private boolean isHomeApp(int uid, @Nullable String packageName) {
- if (mService.mHomeProcess != null) {
+ if (getService().mHomeProcess != null) {
// Fast check
- return uid == mService.mHomeProcess.mUid;
+ return uid == getService().mHomeProcess.mUid;
}
if (packageName == null) {
return false;
}
ComponentName activity =
- mService.getPackageManagerInternalLocked()
+ getService().getPackageManagerInternalLocked()
.getDefaultHomeActivity(UserHandle.getUserId(uid));
return activity != null && packageName.equals(activity.getPackageName());
}
@@ -342,7 +350,8 @@ public class BackgroundActivityStartController {
mAllowBalExemptionForSystemProcess = allowBalExemptionForSystemProcess;
mOriginatingPendingIntent = originatingPendingIntent;
mIntent = intent;
- mRealCallingPackage = mService.getPackageNameIfUnique(realCallingUid, realCallingPid);
+ mRealCallingPackage = getService().getPackageNameIfUnique(realCallingUid,
+ realCallingPid);
mIsCallForResult = resultRecord != null;
mCheckedOptions = checkedOptions;
@BackgroundActivityStartMode int callerBackgroundActivityStartMode =
@@ -401,13 +410,13 @@ public class BackgroundActivityStartController {
checkedOptions, realCallingUid, mRealCallingPackage);
}
- mAppSwitchState = mService.getBalAppSwitchesState();
- mCallingUidProcState = mService.mActiveUids.getUidState(callingUid);
+ mAppSwitchState = getService().getBalAppSwitchesState();
+ mCallingUidProcState = getService().mActiveUids.getUidState(callingUid);
mIsCallingUidPersistentSystemProcess =
mCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
mCallingUidHasVisibleActivity =
- mService.mVisibleActivityProcessTracker.hasVisibleActivity(callingUid);
- mCallingUidHasNonAppVisibleWindow = mService.mActiveUids.hasNonAppVisibleWindow(
+ getService().mVisibleActivityProcessTracker.hasVisibleActivity(callingUid);
+ mCallingUidHasNonAppVisibleWindow = getService().mActiveUids.hasNonAppVisibleWindow(
callingUid);
if (realCallingUid == NO_PROCESS_UID) {
// no process provided
@@ -422,16 +431,17 @@ public class BackgroundActivityStartController {
mRealCallingUidHasNonAppVisibleWindow = mCallingUidHasNonAppVisibleWindow;
// In the PendingIntent case callerApp is not passed in, so resolve it ourselves.
mRealCallerApp = callerApp == null
- ? mService.getProcessController(realCallingPid, realCallingUid)
+ ? getService().getProcessController(realCallingPid, realCallingUid)
: callerApp;
mIsRealCallingUidPersistentSystemProcess = mIsCallingUidPersistentSystemProcess;
} else {
- mRealCallingUidProcState = mService.mActiveUids.getUidState(realCallingUid);
+ mRealCallingUidProcState = getService().mActiveUids.getUidState(realCallingUid);
mRealCallingUidHasVisibleActivity =
- mService.mVisibleActivityProcessTracker.hasVisibleActivity(realCallingUid);
+ getService().mVisibleActivityProcessTracker.hasVisibleActivity(
+ realCallingUid);
mRealCallingUidHasNonAppVisibleWindow =
- mService.mActiveUids.hasNonAppVisibleWindow(realCallingUid);
- mRealCallerApp = mService.getProcessController(realCallingPid, realCallingUid);
+ getService().mActiveUids.hasNonAppVisibleWindow(realCallingUid);
+ mRealCallerApp = getService().getProcessController(realCallingPid, realCallingUid);
mIsRealCallingUidPersistentSystemProcess =
mRealCallingUidProcState <= ActivityManager.PROCESS_STATE_PERSISTENT_UI;
}
@@ -481,7 +491,7 @@ public class BackgroundActivityStartController {
if (uid == 0) {
return "root[debugOnly]";
}
- String name = mService.getPackageManagerInternalLocked().getNameForUid(uid);
+ String name = getService().getPackageManagerInternalLocked().getNameForUid(uid);
if (name == null) {
name = "uid=" + uid;
}
@@ -783,7 +793,7 @@ public class BackgroundActivityStartController {
Process.getAppUidForSdkSandboxUid(state.mRealCallingUid);
// realCallingSdkSandboxUidToAppUid should probably just be used instead (or in addition
// to realCallingUid when calculating resultForRealCaller below.
- if (mService.hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) {
+ if (getService().hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) {
state.setResultForRealCaller(new BalVerdict(BAL_ALLOW_SDK_SANDBOX,
/*background*/ false,
"uid in SDK sandbox has visible (non-toast) window"));
@@ -1000,30 +1010,28 @@ public class BackgroundActivityStartController {
* or {@link #BAL_BLOCK} if the launch should be blocked
*/
BalVerdict checkBackgroundActivityStartAllowedByCaller(BalState state) {
+ boolean evaluateVisibleOnly = balAdditionalStartModes()
+ && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
+ == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
+ if (evaluateVisibleOnly) {
+ return evaluateChain(state, mCheckCallerVisible, mCheckCallerNonAppVisible,
+ mCheckCallerProcessAllowsForeground);
+ }
if (state.isPendingIntent()) {
// PendingIntents should mostly be allowed by the sender (real caller) or a permission
// the creator of the PendingIntent has. Visibility should be the exceptional case, so
// test it last (this does not change the result, just the bal code).
- BalVerdict result = BalVerdict.BLOCK;
- if (!(balAdditionalStartModes()
- && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
- == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) {
- result = checkBackgroundActivityStartAllowedByCallerInBackground(state);
- }
- if (result == BalVerdict.BLOCK) {
- result = checkBackgroundActivityStartAllowedByCallerInForeground(state);
-
- }
- return result;
- } else {
- BalVerdict result = checkBackgroundActivityStartAllowedByCallerInForeground(state);
- if (result == BalVerdict.BLOCK && !(balAdditionalStartModes()
- && state.mCheckedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
- == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) {
- result = checkBackgroundActivityStartAllowedByCallerInBackground(state);
- }
- return result;
+ return evaluateChain(state, mCheckCallerIsAllowlistedUid,
+ mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission,
+ mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp,
+ mCheckCallerProcessAllowsBackground, mCheckCallerVisible,
+ mCheckCallerNonAppVisible, mCheckCallerProcessAllowsForeground);
}
+ return evaluateChain(state, mCheckCallerVisible, mCheckCallerNonAppVisible,
+ mCheckCallerProcessAllowsForeground, mCheckCallerIsAllowlistedUid,
+ mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission,
+ mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp,
+ mCheckCallerProcessAllowsBackground);
}
interface BalExemptionCheck {
@@ -1061,7 +1069,7 @@ public class BackgroundActivityStartController {
if (state.mCallingUidHasNonAppVisibleWindow) {
return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
/*background*/ false, "callingUid has non-app visible window "
- + mService.mActiveUids.getNonAppVisibleWindowDetails(state.mCallingUid));
+ + getService().mActiveUids.getNonAppVisibleWindowDetails(state.mCallingUid));
}
return BalVerdict.BLOCK;
};
@@ -1090,7 +1098,7 @@ public class BackgroundActivityStartController {
final int callingAppId = UserHandle.getAppId(state.mCallingUid);
// IME should always be allowed to start activity, like IME settings.
final WindowState imeWindow =
- mService.mRootWindowContainer.getCurrentInputMethodWindow();
+ getService().mRootWindowContainer.getCurrentInputMethodWindow();
if (imeWindow != null && callingAppId == imeWindow.mOwnerUid) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ false,
@@ -1104,23 +1112,23 @@ public class BackgroundActivityStartController {
}
// don't abort if the caller has the same uid as the recents component
- if (mSupervisor.mRecentTasks.isCallerRecents(state.mCallingUid)) {
+ if (getSupervisor().mRecentTasks.isCallerRecents(state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Recents Component");
}
// don't abort if the callingUid is the device owner
- if (mService.isDeviceOwner(state.mCallingUid)) {
+ if (getService().isDeviceOwner(state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Device Owner");
}
// don't abort if the callingUid is a affiliated profile owner
- if (mService.isAffiliatedProfileOwner(state.mCallingUid)) {
+ if (getService().isAffiliatedProfileOwner(state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Affiliated Profile Owner");
}
// don't abort if the callingUid has companion device
final int callingUserId = UserHandle.getUserId(state.mCallingUid);
- if (mService.isAssociatedCompanionApp(callingUserId, state.mCallingUid)) {
+ if (getService().isAssociatedCompanionApp(callingUserId, state.mCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ true, "Companion App");
}
@@ -1138,7 +1146,7 @@ public class BackgroundActivityStartController {
};
private final BalExemptionCheck mCheckCallerHasSawPermission = state -> {
// don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission
- if (mService.hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid,
+ if (getService().hasSystemAlertWindowPermission(state.mCallingUid, state.mCallingPid,
state.mCallingPackage)) {
return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
/*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted");
@@ -1148,7 +1156,7 @@ public class BackgroundActivityStartController {
private final BalExemptionCheck mCheckCallerHasBgStartAppOp = state -> {
// don't abort if the callingUid and callingPackage have the
// OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION appop
- if (isSystemExemptFlagEnabled() && mService.getAppOpsManager().checkOpNoThrow(
+ if (isSystemExemptFlagEnabled() && getService().getAppOpsManager().checkOpNoThrow(
AppOpsManager.OP_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
state.mCallingUid, state.mCallingPackage) == AppOpsManager.MODE_ALLOWED) {
return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ true,
@@ -1170,34 +1178,18 @@ public class BackgroundActivityStartController {
* @return A code denoting which BAL rule allows an activity to be started,
* or {@link #BAL_BLOCK} if the launch should be blocked
*/
- BalVerdict checkBackgroundActivityStartAllowedByCallerInForeground(BalState state) {
- return evaluateChain(state, mCheckCallerVisible, mCheckCallerNonAppVisible,
- mCheckCallerProcessAllowsForeground);
- }
-
- /**
- * @return A code denoting which BAL rule allows an activity to be started,
- * or {@link #BAL_BLOCK} if the launch should be blocked
- */
- BalVerdict checkBackgroundActivityStartAllowedByCallerInBackground(BalState state) {
- return evaluateChain(state, mCheckCallerIsAllowlistedUid,
- mCheckCallerIsAllowlistedComponent, mCheckCallerHasBackgroundPermission,
- mCheckCallerHasSawPermission, mCheckCallerHasBgStartAppOp,
- mCheckCallerProcessAllowsBackground);
- }
-
- /**
- * @return A code denoting which BAL rule allows an activity to be started,
- * or {@link #BAL_BLOCK} if the launch should be blocked
- */
BalVerdict checkBackgroundActivityStartAllowedByRealCaller(BalState state) {
- BalVerdict result = checkBackgroundActivityStartAllowedByRealCallerInForeground(state);
- if (result == BalVerdict.BLOCK && !(balAdditionalStartModes()
+ boolean evaluateVisibleOnly = balAdditionalStartModes()
&& state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
- == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)) {
- result = checkBackgroundActivityStartAllowedByRealCallerInBackground(state);
+ == MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE;
+ if (evaluateVisibleOnly) {
+ return evaluateChain(state, mCheckRealCallerVisible, mCheckRealCallerNonAppVisible,
+ mCheckRealCallerProcessAllowsBalForeground);
}
- return result;
+ return evaluateChain(state, mCheckRealCallerVisible, mCheckRealCallerNonAppVisible,
+ mCheckRealCallerProcessAllowsBalForeground, mCheckRealCallerBalPermission,
+ mCheckRealCallerSawPermission, mCheckRealCallerAllowlistedUid,
+ mCheckRealCallerAllowlistedComponent, mCheckRealCallerProcessAllowsBalBackground);
}
private final BalExemptionCheck mCheckRealCallerVisible = state -> {
@@ -1218,21 +1210,20 @@ public class BackgroundActivityStartController {
if (state.mRealCallingUidHasNonAppVisibleWindow) {
return new BalVerdict(BAL_ALLOW_NON_APP_VISIBLE_WINDOW,
/*background*/ false, "realCallingUid has non-app visible window "
- + mService.mActiveUids.getNonAppVisibleWindowDetails(state.mRealCallingUid));
+ + getService().mActiveUids.getNonAppVisibleWindowDetails(
+ state.mRealCallingUid));
}
return BalVerdict.BLOCK;
};
- private final BalExemptionCheck mCheckRealCallerProcessAllowsBalForeground = state -> {
- // Don't abort if the realCallerApp or other processes of that uid are considered to be in
- // the foreground.
- return checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_FOREGROUND);
- };
+ // Don't abort if the realCallerApp or other processes of that uid are considered to be in
+ // the foreground.
+ private final BalExemptionCheck mCheckRealCallerProcessAllowsBalForeground =
+ state -> checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_FOREGROUND);
- private final BalExemptionCheck mCheckRealCallerProcessAllowsBalBackground = state -> {
- // don't abort if the callerApp or other processes of that uid are allowed in any way
- return checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_BACKGROUND);
- };
+ // don't abort if the callerApp or other processes of that uid are allowed in any way
+ private final BalExemptionCheck mCheckRealCallerProcessAllowsBalBackground =
+ state -> checkProcessAllowsBal(state.mRealCallerApp, state, BAL_CHECK_BACKGROUND);
private final BalExemptionCheck mCheckRealCallerBalPermission = state -> {
boolean allowAlways = state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode()
@@ -1251,7 +1242,7 @@ public class BackgroundActivityStartController {
== MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS;
// don't abort if the realCallingUid has SYSTEM_ALERT_WINDOW permission
if (allowAlways
- && mService.hasSystemAlertWindowPermission(state.mRealCallingUid,
+ && getService().hasSystemAlertWindowPermission(state.mRealCallingUid,
state.mRealCallingPid, state.mRealCallingPackage)) {
return new BalVerdict(BAL_ALLOW_SAW_PERMISSION,
/*background*/ true, "SYSTEM_ALERT_WINDOW permission is granted");
@@ -1276,7 +1267,7 @@ public class BackgroundActivityStartController {
private final BalExemptionCheck mCheckRealCallerAllowlistedComponent = state -> {
// don't abort if the realCallingUid is an associated companion app
- if (mService.isAssociatedCompanionApp(
+ if (getService().isAssociatedCompanionApp(
UserHandle.getUserId(state.mRealCallingUid), state.mRealCallingUid)) {
return new BalVerdict(BAL_ALLOW_ALLOWLISTED_COMPONENT,
/*background*/ false,
@@ -1285,25 +1276,6 @@ public class BackgroundActivityStartController {
return BalVerdict.BLOCK;
};
- /**
- * @return A code denoting which BAL rule allows an activity to be started,
- * or {@link #BAL_BLOCK} if the launch should be blocked
- */
- BalVerdict checkBackgroundActivityStartAllowedByRealCallerInForeground(BalState state) {
- return evaluateChain(state, mCheckRealCallerVisible, mCheckRealCallerNonAppVisible,
- mCheckRealCallerProcessAllowsBalForeground);
- }
-
- /**
- * @return A code denoting which BAL rule allows an activity to be started,
- * or {@link #BAL_BLOCK} if the launch should be blocked
- */
- BalVerdict checkBackgroundActivityStartAllowedByRealCallerInBackground(BalState state) {
- return evaluateChain(state, mCheckRealCallerBalPermission, mCheckRealCallerSawPermission,
- mCheckRealCallerAllowlistedUid, mCheckRealCallerAllowlistedComponent,
- mCheckRealCallerProcessAllowsBalBackground);
- }
-
@VisibleForTesting boolean hasBalPermission(int uid, int pid) {
return ActivityTaskManagerService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND,
pid, uid) == PERMISSION_GRANTED;
@@ -1329,7 +1301,7 @@ public class BackgroundActivityStartController {
} else {
// only if that one wasn't allowed, check the other ones
final ArraySet<WindowProcessController> uidProcesses =
- mService.mProcessMap.getProcesses(app.mUid);
+ getService().mProcessMap.getProcesses(app.mUid);
if (uidProcesses != null) {
for (int i = uidProcesses.size() - 1; i >= 0; i--) {
final WindowProcessController proc = uidProcesses.valueAt(i);
@@ -1500,7 +1472,7 @@ public class BackgroundActivityStartController {
if (ActivitySecurityModelFeatureFlags.shouldShowToast(callingUid)) {
String toastText = ActivitySecurityModelFeatureFlags.DOC_LINK
+ (enforceBlock ? " blocked " : " would block ")
- + getApplicationLabel(mService.mContext.getPackageManager(),
+ + getApplicationLabel(getService().mContext.getPackageManager(),
launchedFromPackageName);
showToast(toastText);
@@ -1522,7 +1494,7 @@ public class BackgroundActivityStartController {
}
@VisibleForTesting void showToast(String toastText) {
- UiThread.getHandler().post(() -> Toast.makeText(mService.mContext,
+ UiThread.getHandler().post(() -> Toast.makeText(getService().mContext,
toastText, Toast.LENGTH_LONG).show());
}
@@ -1599,7 +1571,7 @@ public class BackgroundActivityStartController {
return;
}
- String packageName = mService.mContext.getPackageManager().getNameForUid(callingUid);
+ String packageName = getService().mContext.getPackageManager().getNameForUid(callingUid);
BalState state = new BalState(callingUid, callingPid, packageName, INVALID_UID,
INVALID_PID, null, null, false, null, null, ActivityOptions.makeBasic());
@BalCode int balCode = checkBackgroundActivityStartAllowedByCaller(state).mCode;
@@ -1660,7 +1632,7 @@ public class BackgroundActivityStartController {
boolean restrictActivitySwitch = ActivitySecurityModelFeatureFlags
.shouldRestrictActivitySwitch(callingUid) && bas.mTopActivityOptedIn;
- PackageManager pm = mService.mContext.getPackageManager();
+ PackageManager pm = getService().mContext.getPackageManager();
String callingPackage = pm.getNameForUid(callingUid);
final CharSequence callingLabel;
if (callingPackage == null) {
@@ -1821,7 +1793,7 @@ public class BackgroundActivityStartController {
return bas.optedIn(ar);
}
- PackageManager pm = mService.mContext.getPackageManager();
+ PackageManager pm = getService().mContext.getPackageManager();
ApplicationInfo applicationInfo;
final int sourceUserId = UserHandle.getUserId(sourceUid);
@@ -1878,7 +1850,7 @@ public class BackgroundActivityStartController {
if (sourceRecord == null) {
joiner.add(prefix + "Source Package: " + targetRecord.launchedFromPackage);
- String realCallingPackage = mService.mContext.getPackageManager().getNameForUid(
+ String realCallingPackage = getService().mContext.getPackageManager().getNameForUid(
realCallingUid);
joiner.add(prefix + "Real Calling Uid Package: " + realCallingPackage);
} else {
@@ -1913,7 +1885,7 @@ public class BackgroundActivityStartController {
joiner.add(prefix + "BalCode: " + balCodeToString(balCode));
joiner.add(prefix + "Allowed By Grace Period: " + allowedByGracePeriod);
joiner.add(prefix + "LastResumedActivity: "
- + recordToString.apply(mService.mLastResumedActivity));
+ + recordToString.apply(getService().mLastResumedActivity));
joiner.add(prefix + "System opted into enforcement: " + asmOptSystemIntoEnforcement());
if (mTopFinishedActivity != null) {
@@ -1986,7 +1958,7 @@ public class BackgroundActivityStartController {
}
private BalVerdict statsLog(BalVerdict finalVerdict, BalState state) {
- if (finalVerdict.blocks() && mService.isActivityStartsLoggingEnabled()) {
+ if (finalVerdict.blocks() && getService().isActivityStartsLoggingEnabled()) {
// log aborted activity start to TRON
mSupervisor
.getActivityMetricsLogger()
@@ -2222,7 +2194,7 @@ public class BackgroundActivityStartController {
return -1;
}
try {
- PackageManager pm = mService.mContext.getPackageManager();
+ PackageManager pm = getService().mContext.getPackageManager();
return pm.getTargetSdkVersion(packageName);
} catch (Exception e) {
return -1;
@@ -2243,8 +2215,8 @@ public class BackgroundActivityStartController {
this.mLaunchCount = entry == null || !ar.isUid(entry.mUid) ? 1 : entry.mLaunchCount + 1;
this.mDebugInfo = getDebugStringForActivityRecord(ar);
- mService.mH.postDelayed(() -> {
- synchronized (mService.mGlobalLock) {
+ getService().mH.postDelayed(() -> {
+ synchronized (getService().mGlobalLock) {
if (mTaskIdToFinishedActivity.get(taskId) == this) {
mTaskIdToFinishedActivity.remove(taskId);
}
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index c2255d8d011a..dc42b32967e2 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -79,7 +79,7 @@ public final class DesktopModeHelper {
}
@VisibleForTesting
- static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
+ public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) {
if (!shouldEnforceDeviceRestrictions()) {
return true;
}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index a874ef6039f9..50f12c305587 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -157,6 +157,7 @@ import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFi
import static com.android.server.wm.utils.RegionUtils.forEachRectReverse;
import static com.android.server.wm.utils.RegionUtils.rectListToRegion;
import static com.android.window.flags.Flags.enablePersistingDensityScaleForConnectedDisplays;
+import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -3835,13 +3836,18 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
/**
* Looking for the focused window on this display if the top focused display hasn't been
- * found yet (topFocusedDisplayId is INVALID_DISPLAY) or per-display focused was allowed.
+ * found yet (topFocusedDisplayId is INVALID_DISPLAY), per-display focused was allowed, or
+ * the display is presenting. The last one is needed to update system bar visibility in response
+ * to presentation visibility because per-display focus is needed to change system bar
+ * visibility, but the display shouldn't get global focus when a presentation gets shown.
*
* @param topFocusedDisplayId Id of the top focused display.
* @return The focused window or null if there isn't any or no need to seek.
*/
WindowState findFocusedWindowIfNeeded(int topFocusedDisplayId) {
- return (hasOwnFocus() || topFocusedDisplayId == INVALID_DISPLAY)
+ return (hasOwnFocus() || topFocusedDisplayId == INVALID_DISPLAY
+ || (enablePresentationForConnectedDisplays()
+ && mWmService.mPresentationController.isPresentationVisible(mDisplayId)))
? findFocusedWindow() : null;
}
@@ -6932,6 +6938,8 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
/** The actual requested visible inset types for this display */
private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
+ private @InsetsType int mAnimatingTypes = 0;
+
/** The component name of the top focused window on this display */
private ComponentName mTopFocusedComponentName = null;
@@ -7069,6 +7077,18 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
}
return 0;
}
+
+ @Override
+ public @InsetsType int getAnimatingTypes() {
+ return mAnimatingTypes;
+ }
+
+ @Override
+ public void setAnimatingTypes(@InsetsType int animatingTypes) {
+ if (mAnimatingTypes != animatingTypes) {
+ mAnimatingTypes = animatingTypes;
+ }
+ }
}
MagnificationSpec getMagnificationSpec() {
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 4908df025dd1..ec5b503fbb9b 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -2564,7 +2564,7 @@ public class DisplayPolicy {
final int rootDisplayAreaId = root == null ? FEATURE_UNDEFINED : root.mFeatureId;
// TODO(b/277290737): Move this to the client side, instead of using a proxy.
callStatusBarSafely(statusBar -> statusBar.immersiveModeChanged(getDisplayId(),
- rootDisplayAreaId, isImmersiveMode));
+ rootDisplayAreaId, isImmersiveMode, win.getWindowType()));
}
// Show transient bars for panic if needed.
diff --git a/services/core/java/com/android/server/wm/InsetsControlTarget.java b/services/core/java/com/android/server/wm/InsetsControlTarget.java
index cee49676eeae..6462a37ae33f 100644
--- a/services/core/java/com/android/server/wm/InsetsControlTarget.java
+++ b/services/core/java/com/android/server/wm/InsetsControlTarget.java
@@ -97,6 +97,20 @@ interface InsetsControlTarget extends InsetsTarget {
@NonNull ImeTracker.Token statsToken) {
}
+ /**
+ * @return {@link WindowInsets.Type.InsetsType}s which are currently animating (showing or
+ * hiding).
+ */
+ default @InsetsType int getAnimatingTypes() {
+ return 0;
+ }
+
+ /**
+ * @param animatingTypes the {@link InsetsType}s, that are currently animating
+ */
+ default void setAnimatingTypes(@InsetsType int animatingTypes) {
+ }
+
/** Returns {@code target.getWindow()}, or null if {@code target} is {@code null}. */
static WindowState asWindowOrNull(InsetsControlTarget target) {
return target != null ? target.getWindow() : null;
diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java
index 009d482ba316..28722141dcd3 100644
--- a/services/core/java/com/android/server/wm/InsetsPolicy.java
+++ b/services/core/java/com/android/server/wm/InsetsPolicy.java
@@ -790,8 +790,6 @@ class InsetsPolicy {
private final Handler mHandler;
private final String mName;
- private boolean mInsetsAnimationRunning;
-
Host(Handler handler, String name) {
mHandler = handler;
mName = name;
@@ -901,10 +899,5 @@ class InsetsPolicy {
public IBinder getWindowToken() {
return null;
}
-
- @Override
- public void notifyAnimationRunningStateChanged(boolean running) {
- mInsetsAnimationRunning = running;
- }
}
}
diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java
index 164abab992d8..5e0395f70e65 100644
--- a/services/core/java/com/android/server/wm/InsetsStateController.java
+++ b/services/core/java/com/android/server/wm/InsetsStateController.java
@@ -225,13 +225,16 @@ class InsetsStateController {
for (int i = mProviders.size() - 1; i >= 0; i--) {
final InsetsSourceProvider provider = mProviders.valueAt(i);
final @InsetsType int type = provider.getSource().getType();
+ final boolean isImeProvider = type == WindowInsets.Type.ime();
if ((type & changedTypes) != 0) {
- final boolean isImeProvider = type == WindowInsets.Type.ime();
changed |= provider.updateClientVisibility(
- caller, isImeProvider ? statsToken : null)
+ caller, isImeProvider ? statsToken : null)
// Fake control target cannot change the client visibility, but it should
// change the insets with its newly requested visibility.
|| (caller == provider.getFakeControlTarget());
+ } else if (isImeProvider && android.view.inputmethod.Flags.refactorInsetsController()) {
+ ImeTracker.forLogging().onCancelled(statsToken,
+ ImeTracker.PHASE_WM_SET_REMOTE_TARGET_IME_VISIBILITY);
}
}
if (changed) {
diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java
index b3cff9c6cc3d..acc658bf635e 100644
--- a/services/core/java/com/android/server/wm/PresentationController.java
+++ b/services/core/java/com/android/server/wm/PresentationController.java
@@ -16,10 +16,17 @@
package com.android.server.wm;
+import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
+import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;
+
+import static com.android.internal.protolog.WmProtoLogGroups.WM_ERROR;
import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;
import android.annotation.NonNull;
-import android.util.IntArray;
+import android.annotation.Nullable;
+import android.hardware.display.DisplayManager;
+import android.util.SparseArray;
+import android.view.WindowManager.LayoutParams.WindowType;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.protolog.WmProtoLogGroups;
@@ -27,15 +34,125 @@ import com.android.internal.protolog.WmProtoLogGroups;
/**
* Manages presentation windows.
*/
-class PresentationController {
+class PresentationController implements DisplayManager.DisplayListener {
+
+ private static class Presentation {
+ @NonNull final WindowState mWin;
+ @NonNull final WindowContainerListener mPresentationListener;
+ // This is the task which started this presentation. This shouldn't be null in most cases
+ // because the intended usage of the Presentation API is that an activity that started a
+ // presentation should control the UI and lifecycle of the presentation window.
+ // However, the API doesn't necessarily requires a host activity to exist (e.g. a background
+ // service can launch a presentation), so this can be null.
+ @Nullable final Task mHostTask;
+ @Nullable final WindowContainerListener mHostTaskListener;
+
+ Presentation(@NonNull WindowState win,
+ @NonNull WindowContainerListener presentationListener,
+ @Nullable Task hostTask,
+ @Nullable WindowContainerListener hostTaskListener) {
+ mWin = win;
+ mPresentationListener = presentationListener;
+ mHostTask = hostTask;
+ mHostTaskListener = hostTaskListener;
+ }
+
+ @Override
+ public String toString() {
+ return "{win: " + mWin.getName() + ", display: " + mWin.getDisplayId()
+ + ", hostTask: " + (mHostTask != null ? mHostTask.getName() : null) + "}";
+ }
+ }
+
+ private final SparseArray<Presentation> mPresentations = new SparseArray();
+
+ @Nullable
+ private Presentation getPresentation(@Nullable WindowState win) {
+ if (win == null) return null;
+ for (int i = 0; i < mPresentations.size(); i++) {
+ final Presentation presentation = mPresentations.valueAt(i);
+ if (win == presentation.mWin) return presentation;
+ }
+ return null;
+ }
- // TODO(b/395475549): Add support for display add/remove, and activity move across displays.
- private final IntArray mPresentingDisplayIds = new IntArray();
+ private boolean hasPresentationWindow(int displayId) {
+ return mPresentations.contains(displayId);
+ }
- PresentationController() {}
+ boolean isPresentationVisible(int displayId) {
+ final Presentation presentation = mPresentations.get(displayId);
+ return presentation != null && presentation.mWin.mToken.isVisibleRequested();
+ }
- private boolean isPresenting(int displayId) {
- return mPresentingDisplayIds.contains(displayId);
+ boolean canPresent(@NonNull WindowState win, @NonNull DisplayContent displayContent) {
+ return canPresent(win, displayContent, win.mAttrs.type, win.getUid());
+ }
+
+ /**
+ * Checks if a presentation window can be shown on the given display.
+ * If the given |win| is empty, a new presentation window is being created.
+ * If the given |win| is not empty, the window already exists as presentation, and we're
+ * revalidate if the |win| is still qualified to be shown.
+ */
+ boolean canPresent(@Nullable WindowState win, @NonNull DisplayContent displayContent,
+ @WindowType int type, int uid) {
+ if (type == TYPE_PRIVATE_PRESENTATION) {
+ // Private presentations can only be created on private displays.
+ return displayContent.isPrivate();
+ }
+
+ if (type != TYPE_PRESENTATION) {
+ return false;
+ }
+
+ if (!enablePresentationForConnectedDisplays()) {
+ return displayContent.getDisplay().isPublicPresentation();
+ }
+
+ boolean allDisplaysArePresenting = true;
+ for (int i = 0; i < displayContent.mWmService.mRoot.mChildren.size(); i++) {
+ final DisplayContent dc = displayContent.mWmService.mRoot.mChildren.get(i);
+ if (displayContent.mDisplayId != dc.mDisplayId
+ && !mPresentations.contains(dc.mDisplayId)) {
+ allDisplaysArePresenting = false;
+ break;
+ }
+ }
+ if (allDisplaysArePresenting) {
+ // All displays can't present simultaneously.
+ return false;
+ }
+
+ final int displayId = displayContent.mDisplayId;
+ if (hasPresentationWindow(displayId)
+ && win != null && win != mPresentations.get(displayId).mWin) {
+ // A display can't have multiple presentations.
+ return false;
+ }
+
+ Task hostTask = null;
+ final Presentation presentation = getPresentation(win);
+ if (presentation != null) {
+ hostTask = presentation.mHostTask;
+ } else if (win == null) {
+ final Task globallyFocusedTask =
+ displayContent.mWmService.mRoot.getTopDisplayFocusedRootTask();
+ if (globallyFocusedTask != null && uid == globallyFocusedTask.effectiveUid) {
+ hostTask = globallyFocusedTask;
+ }
+ }
+ if (hostTask != null && displayId == hostTask.getDisplayId()) {
+ // A presentation can't cover its own host task.
+ return false;
+ }
+ if (hostTask == null && !displayContent.getDisplay().isPublicPresentation()) {
+ // A globally focused host task on a different display is needed to show a
+ // presentation on a non-presenting display.
+ return false;
+ }
+
+ return true;
}
boolean shouldOccludeActivities(int displayId) {
@@ -45,32 +162,87 @@ class PresentationController {
// be shown on them.
// TODO(b/390481621): Disallow a presentation from covering its controlling activity so that
// the presentation won't stop its controlling activity.
- return enablePresentationForConnectedDisplays() && isPresenting(displayId);
+ return enablePresentationForConnectedDisplays() && isPresentationVisible(displayId);
}
- void onPresentationAdded(@NonNull WindowState win) {
+ void onPresentationAdded(@NonNull WindowState win, int uid) {
final int displayId = win.getDisplayId();
- if (isPresenting(displayId)) {
- return;
- }
ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Presentation added to display %d: %s",
- win.getDisplayId(), win);
- mPresentingDisplayIds.add(win.getDisplayId());
+ displayId, win);
win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true);
- }
- void onPresentationRemoved(@NonNull WindowState win) {
- final int displayId = win.getDisplayId();
- if (!isPresenting(displayId)) {
- return;
+ final WindowContainerListener presentationWindowListener = new WindowContainerListener() {
+ @Override
+ public void onRemoved() {
+ if (!hasPresentationWindow(displayId)) {
+ ProtoLog.e(WM_ERROR, "Failed to remove presentation on"
+ + "non-presenting display %d: %s", displayId, win);
+ return;
+ }
+ final Presentation presentation = mPresentations.get(displayId);
+ win.mToken.unregisterWindowContainerListener(presentation.mPresentationListener);
+ if (presentation.mHostTask != null) {
+ presentation.mHostTask.unregisterWindowContainerListener(
+ presentation.mHostTaskListener);
+ }
+ mPresentations.remove(displayId);
+ win.mWmService.mDisplayManagerInternal.onPresentation(displayId, false /*isShown*/);
+ }
+ };
+ win.mToken.registerWindowContainerListener(presentationWindowListener);
+
+ Task hostTask = null;
+ if (enablePresentationForConnectedDisplays()) {
+ final Task globallyFocusedTask =
+ win.mWmService.mRoot.getTopDisplayFocusedRootTask();
+ if (globallyFocusedTask != null && uid == globallyFocusedTask.effectiveUid) {
+ hostTask = globallyFocusedTask;
+ }
+ }
+ WindowContainerListener hostTaskListener = null;
+ if (hostTask != null) {
+ hostTaskListener = new WindowContainerListener() {
+ public void onDisplayChanged(DisplayContent dc) {
+ final Presentation presentation = mPresentations.get(dc.getDisplayId());
+ if (presentation != null && !canPresent(presentation.mWin, dc)) {
+ removePresentation(dc.mDisplayId, "host task moved to display "
+ + dc.getDisplayId());
+ }
+ }
+
+ public void onRemoved() {
+ removePresentation(win.getDisplayId(), "host task removed");
+ }
+ };
+ hostTask.registerWindowContainerListener(hostTaskListener);
}
- ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION,
- "Presentation removed from display %d: %s", win.getDisplayId(), win);
- // TODO(b/393945496): Make sure that there's one presentation at most per display.
- final int displayIdIndex = mPresentingDisplayIds.indexOf(displayId);
- if (displayIdIndex != -1) {
- mPresentingDisplayIds.remove(displayIdIndex);
+
+ mPresentations.put(displayId, new Presentation(win, presentationWindowListener, hostTask,
+ hostTaskListener));
+ }
+
+ void removePresentation(int displayId, @NonNull String reason) {
+ final Presentation presentation = mPresentations.get(displayId);
+ if (enablePresentationForConnectedDisplays() && presentation != null) {
+ ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Removing Presentation %s for "
+ + "reason %s", mPresentations.get(displayId), reason);
+ final WindowState win = presentation.mWin;
+ win.mWmService.mAtmService.mH.post(() -> {
+ synchronized (win.mWmService.mGlobalLock) {
+ win.removeIfPossible();
+ }
+ });
}
- win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false);
}
+
+ @Override
+ public void onDisplayAdded(int displayId) {}
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ removePresentation(displayId, "display removed " + displayId);
+ }
+
+ @Override
+ public void onDisplayChanged(int displayId) {}
}
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index 8d198b26f396..3ed16db7e204 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -737,6 +737,17 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
}
}
+ @Override
+ public void updateAnimatingTypes(IWindow window, @InsetsType int animatingTypes) {
+ synchronized (mService.mGlobalLock) {
+ final WindowState win = mService.windowForClientLocked(this, window,
+ false /* throwOnError */);
+ if (win != null) {
+ win.setAnimatingTypes(animatingTypes);
+ }
+ }
+ }
+
void onWindowAdded(WindowState w) {
if (mPackageName == null) {
mPackageName = mProcess.mInfo.packageName;
@@ -1015,15 +1026,4 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
}
}
}
-
- @Override
- public void notifyInsetsAnimationRunningStateChanged(IWindow window, boolean running) {
- synchronized (mService.mGlobalLock) {
- final WindowState win = mService.windowForClientLocked(this, window,
- false /* throwOnError */);
- if (win != null) {
- win.notifyInsetsAnimationRunningStateChanged(running);
- }
- }
- }
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 821c04011d97..28f2825150c2 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1583,14 +1583,18 @@ public class WindowManagerService extends IWindowManager.Stub
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
- if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) {
+ if (type == TYPE_PRIVATE_PRESENTATION
+ && !mPresentationController.canPresent(null /*win*/, displayContent, type,
+ callingUid)) {
ProtoLog.w(WM_ERROR,
"Attempted to add private presentation window to a non-private display. "
+ "Aborting.");
return WindowManagerGlobal.ADD_PERMISSION_DENIED;
}
- if (type == TYPE_PRESENTATION && !displayContent.getDisplay().isPublicPresentation()) {
+ if (type == TYPE_PRESENTATION
+ && !mPresentationController.canPresent(null /*win*/, displayContent, type,
+ callingUid)) {
ProtoLog.w(WM_ERROR,
"Attempted to add presentation window to a non-suitable display. "
+ "Aborting.");
@@ -1830,7 +1834,8 @@ public class WindowManagerService extends IWindowManager.Stub
}
win.mTransitionController.collect(win.mToken);
res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState,
- outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs);
+ outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs,
+ callingUid);
// A presentation hides all activities behind on the same display.
win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
/*notifyClients=*/ true);
@@ -1841,7 +1846,8 @@ public class WindowManagerService extends IWindowManager.Stub
}
} else {
res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState,
- outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs);
+ outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs,
+ callingUid);
}
}
@@ -1854,7 +1860,7 @@ public class WindowManagerService extends IWindowManager.Stub
@NonNull ActivityRecord activity, @NonNull DisplayContent displayContent,
@NonNull InsetsState outInsetsState, @NonNull Rect outAttachedFrame,
@NonNull InsetsSourceControl.Array outActiveControls, @NonNull IWindow client,
- @NonNull float[] outSizeCompatScale, @NonNull LayoutParams attrs) {
+ @NonNull float[] outSizeCompatScale, @NonNull LayoutParams attrs, int uid) {
int res = 0;
final int type = attrs.type;
boolean imMayMove = true;
@@ -1971,7 +1977,7 @@ public class WindowManagerService extends IWindowManager.Stub
outSizeCompatScale[0] = win.getCompatScaleForClient();
if (res >= ADD_OKAY && win.isPresentation()) {
- mPresentationController.onPresentationAdded(win);
+ mPresentationController.onPresentationAdded(win, uid);
}
return res;
@@ -4767,6 +4773,26 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
+ @EnforcePermission(android.Manifest.permission.MANAGE_APP_TOKENS)
+ @Override
+ public void updateDisplayWindowAnimatingTypes(int displayId, @InsetsType int animatingTypes) {
+ updateDisplayWindowAnimatingTypes_enforcePermission();
+ if (android.view.inputmethod.Flags.reportAnimatingInsetsTypes()) {
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ synchronized (mGlobalLock) {
+ final DisplayContent dc = mRoot.getDisplayContent(displayId);
+ if (dc == null || dc.mRemoteInsetsControlTarget == null) {
+ return;
+ }
+ dc.mRemoteInsetsControlTarget.setAnimatingTypes(animatingTypes);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+ }
+
@Override
public int watchRotation(IRotationWatcher watcher, int displayId) {
final DisplayContent displayContent;
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index ec67dd87533a..3b7d31274326 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -736,6 +736,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
+ private @InsetsType int mAnimatingTypes = 0;
+
/**
* Freeze the insets state in some cases that not necessarily keeps up-to-date to the client.
* (e.g app exiting transition)
@@ -842,6 +844,27 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mRequestedVisibleTypes & ~mask | requestedVisibleTypes & mask);
}
+ @Override
+ public @InsetsType int getAnimatingTypes() {
+ return mAnimatingTypes;
+ }
+
+ @Override
+ public void setAnimatingTypes(@InsetsType int animatingTypes) {
+ if (mAnimatingTypes != animatingTypes) {
+ if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+ Trace.instant(TRACE_TAG_WINDOW_MANAGER,
+ TextUtils.formatSimple("%s: setAnimatingTypes(%s)",
+ getName(),
+ animatingTypes));
+ }
+ mInsetsAnimationRunning = animatingTypes != 0;
+ mWmService.scheduleAnimationLocked();
+
+ mAnimatingTypes = animatingTypes;
+ }
+ }
+
/**
* Set a freeze state for the window to ignore dispatching its insets state to the client.
*
@@ -2435,7 +2458,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mAnimatingExit = true;
mRemoveOnExit = true;
mToken.setVisibleRequested(false);
- mWmService.mPresentationController.onPresentationRemoved(this);
// A presentation hides all activities behind on the same display.
mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
/*notifyClients=*/ true);
@@ -2656,7 +2678,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// The client gave us a touchable region and so first
// we calculate the untouchable region, then punch that out of our
// expanded modal region.
- mTmpRegion.set(0, 0, frame.right, frame.bottom);
+ mTmpRegion.set(0, 0, frame.width(), frame.height());
mTmpRegion.op(mGivenTouchableRegion, Region.Op.DIFFERENCE);
region.op(mTmpRegion, Region.Op.DIFFERENCE);
}
@@ -6079,17 +6101,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mWmService.scheduleAnimationLocked();
}
- void notifyInsetsAnimationRunningStateChanged(boolean running) {
- if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
- Trace.instant(TRACE_TAG_WINDOW_MANAGER,
- TextUtils.formatSimple("%s: notifyInsetsAnimationRunningStateChanged(%s)",
- getName(),
- Boolean.toString(running)));
- }
- mInsetsAnimationRunning = running;
- mWmService.scheduleAnimationLocked();
- }
-
boolean isInsetsAnimationRunning() {
return mInsetsAnimationRunning;
}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index e158310455ac..860b6fb1dcd1 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1814,7 +1814,7 @@ public final class SystemServer implements Dumpable {
t.traceEnd();
}
- if (!isWatch && !isTv && !isAutomotive
+ if (!isWatch && !isTv && !isAutomotive && !isDesktop
&& android.security.Flags.aapmApi()) {
t.traceBegin("StartAdvancedProtectionService");
mSystemServiceManager.startService(AdvancedProtectionService.Lifecycle.class);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
index 5eb23a24908d..12866481b320 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BaseBroadcastQueueTest.java
@@ -16,29 +16,43 @@
package com.android.server.am;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import android.annotation.NonNull;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.app.BackgroundStartPrivileges;
+import android.app.BroadcastOptions;
+import android.app.SystemServiceRegistry;
import android.app.usage.UsageStatsManagerInternal;
import android.content.ComponentName;
import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
+import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.TestLooperManager;
import android.os.UserHandle;
+import android.permission.IPermissionManager;
+import android.permission.PermissionManager;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.platform.test.flag.junit.SetFlagsRule;
@@ -47,7 +61,6 @@ import android.util.SparseArray;
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.internal.util.FrameworkStatsLog;
import com.android.modules.utils.testing.ExtendedMockitoRule;
import com.android.server.AlarmManagerInternal;
@@ -55,6 +68,7 @@ import com.android.server.DropBoxManagerInternal;
import com.android.server.LocalServices;
import com.android.server.appop.AppOpsService;
import com.android.server.compat.PlatformCompat;
+import com.android.server.firewall.IntentFirewall;
import com.android.server.wm.ActivityTaskManagerService;
import org.junit.Rule;
@@ -63,8 +77,11 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.File;
+import java.util.Collections;
+import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
public abstract class BaseBroadcastQueueTest {
@@ -97,6 +114,8 @@ public abstract class BaseBroadcastQueueTest {
public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
.spyStatic(FrameworkStatsLog.class)
.spyStatic(ProcessList.class)
+ .spyStatic(SystemServiceRegistry.class)
+ .mockStatic(AppGlobals.class)
.build();
@@ -119,6 +138,16 @@ public abstract class BaseBroadcastQueueTest {
ProcessList mProcessList;
@Mock
PlatformCompat mPlatformCompat;
+ @Mock
+ IntentFirewall mIntentFirewall;
+ @Mock
+ IPackageManager mIPackageManager;
+ @Mock
+ AppOpsManager mAppOpsManager;
+ @Mock
+ IPermissionManager mIPermissionManager;
+ @Mock
+ PermissionManager mPermissionManager;
@Mock
AppStartInfoTracker mAppStartInfoTracker;
@@ -167,22 +196,22 @@ public abstract class BaseBroadcastQueueTest {
return getUidForPackage(invocation.getArgument(0));
}).when(mPackageManagerInt).getPackageUid(any(), anyLong(), eq(UserHandle.USER_SYSTEM));
+ final Context spyContext = spy(mContext);
+ doReturn(mPermissionManager).when(spyContext).getSystemService(PermissionManager.class);
final ActivityManagerService realAms = new ActivityManagerService(
- new TestInjector(mContext), mServiceThreadRule.getThread());
+ new TestInjector(spyContext), mServiceThreadRule.getThread());
realAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
realAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
realAms.mAtmInternal = spy(realAms.mActivityTaskManager.getAtmInternal());
realAms.mOomAdjuster.mCachedAppOptimizer = mock(CachedAppOptimizer.class);
realAms.mOomAdjuster = spy(realAms.mOomAdjuster);
- ExtendedMockito.doNothing().when(() -> ProcessList.setOomAdj(anyInt(), anyInt(), anyInt()));
+ doNothing().when(() -> ProcessList.setOomAdj(anyInt(), anyInt(), anyInt()));
realAms.mPackageManagerInt = mPackageManagerInt;
realAms.mUsageStatsService = mUsageStatsManagerInt;
realAms.mProcessesReady = true;
mAms = spy(realAms);
- mSkipPolicy = spy(new BroadcastSkipPolicy(mAms));
- doReturn(null).when(mSkipPolicy).shouldSkipMessage(any(), any());
- doReturn(false).when(mSkipPolicy).disallowBackgroundStart(any());
+ mSkipPolicy = createBroadcastSkipPolicy();
doReturn(mAppStartInfoTracker).when(mProcessList).getAppStartInfoTracker();
@@ -198,6 +227,14 @@ public abstract class BaseBroadcastQueueTest {
}
}
+ public BroadcastSkipPolicy createBroadcastSkipPolicy() {
+ final BroadcastSkipPolicy skipPolicy = spy(new BroadcastSkipPolicy(mAms));
+ doReturn(null).when(skipPolicy).shouldSkipAtEnqueueMessage(any(), any());
+ doReturn(null).when(skipPolicy).shouldSkipMessage(any(), any());
+ doReturn(false).when(skipPolicy).disallowBackgroundStart(any());
+ return skipPolicy;
+ }
+
static int getUidForPackage(@NonNull String packageName) {
switch (packageName) {
case PACKAGE_ANDROID: return android.os.Process.SYSTEM_UID;
@@ -240,6 +277,11 @@ public abstract class BaseBroadcastQueueTest {
public BroadcastQueue getBroadcastQueue(ActivityManagerService service) {
return null;
}
+
+ @Override
+ public IntentFirewall getIntentFirewall() {
+ return mIntentFirewall;
+ }
}
abstract String getTag();
@@ -281,24 +323,35 @@ public abstract class BaseBroadcastQueueTest {
ri.activityInfo.packageName = packageName;
ri.activityInfo.processName = processName;
ri.activityInfo.name = name;
+ ri.activityInfo.exported = true;
ri.activityInfo.applicationInfo = makeApplicationInfo(packageName, processName, userId);
return ri;
}
+ // TODO: Reuse BroadcastQueueTest.makeActiveProcessRecord()
+ @SuppressWarnings("GuardedBy")
+ ProcessRecord makeProcessRecord(ApplicationInfo info) {
+ final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid));
+ r.setPid(mNextPid.incrementAndGet());
+ ProcessRecord.updateProcessRecordNodes(r);
+ return r;
+ }
+
BroadcastFilter makeRegisteredReceiver(ProcessRecord app) {
return makeRegisteredReceiver(app, 0);
}
BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
final ReceiverList receiverList = mRegisteredReceivers.get(app.getPid());
- return makeRegisteredReceiver(receiverList, priority);
+ return makeRegisteredReceiver(receiverList, priority, null);
}
- static BroadcastFilter makeRegisteredReceiver(ReceiverList receiverList, int priority) {
+ static BroadcastFilter makeRegisteredReceiver(ReceiverList receiverList, int priority,
+ String requiredPermission) {
final IntentFilter filter = new IntentFilter();
filter.setPriority(priority);
final BroadcastFilter res = new BroadcastFilter(filter, receiverList,
- receiverList.app.info.packageName, null, null, null, receiverList.uid,
+ receiverList.app.info.packageName, null, null, requiredPermission, receiverList.uid,
receiverList.userId, false, false, true, receiverList.app.info,
mock(PlatformCompat.class));
receiverList.add(res);
@@ -313,4 +366,62 @@ public abstract class BaseBroadcastQueueTest {
ArgumentMatcher<ApplicationInfo> appInfoEquals(int uid) {
return test -> (test.uid == uid);
}
+
+ static final class BroadcastRecordBuilder {
+ private BroadcastQueue mQueue = mock(BroadcastQueue.class);
+ private Intent mIntent = mock(Intent.class);
+ private ProcessRecord mProcessRecord = mock(ProcessRecord.class);
+ private String mCallerPackage;
+ private String mCallerFeatureId;
+ private int mCallingPid;
+ private int mCallingUid;
+ private boolean mCallerInstantApp;
+ private String mResolvedType;
+ private String[] mRequiredPermissions;
+ private String[] mExcludedPermissions;
+ private String[] mExcludedPackages;
+ private int mAppOp;
+ private BroadcastOptions mOptions = BroadcastOptions.makeBasic();
+ private List mReceivers = Collections.emptyList();
+ private ProcessRecord mResultToApp;
+ private IIntentReceiver mResultTo;
+ private int mResultCode = Activity.RESULT_OK;
+ private String mResultData;
+ private Bundle mResultExtras;
+ private boolean mSerialized;
+ private boolean mSticky;
+ private boolean mInitialSticky;
+ private int mUserId = UserHandle.USER_SYSTEM;
+ private BackgroundStartPrivileges mBackgroundStartPrivileges =
+ BackgroundStartPrivileges.NONE;
+ private boolean mTimeoutExempt;
+ private BiFunction<Integer, Bundle, Bundle> mFilterExtrasForReceiver;
+ private int mCallerAppProcState = ActivityManager.PROCESS_STATE_UNKNOWN;
+ private PlatformCompat mPlatformCompat = mock(PlatformCompat.class);
+
+ public BroadcastRecordBuilder setIntent(Intent intent) {
+ mIntent = intent;
+ return this;
+ }
+
+ public BroadcastRecordBuilder setRequiredPermissions(String[] requiredPermissions) {
+ mRequiredPermissions = requiredPermissions;
+ return this;
+ }
+
+ public BroadcastRecordBuilder setAppOp(int appOp) {
+ mAppOp = appOp;
+ return this;
+ }
+
+ public BroadcastRecord build() {
+ return new BroadcastRecord(mQueue, mIntent, mProcessRecord, mCallerPackage,
+ mCallerFeatureId, mCallingPid, mCallingUid, mCallerInstantApp, mResolvedType,
+ mRequiredPermissions, mExcludedPermissions, mExcludedPackages, mAppOp,
+ mOptions, mReceivers, mResultToApp, mResultTo, mResultCode, mResultData,
+ mResultExtras, mSerialized, mSticky, mInitialSticky, mUserId,
+ mBackgroundStartPrivileges, mTimeoutExempt, mFilterExtrasForReceiver,
+ mCallerAppProcState, mPlatformCompat);
+ }
+ }
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
index 409706b14c56..b32ce49d049d 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueImplTest.java
@@ -1803,8 +1803,10 @@ public final class BroadcastQueueImplTest extends BaseBroadcastQueueTest {
assertEquals(ProcessList.SCHED_GROUP_DEFAULT, queue.getPreferredSchedulingGroupLocked());
}
+ @SuppressWarnings("GuardedBy")
+ @DisableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE)
@Test
- public void testSkipPolicy_atEnqueueTime() throws Exception {
+ public void testSkipPolicy_atEnqueueTime_flagDisabled() throws Exception {
final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT);
final Object greenReceiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN);
final Object redReceiver = makeManifestReceiver(PACKAGE_RED, CLASS_RED);
@@ -1839,6 +1841,44 @@ public final class BroadcastQueueImplTest extends BaseBroadcastQueueTest {
verifyPendingRecords(redQueue, List.of(userPresent, timeTick));
}
+ @SuppressWarnings("GuardedBy")
+ @EnableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE)
+ @Test
+ public void testSkipPolicy_atEnqueueTime() throws Exception {
+ final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT);
+ final Object greenReceiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN);
+ final Object redReceiver = makeManifestReceiver(PACKAGE_RED, CLASS_RED);
+
+ final BroadcastRecord userPresentRecord = makeBroadcastRecord(userPresent,
+ List.of(greenReceiver, redReceiver));
+
+ final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK);
+ final BroadcastRecord timeTickRecord = makeBroadcastRecord(timeTick,
+ List.of(greenReceiver, redReceiver));
+
+ doAnswer(invocation -> {
+ final BroadcastRecord r = invocation.getArgument(0);
+ final Object o = invocation.getArgument(1);
+ if (userPresent.getAction().equals(r.intent.getAction())
+ && isReceiverEquals(o, greenReceiver)) {
+ return "receiver skipped by test";
+ }
+ return null;
+ }).when(mSkipPolicy).shouldSkipAtEnqueueMessage(any(BroadcastRecord.class), any());
+
+ mImpl.enqueueBroadcastLocked(userPresentRecord);
+ mImpl.enqueueBroadcastLocked(timeTickRecord);
+
+ final BroadcastProcessQueue greenQueue = mImpl.getProcessQueue(PACKAGE_GREEN,
+ getUidForPackage(PACKAGE_GREEN));
+ // There should be only one broadcast for green process as the other would have
+ // been skipped.
+ verifyPendingRecords(greenQueue, List.of(timeTick));
+ final BroadcastProcessQueue redQueue = mImpl.getProcessQueue(PACKAGE_RED,
+ getUidForPackage(PACKAGE_RED));
+ verifyPendingRecords(redQueue, List.of(userPresent, timeTick));
+ }
+
@DisableFlags(Flags.FLAG_LIMIT_PRIORITY_SCOPE)
@Test
public void testDeliveryDeferredForCached_flagDisabled() throws Exception {
@@ -2270,19 +2310,11 @@ public final class BroadcastQueueImplTest extends BaseBroadcastQueueTest {
assertFalse(mImpl.isProcessFreezable(greenProcess));
}
- // TODO: Reuse BroadcastQueueTest.makeActiveProcessRecord()
- private ProcessRecord makeProcessRecord(ApplicationInfo info) {
- final ProcessRecord r = spy(new ProcessRecord(mAms, info, info.processName, info.uid));
- r.setPid(mNextPid.incrementAndGet());
- ProcessRecord.updateProcessRecordNodes(r);
- return r;
- }
-
BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority) {
final IIntentReceiver receiver = mock(IIntentReceiver.class);
final ReceiverList receiverList = new ReceiverList(mAms, app, app.getPid(), app.info.uid,
UserHandle.getUserId(app.info.uid), receiver);
- return makeRegisteredReceiver(receiverList, priority);
+ return makeRegisteredReceiver(receiverList, priority, null /* requiredPermission */);
}
private Intent createPackageChangedIntent(int uid, List<String> componentNameList) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
index ad35b25a0d74..3a9c99d57d71 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java
@@ -2301,6 +2301,52 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest {
}
/**
+ * Verify that we skip broadcasts at enqueue if {@link BroadcastSkipPolicy} decides it
+ * should be skipped.
+ */
+ @EnableFlags(Flags.FLAG_AVOID_NOTE_OP_AT_ENQUEUE)
+ @Test
+ public void testSkipPolicy_atEnqueueTime() throws Exception {
+ final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED);
+ final ProcessRecord receiverGreenApp = makeActiveProcessRecord(PACKAGE_GREEN);
+ final ProcessRecord receiverBlueApp = makeActiveProcessRecord(PACKAGE_BLUE);
+
+ final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ final Object greenReceiver = makeRegisteredReceiver(receiverGreenApp);
+ final Object blueReceiver = makeRegisteredReceiver(receiverBlueApp);
+ final Object yellowReceiver = makeManifestReceiver(PACKAGE_YELLOW, CLASS_YELLOW);
+ final Object orangeReceiver = makeManifestReceiver(PACKAGE_ORANGE, CLASS_ORANGE);
+
+ doAnswer(invocation -> {
+ final BroadcastRecord r = invocation.getArgument(0);
+ final Object o = invocation.getArgument(1);
+ if (airplane.getAction().equals(r.intent.getAction())
+ && (isReceiverEquals(o, greenReceiver)
+ || isReceiverEquals(o, orangeReceiver))) {
+ return "test skipped receiver";
+ }
+ return null;
+ }).when(mSkipPolicy).shouldSkipAtEnqueueMessage(any(BroadcastRecord.class), any());
+ enqueueBroadcast(makeBroadcastRecord(airplane, callerApp,
+ List.of(greenReceiver, blueReceiver, yellowReceiver, orangeReceiver)));
+
+ waitForIdle();
+ // Verify that only blue and yellow receiver apps received the broadcast.
+ verifyScheduleRegisteredReceiver(never(), receiverGreenApp, USER_SYSTEM);
+ verify(mSkipPolicy, never()).shouldSkipMessage(any(BroadcastRecord.class),
+ eq(greenReceiver));
+ verifyScheduleRegisteredReceiver(receiverBlueApp, airplane);
+ final ProcessRecord receiverYellowApp = mAms.getProcessRecordLocked(PACKAGE_YELLOW,
+ getUidForPackage(PACKAGE_YELLOW));
+ verifyScheduleReceiver(receiverYellowApp, airplane);
+ final ProcessRecord receiverOrangeApp = mAms.getProcessRecordLocked(PACKAGE_ORANGE,
+ getUidForPackage(PACKAGE_ORANGE));
+ assertNull(receiverOrangeApp);
+ verify(mSkipPolicy, never()).shouldSkipMessage(any(BroadcastRecord.class),
+ eq(orangeReceiver));
+ }
+
+ /**
* Verify broadcasts to runtime receivers in cached processes are deferred
* until that process leaves the cached state.
*/
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java
new file mode 100644
index 000000000000..c8aad79edd12
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastSkipPolicyTest.java
@@ -0,0 +1,305 @@
+/*
+ * 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.am;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.never;
+
+import android.Manifest;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.content.IIntentReceiver;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+@SmallTest
+public class BroadcastSkipPolicyTest extends BaseBroadcastQueueTest {
+ private static final String TAG = "BroadcastSkipPolicyTest";
+
+ BroadcastSkipPolicy mBroadcastSkipPolicy;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ mBroadcastSkipPolicy = new BroadcastSkipPolicy(mAms);
+
+ doReturn(true).when(mIntentFirewall).checkBroadcast(any(Intent.class),
+ anyInt(), anyInt(), nullable(String.class), anyInt());
+
+ doReturn(mIPackageManager).when(AppGlobals::getPackageManager);
+ doReturn(true).when(mIPackageManager).isPackageAvailable(anyString(), anyInt());
+
+ doReturn(ActivityManager.APP_START_MODE_NORMAL).when(mAms).getAppStartModeLOSP(anyInt(),
+ anyString(), anyInt(), anyInt(), eq(true), eq(false), eq(false));
+
+ doReturn(mAppOpsManager).when(mAms).getAppOpsManager();
+ doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager).checkOpNoThrow(anyString(),
+ anyInt(), anyString(), nullable(String.class));
+ doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager).noteOpNoThrow(anyString(),
+ anyInt(), anyString(), nullable(String.class), anyString());
+
+ doReturn(mIPermissionManager).when(AppGlobals::getPermissionManager);
+ doReturn(PackageManager.PERMISSION_GRANTED).when(mIPermissionManager).checkUidPermission(
+ anyInt(), anyString(), anyInt());
+ }
+
+ @Override
+ public String getTag() {
+ return TAG;
+ }
+
+ @Override
+ public BroadcastSkipPolicy createBroadcastSkipPolicy() {
+ return new BroadcastSkipPolicy(mAms);
+ }
+
+ @Test
+ public void testShouldSkipMessage_withManifestRcvr_withCompPerm_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .build();
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+ makeManifestReceiverWithPermission(PACKAGE_GREEN, CLASS_GREEN,
+ Manifest.permission.PACKAGE_USAGE_STATS));
+ assertNull(msg);
+ verify(mAppOpsManager).noteOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId),
+ anyString());
+ verify(mAppOpsManager, never()).checkOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class));
+ }
+
+ @Test
+ public void testShouldSkipMessage_withRegRcvr_withCompPerm_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+ makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ Manifest.permission.PACKAGE_USAGE_STATS));
+ assertNull(msg);
+ verify(mAppOpsManager).noteOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId),
+ anyString());
+ verify(mAppOpsManager, never()).checkOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class));
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withCompPerm_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .build();
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+ makeManifestReceiverWithPermission(PACKAGE_GREEN, CLASS_GREEN,
+ Manifest.permission.PACKAGE_USAGE_STATS));
+ assertNull(msg);
+ verify(mAppOpsManager).checkOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId));
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withRegRcvr_withCompPerm_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+ makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ Manifest.permission.PACKAGE_USAGE_STATS));
+ assertNull(msg);
+ verify(mAppOpsManager).checkOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(record.callingUid), eq(record.callerPackage), eq(record.callerFeatureId));
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+ }
+
+ @Test
+ public void testShouldSkipMessage_withManifestRcvr_withAppOp_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+ .build();
+ final ResolveInfo receiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN);
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, receiver);
+ assertNull(msg);
+ verify(mAppOpsManager).noteOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(receiver.activityInfo.applicationInfo.uid),
+ eq(receiver.activityInfo.packageName), nullable(String.class), anyString());
+ verify(mAppOpsManager, never()).checkOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class));
+ }
+
+ @Test
+ public void testShouldSkipMessage_withRegRcvr_withAppOp_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final BroadcastFilter filter = makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ null /* requiredPermission */);
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record, filter);
+ assertNull(msg);
+ verify(mAppOpsManager).noteOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(filter.receiverList.uid),
+ eq(filter.packageName), nullable(String.class), anyString());
+ verify(mAppOpsManager, never()).checkOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class));
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withAppOp_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+ .build();
+ final ResolveInfo receiver = makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN);
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, receiver);
+ assertNull(msg);
+ verify(mAppOpsManager).checkOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(receiver.activityInfo.applicationInfo.uid),
+ eq(receiver.activityInfo.applicationInfo.packageName), nullable(String.class));
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withRegRcvr_withAppOp_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setAppOp(AppOpsManager.permissionToOpCode(Manifest.permission.PACKAGE_USAGE_STATS))
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final BroadcastFilter filter = makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ null /* requiredPermission */);
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record, filter);
+ assertNull(msg);
+ verify(mAppOpsManager).checkOpNoThrow(
+ eq(AppOpsManager.permissionToOp(Manifest.permission.PACKAGE_USAGE_STATS)),
+ eq(filter.receiverList.uid),
+ eq(filter.packageName), nullable(String.class));
+ verify(mAppOpsManager, never()).noteOpNoThrow(
+ anyString(), anyInt(), anyString(), nullable(String.class), anyString());
+ }
+
+ @Test
+ public void testShouldSkipMessage_withManifestRcvr_withRequiredPerms_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+ .build();
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+ makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN));
+ assertNull(msg);
+ verify(mPermissionManager).checkPermissionForDataDelivery(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+ verify(mPermissionManager, never()).checkPermissionForPreflight(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+ }
+
+ @Test
+ public void testShouldSkipMessage_withRegRcvr_withRequiredPerms_invokesNoteOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final String msg = mBroadcastSkipPolicy.shouldSkipMessage(record,
+ makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ null /* requiredPermission */));
+ assertNull(msg);
+ verify(mPermissionManager).checkPermissionForDataDelivery(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+ verify(mPermissionManager, never()).checkPermissionForPreflight(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withManifestRcvr_withRequiredPerms_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+ .build();
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+ makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN));
+ assertNull(msg);
+ verify(mPermissionManager, never()).checkPermissionForDataDelivery(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+ verify(mPermissionManager).checkPermissionForPreflight(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+ }
+
+ @Test
+ public void testShouldSkipAtEnqueueMessage_withRegRcvr_withRequiredPerms_invokesCheckOp() {
+ final BroadcastRecord record = new BroadcastRecordBuilder()
+ .setIntent(new Intent(Intent.ACTION_TIME_TICK))
+ .setRequiredPermissions(new String[] {Manifest.permission.PACKAGE_USAGE_STATS})
+ .build();
+ final ProcessRecord receiverApp = makeProcessRecord(makeApplicationInfo(PACKAGE_GREEN));
+ final String msg = mBroadcastSkipPolicy.shouldSkipAtEnqueueMessage(record,
+ makeRegisteredReceiver(receiverApp, 0 /* priority */,
+ null /* requiredPermission */));
+ assertNull(msg);
+ verify(mPermissionManager, never()).checkPermissionForDataDelivery(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any(), anyString());
+ verify(mPermissionManager).checkPermissionForPreflight(
+ eq(Manifest.permission.PACKAGE_USAGE_STATS), any());
+ }
+
+ private ResolveInfo makeManifestReceiverWithPermission(String packageName, String name,
+ String permission) {
+ final ResolveInfo resolveInfo = makeManifestReceiver(packageName, name);
+ resolveInfo.activityInfo.permission = permission;
+ return resolveInfo;
+ }
+
+ private BroadcastFilter makeRegisteredReceiver(ProcessRecord app, int priority,
+ String requiredPermission) {
+ final IIntentReceiver receiver = mock(IIntentReceiver.class);
+ final ReceiverList receiverList = new ReceiverList(mAms, app, app.getPid(), app.info.uid,
+ UserHandle.getUserId(app.info.uid), receiver);
+ return makeRegisteredReceiver(receiverList, priority, requiredPermission);
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
index bada337c7aa6..6b8ef88c556c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -64,7 +64,6 @@ import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.ServiceInfo;
-import android.content.res.Resources;
import android.graphics.Color;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
@@ -95,6 +94,7 @@ import com.android.internal.R;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.LocalServices;
+import com.android.server.wm.DesktopModeHelper;
import com.android.server.wm.WindowManagerInternal;
import org.hamcrest.CoreMatchers;
@@ -155,8 +155,6 @@ public class WallpaperManagerServiceTests {
private IPackageManager mIpm = AppGlobals.getPackageManager();
- private Resources mResources = sContext.getResources();
-
@Mock
private DisplayManager mDisplayManager;
@@ -178,6 +176,7 @@ public class WallpaperManagerServiceTests {
.spyStatic(WallpaperUtils.class)
.spyStatic(LocalServices.class)
.spyStatic(WallpaperManager.class)
+ .spyStatic(DesktopModeHelper.class)
.startMocking();
sWindowManagerInternal = mock(WindowManagerInternal.class);
@@ -246,6 +245,8 @@ public class WallpaperManagerServiceTests {
int userId = (invocation.getArgument(0));
return getWallpaperTestDir(userId);
}).when(() -> WallpaperUtils.getWallpaperDir(anyInt()));
+ ExtendedMockito.doAnswer(invocation -> true).when(
+ () -> DesktopModeHelper.isDeviceEligibleForDesktopMode(any()));
sContext.addMockSystemService(DisplayManager.class, mDisplayManager);
@@ -257,10 +258,6 @@ public class WallpaperManagerServiceTests {
doReturn(displays).when(mDisplayManager).getDisplays();
spyOn(mIpm);
- spyOn(mResources);
- doReturn(true).when(mResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
- doReturn(true).when(mResources).getBoolean(
- eq(R.bool.config_canInternalDisplayHostDesktops));
mService = new TestWallpaperManagerService(sContext);
spyOn(mService);
mService.systemReady();
diff --git a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
index a4e77c00d647..1de864cb4eb0 100644
--- a/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
+++ b/services/tests/servicestests/src/com/android/server/location/contexthub/ContextHubEndpointTest.java
@@ -17,9 +17,9 @@
package com.android.server.location.contexthub;
import static com.google.common.truth.Truth.assertThat;
-
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -33,15 +33,21 @@ import android.hardware.contexthub.HubMessage;
import android.hardware.contexthub.IContextHubEndpoint;
import android.hardware.contexthub.IContextHubEndpointCallback;
import android.hardware.contexthub.IEndpointCommunication;
+import android.hardware.contexthub.Message;
import android.hardware.contexthub.MessageDeliveryStatus;
import android.hardware.contexthub.Reason;
+import android.hardware.location.IContextHubTransactionCallback;
+import android.hardware.location.NanoAppState;
import android.os.Binder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
-
+import android.util.Log;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
+import java.util.Collections;
+import java.util.List;
+
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -51,11 +57,11 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
-import java.util.Collections;
-
@RunWith(AndroidJUnit4.class)
@Presubmit
public class ContextHubEndpointTest {
+ private static final String TAG = "ContextHubEndpointTest";
+
private static final int SESSION_ID_RANGE = ContextHubEndpointManager.SERVICE_SESSION_RANGE;
private static final int MIN_SESSION_ID = 0;
private static final int MAX_SESSION_ID = MIN_SESSION_ID + SESSION_ID_RANGE - 1;
@@ -206,6 +212,68 @@ public class ContextHubEndpointTest {
assertThat(mEndpointManager.getNumAvailableSessions()).isEqualTo(SESSION_ID_RANGE);
}
+ @Test
+ public void testMessageTransaction() throws RemoteException {
+ IContextHubEndpoint endpoint = registerExampleEndpoint();
+ testMessageTransactionInternal(endpoint, /* deliverMessageStatus= */ true);
+
+ unregisterExampleEndpoint(endpoint);
+ }
+
+ @Test
+ public void testMessageTransactionCleanupOnUnregistration() throws RemoteException {
+ IContextHubEndpoint endpoint = registerExampleEndpoint();
+ testMessageTransactionInternal(endpoint, /* deliverMessageStatus= */ false);
+
+ unregisterExampleEndpoint(endpoint);
+ assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(0);
+ }
+
+ /** A helper method to create a session and validates reliable message sending. */
+ private void testMessageTransactionInternal(
+ IContextHubEndpoint endpoint, boolean deliverMessageStatus) throws RemoteException {
+ HubEndpointInfo targetInfo =
+ new HubEndpointInfo(
+ TARGET_ENDPOINT_NAME,
+ TARGET_ENDPOINT_ID,
+ ENDPOINT_PACKAGE_NAME,
+ Collections.emptyList());
+ int sessionId = endpoint.openSession(targetInfo, /* serviceDescriptor= */ null);
+ mEndpointManager.onEndpointSessionOpenComplete(sessionId);
+
+ final int messageType = 1234;
+ HubMessage message =
+ new HubMessage.Builder(messageType, new byte[] {1, 2, 3, 4, 5})
+ .setResponseRequired(true)
+ .build();
+ IContextHubTransactionCallback callback =
+ new IContextHubTransactionCallback.Stub() {
+ @Override
+ public void onQueryResponse(int result, List<NanoAppState> nanoappList) {
+ Log.i(TAG, "Received onQueryResponse callback, result=" + result);
+ }
+
+ @Override
+ public void onTransactionComplete(int result) {
+ Log.i(TAG, "Received onTransactionComplete callback, result=" + result);
+ }
+ };
+ endpoint.sendMessage(sessionId, message, callback);
+ ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
+ verify(mMockEndpointCommunications, timeout(1000))
+ .sendMessageToEndpoint(eq(sessionId), messageCaptor.capture());
+ Message halMessage = messageCaptor.getValue();
+ assertThat(halMessage.type).isEqualTo(message.getMessageType());
+ assertThat(halMessage.content).isEqualTo(message.getMessageBody());
+ assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(1);
+
+ if (deliverMessageStatus) {
+ mEndpointManager.onMessageDeliveryStatusReceived(
+ sessionId, halMessage.sequenceNumber, ErrorCode.OK);
+ assertThat(mTransactionManager.numReliableMessageTransactionPending()).isEqualTo(0);
+ }
+ }
+
private IContextHubEndpoint registerExampleEndpoint() throws RemoteException {
HubEndpointInfo info =
new HubEndpointInfo(
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
index b33233107766..6b989cb0aaee 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
@@ -38,6 +38,7 @@ import android.os.IInterface;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.Condition;
+import com.android.internal.R;
import com.android.server.UiServiceTestCase;
import org.junit.Before;
@@ -46,6 +47,8 @@ import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.List;
+
public class ConditionProvidersTest extends UiServiceTestCase {
private ConditionProviders mProviders;
@@ -169,4 +172,15 @@ public class ConditionProvidersTest extends UiServiceTestCase {
assertTrue(mProviders.getApproved(userId, true).isEmpty());
}
+
+ @Test
+ public void getDefaultDndAccessPackages_returnsPackages() {
+ mContext.getOrCreateTestableResources().addOverride(
+ R.string.config_defaultDndAccessPackages,
+ "com.example.a:com.example.b::::com.example.c");
+
+ List<String> packages = ConditionProviders.getDefaultDndAccessPackages(mContext);
+
+ assertThat(packages).containsExactly("com.example.a", "com.example.b", "com.example.c");
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java
new file mode 100644
index 000000000000..154a905c776b
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenConfigTrimmerTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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.notification;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.ZenRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class ZenConfigTrimmerTest extends UiServiceTestCase {
+
+ private static final String TRUSTED_PACKAGE = "com.trust.me";
+ private static final int ONE_PERCENT = 1_500;
+
+ private ZenConfigTrimmer mTrimmer;
+
+ @Before
+ public void setUp() {
+ mContext.getOrCreateTestableResources().addOverride(
+ R.string.config_defaultDndAccessPackages, TRUSTED_PACKAGE);
+
+ mTrimmer = new ZenConfigTrimmer(mContext);
+ }
+
+ @Test
+ public void trimToMaximumSize_belowMax_untouched() {
+ ZenModeConfig config = new ZenModeConfig();
+ addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "4", "pkg2", 20 * ONE_PERCENT);
+ addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT);
+
+ mTrimmer.trimToMaximumSize(config);
+
+ assertThat(config.automaticRules.keySet()).containsExactly("1", "2", "3", "4", "5");
+ }
+
+ @Test
+ public void trimToMaximumSize_exceedsMax_removesAllRulesOfLargestPackages() {
+ ZenModeConfig config = new ZenModeConfig();
+ addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "4", "pkg2", 20 * ONE_PERCENT);
+ addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT);
+ addZenRule(config, "6", "pkg3", 35 * ONE_PERCENT);
+ addZenRule(config, "7", "pkg4", 38 * ONE_PERCENT);
+
+ mTrimmer.trimToMaximumSize(config);
+
+ assertThat(config.automaticRules.keySet()).containsExactly("1", "2", "3", "6");
+ assertThat(config.automaticRules.values().stream().map(r -> r.pkg).distinct())
+ .containsExactly("pkg1", "pkg3");
+ }
+
+ @Test
+ public void trimToMaximumSize_keepsRulesFromTrustedPackages() {
+ ZenModeConfig config = new ZenModeConfig();
+ addZenRule(config, "1", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "2", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "3", "pkg1", 10 * ONE_PERCENT);
+ addZenRule(config, "4", TRUSTED_PACKAGE, 60 * ONE_PERCENT);
+ addZenRule(config, "5", "pkg2", 20 * ONE_PERCENT);
+ addZenRule(config, "6", "pkg3", 35 * ONE_PERCENT);
+
+ mTrimmer.trimToMaximumSize(config);
+
+ assertThat(config.automaticRules.keySet()).containsExactly("4", "5");
+ assertThat(config.automaticRules.values().stream().map(r -> r.pkg).distinct())
+ .containsExactly(TRUSTED_PACKAGE, "pkg2");
+ }
+
+ /**
+ * Create a ZenRule that, when serialized to a Parcel, will take <em>approximately</em>
+ * {@code desiredSize} bytes (within 100 bytes). Try to make the tests not rely on a very tight
+ * fit.
+ */
+ private static void addZenRule(ZenModeConfig config, String id, String pkg, int desiredSize) {
+ ZenRule rule = new ZenRule();
+ rule.id = id;
+ rule.pkg = pkg;
+ config.automaticRules.put(id, rule);
+
+ // Make the ZenRule as large as desired. Not to the exact byte, because otherwise this
+ // test would have to be adjusted whenever we change the parceling of ZenRule in any way.
+ // (Still might need adjustment if we change the serialization _significantly_).
+ int nameLength = desiredSize - id.length() - pkg.length() - 232;
+ rule.name = "A".repeat(nameLength);
+
+ Parcel verification = Parcel.obtain();
+ try {
+ verification.writeParcelable(rule, 0);
+ assertThat(verification.dataSize()).isWithin(100).of(desiredSize);
+ } finally {
+ verification.recycle();
+ }
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index f8387a4c54cc..51891ef71bbd 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -90,6 +90,7 @@ import static com.android.os.dnd.DNDProtoEnums.PEOPLE_STARRED;
import static com.android.os.dnd.DNDProtoEnums.ROOT_CONFIG;
import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW;
import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW;
+import static com.android.server.notification.Flags.FLAG_LIMIT_ZEN_CONFIG_SIZE;
import static com.android.server.notification.Flags.FLAG_PREVENT_ZEN_DEVICE_EFFECTS_WHILE_DRIVING;
import static com.android.server.notification.ZenModeEventLogger.ACTIVE_RULE_TYPE_MANUAL;
import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE;
@@ -236,6 +237,7 @@ import java.util.stream.Collectors;
@SmallTest
@SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service.
@RunWith(ParameterizedAndroidJunit4.class)
+@EnableFlags(FLAG_LIMIT_ZEN_CONFIG_SIZE) // Should be parameterization, but off path does nothing.
@TestableLooper.RunWithLooper
public class ZenModeHelperTest extends UiServiceTestCase {
@@ -7480,6 +7482,45 @@ public class ZenModeHelperTest extends UiServiceTestCase {
assertThat(getZenRule(ruleId).lastActivation).isNull();
}
+ @Test
+ @EnableFlags(FLAG_LIMIT_ZEN_CONFIG_SIZE)
+ public void addAutomaticZenRule_trimsConfiguration() {
+ mZenModeHelper.mConfig.automaticRules.clear();
+ AutomaticZenRule smallRule = new AutomaticZenRule.Builder("Reasonable", CONDITION_ID)
+ .setConfigurationActivity(new ComponentName(mPkg, "cls"))
+ .build();
+ AutomaticZenRule systemRule = new AutomaticZenRule.Builder("System", CONDITION_ID)
+ .setOwner(new ComponentName("android", "ScheduleConditionProvider"))
+ .build();
+
+ AutomaticZenRule bigRule = new AutomaticZenRule.Builder("Yuge", CONDITION_ID)
+ .setConfigurationActivity(new ComponentName("evil.package", "cls"))
+ .setTriggerDescription("0123456789".repeat(6000)) // ~60k bytes utf16.
+ .build();
+
+ String systemRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "android",
+ systemRule, ORIGIN_SYSTEM, "add", SYSTEM_UID);
+ String smallRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, smallRule,
+ ORIGIN_APP, "add", CUSTOM_PKG_UID);
+ String bigRuleId1 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package",
+ bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+ systemRuleId, smallRuleId, bigRuleId1);
+
+ String bigRuleId2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package",
+ bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+ systemRuleId, smallRuleId, bigRuleId1, bigRuleId2);
+
+ // This should go over the threshold
+ String bigRuleId3 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "evil.package",
+ bigRule, ORIGIN_APP, "add", CUSTOM_PKG_UID);
+
+ // Rules from evil.package are gone.
+ assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(
+ systemRuleId, smallRuleId);
+ }
+
private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode,
@Nullable ZenPolicy zenPolicy) {
ZenRule rule = new ZenRule();
diff --git a/services/tests/wmtests/src/com/android/server/TransitionSubject.java b/services/tests/wmtests/src/com/android/server/TransitionSubject.java
new file mode 100644
index 000000000000..07026b98f226
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/TransitionSubject.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.annotation.Nullable;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TransitionSubject extends Subject {
+
+ @Nullable
+ private final Transition actual;
+
+ /**
+ * Internal constructor.
+ *
+ * @see TransitionSubject#assertThat(Transition)
+ */
+ private TransitionSubject(FailureMetadata metadata, @Nullable Transition actual) {
+ super(metadata, actual);
+ this.actual = actual;
+ }
+
+ /**
+ * In a fluent assertion chain, the argument to the "custom" overload of {@link
+ * StandardSubjectBuilder#about(CustomSubjectBuilder.Factory) about}, the method that specifies
+ * what kind of {@link Subject} to create.
+ */
+ public static Factory<TransitionSubject, Transition> transitions() {
+ return TransitionSubject::new;
+ }
+
+ /**
+ * Typical entry point for making assertions about Transitions.
+ *
+ * @see @Truth#assertThat(Object)
+ */
+ public static TransitionSubject assertThat(Transition transition) {
+ return Truth.assertAbout(transitions()).that(transition);
+ }
+
+ /**
+ * Converts to a {@link IterableSubject} containing {@link Transition#getFlags()} separated into
+ * a list of individual flags for assertions such as {@code flags().contains(TRANSIT_FLAG_XYZ)}.
+ *
+ * <p>If the subject is null, this will fail instead of returning a null subject.
+ */
+ public IterableSubject flags() {
+ isNotNull();
+
+ final List<Integer> sortedFlags = new ArrayList<>();
+ for (int i = 0; i < 32; i++) {
+ if ((actual.getFlags() & (1 << i)) != 0) {
+ sortedFlags.add((1 << i));
+ }
+ }
+ return com.google.common.truth.Truth.assertThat(sortedFlags);
+ }
+}
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 cfd501abbe8b..61ed0b53cdcf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -63,6 +63,9 @@ import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
import static android.view.WindowManager.LayoutParams.TYPE_VOICE_INTERACTION;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
+import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.window.DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
@@ -80,6 +83,7 @@ import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_TOKEN_TRANSFO
import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
import static com.android.server.wm.WindowContainer.POSITION_TOP;
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
+import static com.android.server.wm.TransitionSubject.assertThat;
import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE;
import static com.android.server.display.feature.flags.Flags.FLAG_ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT;
@@ -147,6 +151,7 @@ import com.android.internal.logging.nano.MetricsProto;
import com.android.server.LocalServices;
import com.android.server.policy.WindowManagerPolicy;
import com.android.server.wm.utils.WmDisplayCutout;
+import com.android.window.flags.Flags;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -2620,6 +2625,7 @@ public class DisplayContentTests extends WindowTestsBase {
final KeyguardController keyguard = mAtm.mKeyguardController;
final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
final int displayId = mDisplayContent.getDisplayId();
+ final TestTransitionPlayer transitions = registerTestTransitionPlayer();
final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId);
final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId);
@@ -2629,21 +2635,40 @@ public class DisplayContentTests extends WindowTestsBase {
keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */);
assertFalse(keyguardGoingAway.getAsBoolean());
assertFalse(appVisible.getAsBoolean());
+ transitions.flush();
// Start unlocking from AOD.
keyguard.keyguardGoingAway(displayId, 0x0 /* flags */);
assertTrue(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+ if (Flags.ensureKeyguardDoesTransitionStarting()) {
+ assertThat(transitions.mLastTransit).isNull();
+ } else {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_KEYGUARD_GOING_AWAY);
+ }
+ transitions.flush();
+
// Clear AOD. This does *not* clear the going-away status.
keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
assertTrue(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+ if (Flags.aodTransition()) {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_AOD_APPEARING);
+ } else {
+ assertThat(transitions.mLastTransit).isNull();
+ }
+ transitions.flush();
+
// Finish unlock
keyguard.setKeyguardShown(displayId, false /* keyguard */, false /* aod */);
assertFalse(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+
+ assertThat(transitions.mLastTransit).isNull();
}
@Test
@@ -2653,6 +2678,7 @@ public class DisplayContentTests extends WindowTestsBase {
final KeyguardController keyguard = mAtm.mKeyguardController;
final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
final int displayId = mDisplayContent.getDisplayId();
+ final TestTransitionPlayer transitions = registerTestTransitionPlayer();
final BooleanSupplier keyguardShowing = () -> keyguard.isKeyguardShowing(displayId);
final BooleanSupplier keyguardGoingAway = () -> keyguard.isKeyguardGoingAway(displayId);
@@ -2662,22 +2688,44 @@ public class DisplayContentTests extends WindowTestsBase {
keyguard.setKeyguardShown(displayId, true /* keyguard */, true /* aod */);
assertFalse(keyguardGoingAway.getAsBoolean());
assertFalse(appVisible.getAsBoolean());
+ transitions.flush();
// Start unlocking from AOD.
keyguard.keyguardGoingAway(displayId, 0x0 /* flags */);
assertTrue(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+ if (!Flags.ensureKeyguardDoesTransitionStarting()) {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_KEYGUARD_GOING_AWAY);
+ }
+ transitions.flush();
+
// Clear AOD. This does *not* clear the going-away status.
keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
assertTrue(keyguardGoingAway.getAsBoolean());
assertTrue(appVisible.getAsBoolean());
+ if (Flags.aodTransition()) {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_AOD_APPEARING);
+ } else {
+ assertThat(transitions.mLastTransit).isNull();
+ }
+ transitions.flush();
+
// Same API call a second time cancels the unlock, because AOD isn't changing.
keyguard.setKeyguardShown(displayId, true /* keyguard */, false /* aod */);
assertTrue(keyguardShowing.getAsBoolean());
assertFalse(keyguardGoingAway.getAsBoolean());
assertFalse(appVisible.getAsBoolean());
+
+ if (Flags.ensureKeyguardDoesTransitionStarting()) {
+ assertThat(transitions.mLastTransit).isNull();
+ } else {
+ assertThat(transitions.mLastTransit).flags()
+ .containsExactly(TRANSIT_FLAG_KEYGUARD_APPEARING);
+ }
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
index 2d4101e40615..6e0f7fbbf388 100644
--- a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java
@@ -16,9 +16,12 @@
package com.android.server.wm;
+import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.FLAG_PRESENTATION;
+import static android.view.Display.FLAG_TRUSTED;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_WAKE;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS;
@@ -30,6 +33,7 @@ import static org.mockito.ArgumentMatchers.eq;
import android.annotation.NonNull;
import android.graphics.Rect;
+import android.os.Binder;
import android.os.UserHandle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
@@ -118,6 +122,112 @@ public class PresentationControllerTests extends WindowTestsBase {
assertFalse(window.isAttached());
}
+ @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testPresentationCannotCoverHostTask() {
+ int uid = Binder.getCallingUid();
+ final DisplayContent presentationDisplay = createPresentationDisplay();
+ final Task task = createTask(presentationDisplay);
+ task.effectiveUid = uid;
+ final ActivityRecord activity = createActivityRecord(task);
+ assertTrue(activity.isVisible());
+
+ // Adding a presentation window over its host task must fail.
+ assertAddPresentationWindowFails(uid, presentationDisplay.mDisplayId);
+
+ // Adding a presentation window on the other display must succeed.
+ final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
+ final Transition addTransition = window.mTransitionController.getCollectingTransition();
+ completeTransition(addTransition, /*abortSync=*/ true);
+ assertTrue(window.isVisible());
+
+ // Moving the host task to the presenting display will remove the presentation.
+ task.reparent(mDefaultDisplay.getDefaultTaskDisplayArea(), true);
+ waitHandlerIdle(window.mWmService.mAtmService.mH);
+ final Transition removeTransition = window.mTransitionController.getCollectingTransition();
+ assertEquals(TRANSIT_CLOSE, removeTransition.mType);
+ completeTransition(removeTransition, /*abortSync=*/ false);
+ assertFalse(window.isVisible());
+ }
+
+ @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testPresentationCannotLaunchOnAllDisplays() {
+ final int uid = Binder.getCallingUid();
+ final DisplayContent presentationDisplay = createPresentationDisplay();
+ final Task task = createTask(presentationDisplay);
+ task.effectiveUid = uid;
+ final ActivityRecord activity = createActivityRecord(task);
+ assertTrue(activity.isVisible());
+
+ // Add a presentation window on the default display.
+ final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
+ final Transition addTransition = window.mTransitionController.getCollectingTransition();
+ completeTransition(addTransition, /*abortSync=*/ true);
+ assertTrue(window.isVisible());
+
+ // Adding another presentation window over the task even if it's a different UID because
+ // it would end up showing presentations on all displays.
+ assertAddPresentationWindowFails(uid + 1, presentationDisplay.mDisplayId);
+ }
+
+ @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testPresentationCannotLaunchOnNonPresentationDisplayWithoutHostHavingGlobalFocus() {
+ final int uid = Binder.getCallingUid();
+ // Adding a presentation window on an internal display requires a host task
+ // with global focus on another display.
+ assertAddPresentationWindowFails(uid, DEFAULT_DISPLAY);
+
+ final DisplayContent presentationDisplay = createPresentationDisplay();
+ final Task taskWiSameUid = createTask(presentationDisplay);
+ taskWiSameUid.effectiveUid = uid;
+ final ActivityRecord activity = createActivityRecord(taskWiSameUid);
+ assertTrue(activity.isVisible());
+ final Task taskWithDifferentUid = createTask(presentationDisplay);
+ taskWithDifferentUid.effectiveUid = uid + 1;
+ createActivityRecord(taskWithDifferentUid);
+ assertEquals(taskWithDifferentUid, presentationDisplay.getFocusedRootTask());
+
+ // The task with the same UID is covered by another task with a different UID, so this must
+ // also fail.
+ assertAddPresentationWindowFails(uid, DEFAULT_DISPLAY);
+
+ // Moving the task with the same UID to front and giving it global focus allows a
+ // presentation to show on the default display.
+ taskWiSameUid.moveToFront("test");
+ final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
+ final Transition addTransition = window.mTransitionController.getCollectingTransition();
+ completeTransition(addTransition, /*abortSync=*/ true);
+ assertTrue(window.isVisible());
+ }
+
+ @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testReparentingActivityToSameDisplayClosesPresentation() {
+ final int uid = Binder.getCallingUid();
+ final Task task = createTask(mDefaultDisplay);
+ task.effectiveUid = uid;
+ final ActivityRecord activity = createActivityRecord(task);
+ assertTrue(activity.isVisible());
+
+ // Add a presentation window on a presentation display.
+ final DisplayContent presentationDisplay = createPresentationDisplay();
+ final WindowState window = addPresentationWindow(uid, presentationDisplay.getDisplayId());
+ final Transition addTransition = window.mTransitionController.getCollectingTransition();
+ completeTransition(addTransition, /*abortSync=*/ true);
+ assertTrue(window.isVisible());
+
+ // Reparenting the host task below the presentation must close the presentation.
+ task.reparent(presentationDisplay.getDefaultTaskDisplayArea(), true);
+ waitHandlerIdle(window.mWmService.mAtmService.mH);
+ final Transition removeTransition = window.mTransitionController.getCollectingTransition();
+ // It's a WAKE transition instead of CLOSE because
+ assertEquals(TRANSIT_WAKE, removeTransition.mType);
+ completeTransition(removeTransition, /*abortSync=*/ false);
+ assertFalse(window.isVisible());
+ }
+
private WindowState addPresentationWindow(int uid, int displayId) {
final Session session = createTestSession(mAtm, 1234 /* pid */, uid);
final int userId = UserHandle.getUserId(uid);
@@ -134,10 +244,29 @@ public class PresentationControllerTests extends WindowTestsBase {
return window;
}
+ private void assertAddPresentationWindowFails(int uid, int displayId) {
+ final Session session = createTestSession(mAtm, 1234 /* pid */, uid);
+ final IWindow clientWindow = new TestIWindow();
+ final int res = addPresentationWindowInner(uid, displayId, session, clientWindow);
+ assertEquals(WindowManagerGlobal.ADD_INVALID_DISPLAY, res);
+ }
+
+ private int addPresentationWindowInner(int uid, int displayId, Session session,
+ IWindow clientWindow) {
+ final int userId = UserHandle.getUserId(uid);
+ doReturn(true).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId));
+ final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.TYPE_PRESENTATION);
+ return mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, userId,
+ WindowInsets.Type.defaultVisible(), null, new InsetsState(),
+ new InsetsSourceControl.Array(), new Rect(), new float[1]);
+ }
+
private DisplayContent createPresentationDisplay() {
final DisplayInfo displayInfo = new DisplayInfo();
displayInfo.copyFrom(mDisplayInfo);
- displayInfo.flags = FLAG_PRESENTATION;
+ displayInfo.flags = FLAG_PRESENTATION | FLAG_TRUSTED;
+ displayInfo.displayId = DEFAULT_DISPLAY + 1;
final DisplayContent dc = createNewDisplay(displayInfo);
final int displayId = dc.getDisplayId();
doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index 45436e47e881..d3f3269392d8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -33,6 +33,7 @@ import android.os.Parcel;
import android.platform.test.annotations.Presubmit;
import android.view.Display.Mode;
import android.view.Surface;
+import android.view.WindowInsets;
import android.view.WindowManager.LayoutParams;
import androidx.test.filters.SmallTest;
@@ -283,7 +284,7 @@ public class RefreshRatePolicyTest extends WindowTestsBase {
assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
- overrideWindow.notifyInsetsAnimationRunningStateChanged(true);
+ overrideWindow.setAnimatingTypes(WindowInsets.Type.statusBars());
assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(overrideWindow));
assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote);
@@ -303,7 +304,7 @@ public class RefreshRatePolicyTest extends WindowTestsBase {
assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
- overrideWindow.notifyInsetsAnimationRunningStateChanged(true);
+ overrideWindow.setAnimatingTypes(WindowInsets.Type.statusBars());
assertEquals(0, mPolicy.getPreferredModeId(overrideWindow));
assertTrue(mPolicy.updateFrameRateVote(overrideWindow));
assertEquals(FRAME_RATE_VOTE_NONE, overrideWindow.mFrameRateVote);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index c0642f5533eb..57ab13ffee89 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -2151,6 +2151,14 @@ public class WindowTestsBase extends SystemServiceTestsBase {
mLastRequest = null;
}
+ void flush() {
+ if (mLastTransit != null) {
+ start();
+ finish();
+ clear();
+ }
+ }
+
@Override
public void onTransitionReady(IBinder transitToken, TransitionInfo transitionInfo,
SurfaceControl.Transaction transaction, SurfaceControl.Transaction finishT)
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index ca4a643d7b20..ae7346eb6df4 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -1737,13 +1737,8 @@ public class EuiccManager {
private int getCardIdForDefaultEuicc() {
int cardId = TelephonyManager.UNINITIALIZED_CARD_ID;
- if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
- PackageManager pm = mContext.getPackageManager();
- if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) {
- TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
- cardId = tm.getCardIdForDefaultEuicc();
- }
- } else {
+ PackageManager pm = mContext.getPackageManager();
+ if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_EUICC)) {
TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
cardId = tm.getCardIdForDefaultEuicc();
}
diff --git a/tools/processors/view_inspector/OWNERS b/tools/processors/view_inspector/OWNERS
index 0473f54e57ca..38d21e141f43 100644
--- a/tools/processors/view_inspector/OWNERS
+++ b/tools/processors/view_inspector/OWNERS
@@ -1,3 +1,2 @@
alanv@google.com
-ashleyrose@google.com
-aurimas@google.com \ No newline at end of file
+aurimas@google.com