summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java278
-rw-r--r--apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java6
-rw-r--r--cmds/idmap2/Android.bp1
-rw-r--r--cmds/idmap2/idmap2/Create.cpp5
-rw-r--r--cmds/idmap2/idmap2/CreateMultiple.cpp6
-rw-r--r--cmds/idmap2/idmap2d/Idmap2Service.cpp33
-rw-r--r--cmds/idmap2/idmap2d/Idmap2Service.h3
-rw-r--r--cmds/idmap2/idmap2d/aidl/core/android/os/OverlayConstraint.aidl25
-rw-r--r--cmds/idmap2/idmap2d/aidl/services/android/os/IIdmap2.aidl7
-rw-r--r--cmds/idmap2/include/idmap2/BinaryStreamVisitor.h1
-rw-r--r--cmds/idmap2/include/idmap2/Idmap.h42
-rw-r--r--cmds/idmap2/include/idmap2/PrettyPrintVisitor.h1
-rw-r--r--cmds/idmap2/include/idmap2/RawPrintVisitor.h1
-rw-r--r--cmds/idmap2/libidmap2/BinaryStreamVisitor.cpp8
-rw-r--r--cmds/idmap2/libidmap2/Idmap.cpp42
-rw-r--r--cmds/idmap2/libidmap2/PrettyPrintVisitor.cpp13
-rw-r--r--cmds/idmap2/libidmap2/RawPrintVisitor.cpp8
-rw-r--r--cmds/idmap2/self_targeting/SelfTargeting.cpp6
-rw-r--r--cmds/idmap2/tests/BinaryStreamVisitorTests.cpp2
-rw-r--r--cmds/idmap2/tests/Idmap2BinaryTests.cpp1
-rw-r--r--cmds/idmap2/tests/IdmapTests.cpp44
-rw-r--r--cmds/idmap2/tests/PrettyPrintVisitorTests.cpp4
-rw-r--r--cmds/idmap2/tests/RawPrintVisitorTests.cpp14
-rw-r--r--cmds/idmap2/tests/TestHelpers.h108
-rw-r--r--core/api/current.txt14
-rw-r--r--core/api/system-current.txt14
-rw-r--r--core/api/test-current.txt2
-rw-r--r--core/java/android/app/ApplicationPackageManager.java23
-rw-r--r--core/java/android/app/PropertyInvalidatedCache.java32
-rw-r--r--core/java/android/app/appfunctions/AppFunctionManager.java23
-rw-r--r--core/java/android/app/appfunctions/AppFunctionManagerHelper.java17
-rw-r--r--core/java/android/companion/virtual/VirtualDeviceManager.java27
-rw-r--r--core/java/android/companion/virtual/VirtualDeviceParams.java24
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCamera.java3
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraCallback.java3
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraConfig.java4
-rw-r--r--core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java3
-rw-r--r--core/java/android/companion/virtual/flags/flags.aconfig30
-rw-r--r--core/java/android/content/om/IOverlayManager.aidl19
-rw-r--r--core/java/android/content/om/OverlayConstraint.aidl19
-rw-r--r--core/java/android/content/om/OverlayConstraint.java151
-rw-r--r--core/java/android/content/om/OverlayInfo.java67
-rw-r--r--core/java/android/content/om/OverlayManager.java44
-rw-r--r--core/java/android/content/om/OverlayManagerTransaction.java94
-rw-r--r--core/java/android/content/pm/PackageManager.java31
-rw-r--r--core/java/android/content/res/flags.aconfig8
-rw-r--r--core/java/android/hardware/Camera.java3
-rw-r--r--core/java/android/hardware/camera2/CameraManager.java13
-rw-r--r--core/java/android/hardware/input/IInputManager.aidl11
-rw-r--r--core/java/android/hardware/input/IKeyEventActivityListener.aidl23
-rw-r--r--core/java/android/hardware/input/InputManager.java37
-rw-r--r--core/java/android/hardware/input/InputManagerGlobal.java65
-rw-r--r--core/java/android/os/BaseBundle.java11
-rw-r--r--core/java/android/os/BatteryUsageStats.java25
-rw-r--r--core/java/android/os/Bundle.java36
-rw-r--r--core/java/android/os/Parcel.java65
-rw-r--r--core/java/android/permission/flags.aconfig8
-rw-r--r--core/java/android/provider/Telephony.java7
-rw-r--r--core/java/android/view/InsetsSourceConsumer.java6
-rw-r--r--core/java/android/view/ViewConfiguration.java231
-rw-r--r--core/java/android/view/ViewGroup.java8
-rw-r--r--core/java/android/view/WindowManager.java10
-rw-r--r--core/java/android/view/accessibility/flags/accessibility_flags.aconfig8
-rw-r--r--core/java/android/window/TransitionInfo.java4
-rw-r--r--core/java/com/android/internal/content/om/OverlayManagerImpl.java2
-rw-r--r--core/java/com/android/internal/jank/Cuj.java15
-rw-r--r--core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java10
-rw-r--r--core/jni/android_util_Binder.cpp32
-rw-r--r--core/res/res/values/config.xml37
-rw-r--r--core/res/res/values/symbols.xml11
-rw-r--r--core/tests/coretests/src/android/content/IntentTest.java39
-rw-r--r--core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java18
-rw-r--r--libs/WindowManager/Shell/OWNERS4
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml118
-rw-r--r--libs/WindowManager/Shell/res/values/styles.xml29
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java67
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java27
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java47
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt48
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt16
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt86
-rw-r--r--libs/androidfw/Idmap.cpp38
-rw-r--r--libs/androidfw/ResourceTypes.cpp17
-rw-r--r--libs/androidfw/include/androidfw/Idmap.h8
-rw-r--r--libs/androidfw/include/androidfw/ResourceTypes.h4
-rw-r--r--libs/androidfw/tests/data/overlay/overlay.idmapbin732 -> 736 bytes
-rw-r--r--libs/hwui/OWNERS1
-rw-r--r--media/java/android/media/flags/media_better_together.aconfig10
-rw-r--r--media/jni/android_media_MediaDrm.cpp7
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java8
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java8
-rw-r--r--packages/SystemUI/Android.bp1
-rw-r--r--packages/SystemUI/compose/core/Android.bp5
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt7
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt18
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt8
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt9
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt2
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt6
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt8
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt9
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt23
-rw-r--r--packages/SystemUI/compose/scene/Android.bp5
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt9
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/DataPointTypes.kt84
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/FeatureCaptures.kt57
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt4
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt51
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java23
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt85
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt47
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt23
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt94
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt61
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java (renamed from packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java)27
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java18
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt97
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt30
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt22
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java1189
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt910
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt37
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt143
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt43
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt27
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt37
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowTest.kt100
-rw-r--r--packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/SwipeHelper.java41
-rw-r--r--packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt57
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java234
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java89
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt51
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt49
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/core/NewStatusBarIcons.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java46
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java33
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt50
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/data/repository/BatteryRepository.kt163
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt93
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/shared/ui/BatteryColors.kt73
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/shared/ui/BatteryFrame.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/shared/ui/BatteryGlyph.kt318
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/binder/UnifiedBatteryViewBinder.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithEstimate.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt155
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/model/AttributionGlyph.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt246
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt8
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java166
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/data/repository/BatteryRepositoryTest.kt158
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractorTest.kt61
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelTest.kt100
-rw-r--r--packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt13
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt9
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt20
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt8
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt39
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt116
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/data/repository/BatteryRepositoryKosmos.kt36
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractorKosmos.kt22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelKosmos.kt24
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/BatteryControllerKosmos.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeBatteryControllerImpl.kt159
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt14
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt23
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java42
-rw-r--r--services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java5
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java24
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java14
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerShellCommand.java1
-rw-r--r--services/core/java/com/android/server/am/CachedAppOptimizer.java452
-rw-r--r--services/core/java/com/android/server/am/OomAdjuster.java1
-rw-r--r--services/core/java/com/android/server/am/UserController.java38
-rw-r--r--services/core/java/com/android/server/am/compaction/AggregatedCompactionStats.java124
-rw-r--r--services/core/java/com/android/server/am/compaction/AggregatedProcessCompactionStats.java23
-rw-r--r--services/core/java/com/android/server/am/compaction/AggregatedSourceCompactionStats.java27
-rw-r--r--services/core/java/com/android/server/am/compaction/CompactionStatsManager.java320
-rw-r--r--services/core/java/com/android/server/am/compaction/OWNERS5
-rw-r--r--services/core/java/com/android/server/am/compaction/SingleCompactionStats.java94
-rw-r--r--services/core/java/com/android/server/appop/AppOpsService.java29
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java3
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java87
-rw-r--r--services/core/java/com/android/server/om/IdmapDaemon.java13
-rw-r--r--services/core/java/com/android/server/om/IdmapManager.java24
-rw-r--r--services/core/java/com/android/server/om/OverlayManagerService.java21
-rw-r--r--services/core/java/com/android/server/om/OverlayManagerServiceImpl.java53
-rw-r--r--services/core/java/com/android/server/om/OverlayManagerSettings.java74
-rw-r--r--services/core/java/com/android/server/pm/AppsFilterImpl.java5
-rw-r--r--services/core/java/com/android/server/power/ThermalManagerService.java27
-rw-r--r--services/core/java/com/android/server/power/stats/BatteryChargeCalculator.java10
-rw-r--r--services/core/java/com/android/server/power/stats/processor/BasePowerStatsProcessor.java41
-rw-r--r--services/core/java/com/android/server/power/stats/processor/PowerStatsAggregator.java3
-rw-r--r--services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java8
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java22
-rw-r--r--services/core/java/com/android/server/wm/DisplayContent.java16
-rw-r--r--services/core/java/com/android/server/wm/KeyguardController.java24
-rw-r--r--services/core/java/com/android/server/wm/Transition.java5
-rw-r--r--services/core/java/com/android/server/wm/TransitionController.java13
-rw-r--r--services/core/java/com/android/server/wm/WallpaperController.java25
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java10
-rw-r--r--services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java56
-rw-r--r--services/tests/mockingservicestests/AndroidManifest.xml10
-rw-r--r--services/tests/mockingservicestests/res/xml/test_wallpaper.xml19
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java82
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/wallpaper/TestWallpaperService.java26
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java120
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java6
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java18
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java10
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java25
-rw-r--r--services/tests/servicestests/src/com/android/server/am/UserControllerTest.java19
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java1
-rw-r--r--services/tests/servicestests/src/com/android/server/om/OverlayConstraintsTests.java195
-rw-r--r--services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java3
-rw-r--r--services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java75
-rw-r--r--services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java5
-rw-r--r--services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java201
-rw-r--r--telephony/java/android/telephony/data/ApnSetting.java2
-rw-r--r--tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java4
-rw-r--r--tests/Input/src/com/android/server/input/InputManagerServiceTests.kt46
-rw-r--r--tools/aapt2/cmd/Link.cpp3
-rw-r--r--tools/aapt2/cmd/Link_test.cpp2
-rw-r--r--tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout1.xml4
-rw-r--r--tools/aapt2/link/FeatureFlagsFilter.cpp10
-rw-r--r--tools/aapt2/link/FeatureFlagsFilter_test.cpp28
-rw-r--r--tools/aapt2/link/FlaggedResources_test.cpp21
291 files changed, 8479 insertions, 3671 deletions
diff --git a/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java b/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java
index 7a7250b9e910..8e3ed6d9931c 100644
--- a/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java
+++ b/apct-tests/perftests/core/src/android/view/ViewConfigurationPerfTest.java
@@ -19,24 +19,27 @@ package android.view;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import android.content.Context;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
-import androidx.benchmark.BenchmarkState;
-import androidx.benchmark.junit4.BenchmarkRule;
-import androidx.test.filters.SmallTest;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
-@SmallTest
+@LargeTest
+@RunWith(AndroidJUnit4.class)
public class ViewConfigurationPerfTest {
@Rule
- public final BenchmarkRule mBenchmarkRule = new BenchmarkRule();
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
private final Context mContext = getInstrumentation().getTargetContext();
@Test
public void testGet_newViewConfiguration() {
- final BenchmarkState state = mBenchmarkRule.getState();
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
state.pauseTiming();
@@ -50,7 +53,7 @@ public class ViewConfigurationPerfTest {
@Test
public void testGet_cachedViewConfiguration() {
- final BenchmarkState state = mBenchmarkRule.getState();
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
// Do `get` once to make sure there's something cached.
ViewConfiguration.get(mContext);
@@ -58,4 +61,265 @@ public class ViewConfigurationPerfTest {
ViewConfiguration.get(mContext);
}
}
+
+ @Test
+ public void testGetPressedStateDuration_unCached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ // Reset any caches.
+ ViewConfiguration.resetCacheForTesting();
+ state.resumeTiming();
+
+ ViewConfiguration.getPressedStateDuration();
+ }
+ }
+
+ @Test
+ public void testGetPressedStateDuration_cached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ // Do `get` once to make sure the value gets cached.
+ ViewConfiguration.getPressedStateDuration();
+
+ while (state.keepRunning()) {
+ ViewConfiguration.getPressedStateDuration();
+ }
+ }
+
+ @Test
+ public void testGetTapTimeout_unCached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ // Reset any caches.
+ ViewConfiguration.resetCacheForTesting();
+ state.resumeTiming();
+
+ ViewConfiguration.getTapTimeout();
+ }
+ }
+
+ @Test
+ public void testGetTapTimeout_cached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ // Do `get` once to make sure the value gets cached.
+ ViewConfiguration.getTapTimeout();
+
+ while (state.keepRunning()) {
+ ViewConfiguration.getTapTimeout();
+ }
+ }
+
+ @Test
+ public void testGetJumpTapTimeout_unCached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ // Reset any caches.
+ ViewConfiguration.resetCacheForTesting();
+ state.resumeTiming();
+
+ ViewConfiguration.getJumpTapTimeout();
+ }
+ }
+
+ @Test
+ public void testGetJumpTapTimeout_cached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ // Do `get` once to make sure the value gets cached.
+ ViewConfiguration.getJumpTapTimeout();
+
+ while (state.keepRunning()) {
+ ViewConfiguration.getJumpTapTimeout();
+ }
+ }
+
+ @Test
+ public void testGetDoubleTapTimeout_unCached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ // Reset any caches.
+ ViewConfiguration.resetCacheForTesting();
+ state.resumeTiming();
+
+ ViewConfiguration.getDoubleTapTimeout();
+ }
+ }
+
+ @Test
+ public void testGetDoubleTapTimeout_cached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ // Do `get` once to make sure the value gets cached.
+ ViewConfiguration.getDoubleTapTimeout();
+
+ while (state.keepRunning()) {
+ ViewConfiguration.getDoubleTapTimeout();
+ }
+ }
+
+ @Test
+ public void testGetDoubleTapMinTime_unCached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ // Reset any caches.
+ ViewConfiguration.resetCacheForTesting();
+ state.resumeTiming();
+
+ ViewConfiguration.getDoubleTapMinTime();
+ }
+ }
+
+ @Test
+ public void testGetDoubleTapMinTime_cached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ // Do `get` once to make sure the value gets cached.
+ ViewConfiguration.getDoubleTapMinTime();
+
+ while (state.keepRunning()) {
+ ViewConfiguration.getDoubleTapMinTime();
+ }
+ }
+
+ @Test
+ public void testGetZoomControlsTimeout_unCached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ // Reset any caches.
+ ViewConfiguration.resetCacheForTesting();
+ state.resumeTiming();
+
+ ViewConfiguration.getZoomControlsTimeout();
+ }
+ }
+
+ @Test
+ public void testGetZoomControlsTimeout_cached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ // Do `get` once to make sure the value gets cached.
+ ViewConfiguration.getZoomControlsTimeout();
+
+ while (state.keepRunning()) {
+ ViewConfiguration.getZoomControlsTimeout();
+ }
+ }
+
+ @Test
+ public void testGetLongPressTimeout() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ ViewConfiguration.getLongPressTimeout();
+ }
+ }
+
+ @Test
+ public void testGetMultiPressTimeout() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ ViewConfiguration.getMultiPressTimeout();
+ }
+ }
+
+ @Test
+ public void testGetKeyRepeatTimeout() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ ViewConfiguration.getKeyRepeatTimeout();
+ }
+ }
+
+ @Test
+ public void testGetKeyRepeatDelay() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ ViewConfiguration.getKeyRepeatDelay();
+ }
+ }
+
+ @Test
+ public void testGetHoverTapSlop_unCached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ // Reset any caches.
+ ViewConfiguration.resetCacheForTesting();
+ state.resumeTiming();
+
+ ViewConfiguration.getHoverTapSlop();
+ }
+ }
+
+ @Test
+ public void testGetHoverTapSlop_cached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ // Do `get` once to make sure the value gets cached.
+ ViewConfiguration.getHoverTapSlop();
+
+ while (state.keepRunning()) {
+ ViewConfiguration.getHoverTapSlop();
+ }
+ }
+
+ @Test
+ public void testGetScrollFriction_unCached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ // Reset any caches.
+ ViewConfiguration.resetCacheForTesting();
+ state.resumeTiming();
+
+ ViewConfiguration.getScrollFriction();
+ }
+ }
+
+ @Test
+ public void testGetScrollFriction_cached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ // Do `get` once to make sure the value gets cached.
+ ViewConfiguration.getScrollFriction();
+
+ while (state.keepRunning()) {
+ ViewConfiguration.getScrollFriction();
+ }
+ }
+
+ @Test
+ public void testGetDefaultActionModeHideDuration_unCached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ // Reset any caches.
+ ViewConfiguration.resetCacheForTesting();
+ state.resumeTiming();
+
+ ViewConfiguration.getDefaultActionModeHideDuration();
+ }
+ }
+
+ @Test
+ public void testGetDefaultActionModeHideDuration_cached() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ // Do `get` once to make sure the value gets cached.
+ ViewConfiguration.getDefaultActionModeHideDuration();
+
+ while (state.keepRunning()) {
+ ViewConfiguration.getDefaultActionModeHideDuration();
+ }
+ }
}
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index 9871d713178e..ab8131ba5126 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -264,6 +264,8 @@ public class AppStandbyController
@GuardedBy("mCarrierPrivilegedLock")
private boolean mHaveCarrierPrivilegedApps;
+ private final boolean mHasFeatureTelephonySubscription;
+
/** List of carrier-privileged apps that should be excluded from standby */
@GuardedBy("mCarrierPrivilegedLock")
private List<String> mCarrierPrivilegedApps;
@@ -603,6 +605,8 @@ public class AppStandbyController
mContext = mInjector.getContext();
mHandler = new AppStandbyHandler(mInjector.getLooper());
mPackageManager = mContext.getPackageManager();
+ mHasFeatureTelephonySubscription = mPackageManager.hasSystemFeature(
+ PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION);
DeviceStateReceiver deviceStateReceiver = new DeviceStateReceiver();
IntentFilter deviceStates = new IntentFilter(BatteryManager.ACTION_CHARGING);
@@ -1515,7 +1519,7 @@ public class AppStandbyController
}
// Check this last, as it can be the most expensive check
- if (isCarrierApp(packageName)) {
+ if (mHasFeatureTelephonySubscription && isCarrierApp(packageName)) {
return STANDBY_BUCKET_EXEMPTED;
}
diff --git a/cmds/idmap2/Android.bp b/cmds/idmap2/Android.bp
index d9ff19051de9..f6bee52661da 100644
--- a/cmds/idmap2/Android.bp
+++ b/cmds/idmap2/Android.bp
@@ -348,6 +348,7 @@ filegroup {
"idmap2d/aidl/core/android/os/FabricatedOverlayInternal.aidl",
"idmap2d/aidl/core/android/os/FabricatedOverlayInternalEntry.aidl",
"idmap2d/aidl/core/android/os/FabricatedOverlayInfo.aidl",
+ "idmap2d/aidl/core/android/os/OverlayConstraint.aidl",
],
path: "idmap2d/aidl/core/",
}
diff --git a/cmds/idmap2/idmap2/Create.cpp b/cmds/idmap2/idmap2/Create.cpp
index d5f1b895facf..d94940131c8b 100644
--- a/cmds/idmap2/idmap2/Create.cpp
+++ b/cmds/idmap2/idmap2/Create.cpp
@@ -35,6 +35,7 @@ using android::idmap2::BinaryStreamVisitor;
using android::idmap2::CommandLineOptions;
using android::idmap2::Error;
using android::idmap2::Idmap;
+using android::idmap2::IdmapConstraints;
using android::idmap2::OverlayResourceContainer;
using android::idmap2::Result;
using android::idmap2::TargetResourceContainer;
@@ -104,8 +105,10 @@ Result<Unit> Create(const std::vector<std::string>& args) {
return Error("failed to load apk overlay '%s'", overlay_apk_path.c_str());
}
+ // TODO(b/371801644): Add command-line support for RRO constraints.
+ auto constraints = std::make_unique<const IdmapConstraints>();
const auto idmap = Idmap::FromContainers(**target, **overlay, overlay_name, fulfilled_policies,
- !ignore_overlayable);
+ !ignore_overlayable, std::move(constraints));
if (!idmap) {
return Error(idmap.GetError(), "failed to create idmap");
}
diff --git a/cmds/idmap2/idmap2/CreateMultiple.cpp b/cmds/idmap2/idmap2/CreateMultiple.cpp
index 2608c69be66f..70a2ed1940b2 100644
--- a/cmds/idmap2/idmap2/CreateMultiple.cpp
+++ b/cmds/idmap2/idmap2/CreateMultiple.cpp
@@ -39,6 +39,7 @@ using android::idmap2::BinaryStreamVisitor;
using android::idmap2::CommandLineOptions;
using android::idmap2::Error;
using android::idmap2::Idmap;
+using android::idmap2::IdmapConstraints;
using android::idmap2::OverlayResourceContainer;
using android::idmap2::Result;
using android::idmap2::TargetResourceContainer;
@@ -115,8 +116,11 @@ Result<Unit> CreateMultiple(const std::vector<std::string>& args) {
continue;
}
+ // TODO(b/371801644): Add command-line support for RRO constraints.
+ auto constraints = std::make_unique<const IdmapConstraints>();
const auto idmap =
- Idmap::FromContainers(**target, **overlay, "", fulfilled_policies, !ignore_overlayable);
+ Idmap::FromContainers(**target, **overlay, "", fulfilled_policies, !ignore_overlayable,
+ std::move(constraints));
if (!idmap) {
LOG(WARNING) << "failed to create idmap";
continue;
diff --git a/cmds/idmap2/idmap2d/Idmap2Service.cpp b/cmds/idmap2/idmap2d/Idmap2Service.cpp
index 6902d6db6751..2495c55cc065 100644
--- a/cmds/idmap2/idmap2d/Idmap2Service.cpp
+++ b/cmds/idmap2/idmap2d/Idmap2Service.cpp
@@ -46,6 +46,8 @@ using android::binder::Status;
using android::idmap2::BinaryStreamVisitor;
using android::idmap2::FabricatedOverlayContainer;
using android::idmap2::Idmap;
+using android::idmap2::IdmapConstraint;
+using android::idmap2::IdmapConstraints;
using android::idmap2::IdmapHeader;
using android::idmap2::OverlayResourceContainer;
using android::idmap2::PrettyPrintVisitor;
@@ -74,6 +76,18 @@ PolicyBitmask ConvertAidlArgToPolicyBitmask(int32_t arg) {
return static_cast<PolicyBitmask>(arg);
}
+std::unique_ptr<const IdmapConstraints> ConvertAidlConstraintsToIdmapConstraints(
+ const std::vector<android::os::OverlayConstraint>& constraints) {
+ auto idmapConstraints = std::make_unique<IdmapConstraints>();
+ for (const auto& constraint : constraints) {
+ IdmapConstraint idmapConstraint{};
+ idmapConstraint.constraint_type = constraint.type;
+ idmapConstraint.constraint_value = constraint.value;
+ idmapConstraints->constraints.insert(idmapConstraint);
+ }
+ return idmapConstraints;
+}
+
} // namespace
namespace android::os {
@@ -113,6 +127,7 @@ Status Idmap2Service::removeIdmap(const std::string& overlay_path, int32_t user_
Status Idmap2Service::verifyIdmap(const std::string& target_path, const std::string& overlay_path,
const std::string& overlay_name, int32_t fulfilled_policies,
bool enforce_overlayable, int32_t user_id ATTRIBUTE_UNUSED,
+ const std::vector<os::OverlayConstraint>& constraints,
bool* _aidl_return) {
SYSTRACE << "Idmap2Service::verifyIdmap " << overlay_path;
assert(_aidl_return);
@@ -120,12 +135,19 @@ Status Idmap2Service::verifyIdmap(const std::string& target_path, const std::str
const std::string idmap_path = Idmap::CanonicalIdmapPathFor(kIdmapCacheDir, overlay_path);
std::ifstream fin(idmap_path);
const std::unique_ptr<const IdmapHeader> header = IdmapHeader::FromBinaryStream(fin);
+ const std::unique_ptr<const IdmapConstraints> oldConstraints =
+ IdmapConstraints::FromBinaryStream(fin);
fin.close();
if (!header) {
*_aidl_return = false;
LOG(WARNING) << "failed to parse idmap header of '" << idmap_path << "'";
return ok();
}
+ if (!oldConstraints) {
+ *_aidl_return = false;
+ LOG(WARNING) << "failed to parse idmap constraints of '" << idmap_path << "'";
+ return ok();
+ }
const auto target = GetTargetContainer(target_path);
if (!target) {
@@ -145,7 +167,10 @@ Status Idmap2Service::verifyIdmap(const std::string& target_path, const std::str
header->IsUpToDate(*GetPointer(*target), **overlay, overlay_name,
ConvertAidlArgToPolicyBitmask(fulfilled_policies), enforce_overlayable);
- *_aidl_return = static_cast<bool>(up_to_date);
+ std::unique_ptr<const IdmapConstraints> newConstraints =
+ ConvertAidlConstraintsToIdmapConstraints(constraints);
+
+ *_aidl_return = static_cast<bool>(up_to_date && (*oldConstraints == *newConstraints));
if (!up_to_date) {
LOG(WARNING) << "idmap '" << idmap_path
<< "' not up to date : " << up_to_date.GetErrorMessage();
@@ -156,6 +181,7 @@ Status Idmap2Service::verifyIdmap(const std::string& target_path, const std::str
Status Idmap2Service::createIdmap(const std::string& target_path, const std::string& overlay_path,
const std::string& overlay_name, int32_t fulfilled_policies,
bool enforce_overlayable, int32_t user_id ATTRIBUTE_UNUSED,
+ const std::vector<os::OverlayConstraint>& constraints,
std::optional<std::string>* _aidl_return) {
assert(_aidl_return);
SYSTRACE << "Idmap2Service::createIdmap " << target_path << " " << overlay_path;
@@ -186,8 +212,11 @@ Status Idmap2Service::createIdmap(const std::string& target_path, const std::str
return error("failed to load apk overlay '%s'" + overlay_path);
}
+ std::unique_ptr<const IdmapConstraints> idmapConstraints =
+ ConvertAidlConstraintsToIdmapConstraints(constraints);
const auto idmap = Idmap::FromContainers(*GetPointer(*target), **overlay, overlay_name,
- policy_bitmask, enforce_overlayable);
+ policy_bitmask, enforce_overlayable,
+ std::move(idmapConstraints));
if (!idmap) {
return error(idmap.GetErrorMessage());
}
diff --git a/cmds/idmap2/idmap2d/Idmap2Service.h b/cmds/idmap2/idmap2d/Idmap2Service.h
index 272ec6be3bac..344a77f5581f 100644
--- a/cmds/idmap2/idmap2d/Idmap2Service.h
+++ b/cmds/idmap2/idmap2d/Idmap2Service.h
@@ -20,6 +20,7 @@
#include <android-base/unique_fd.h>
#include <android/os/BnIdmap2.h>
#include <android/os/FabricatedOverlayInfo.h>
+#include <android/os/OverlayConstraint.h>
#include <binder/BinderService.h>
#include <idmap2/ResourceContainer.h>
#include <idmap2/Result.h>
@@ -49,11 +50,13 @@ class Idmap2Service : public BinderService<Idmap2Service>, public BnIdmap2 {
binder::Status verifyIdmap(const std::string& target_path, const std::string& overlay_path,
const std::string& overlay_name, int32_t fulfilled_policies,
bool enforce_overlayable, int32_t user_id,
+ const std::vector<os::OverlayConstraint>& constraints,
bool* _aidl_return) override;
binder::Status createIdmap(const std::string& target_path, const std::string& overlay_path,
const std::string& overlay_name, int32_t fulfilled_policies,
bool enforce_overlayable, int32_t user_id,
+ const std::vector<os::OverlayConstraint>& constraints,
std::optional<std::string>* _aidl_return) override;
binder::Status createFabricatedOverlay(
diff --git a/cmds/idmap2/idmap2d/aidl/core/android/os/OverlayConstraint.aidl b/cmds/idmap2/idmap2d/aidl/core/android/os/OverlayConstraint.aidl
new file mode 100644
index 000000000000..8fce3d6567ab
--- /dev/null
+++ b/cmds/idmap2/idmap2d/aidl/core/android/os/OverlayConstraint.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+/**
+ * @hide
+ */
+parcelable OverlayConstraint {
+ int type;
+ int value;
+} \ No newline at end of file
diff --git a/cmds/idmap2/idmap2d/aidl/services/android/os/IIdmap2.aidl b/cmds/idmap2/idmap2d/aidl/services/android/os/IIdmap2.aidl
index 2bbfba97a6c6..4f4f075a0e63 100644
--- a/cmds/idmap2/idmap2d/aidl/services/android/os/IIdmap2.aidl
+++ b/cmds/idmap2/idmap2d/aidl/services/android/os/IIdmap2.aidl
@@ -18,6 +18,7 @@ package android.os;
import android.os.FabricatedOverlayInfo;
import android.os.FabricatedOverlayInternal;
+import android.os.OverlayConstraint;
/**
* @hide
@@ -30,13 +31,15 @@ interface IIdmap2 {
@utf8InCpp String overlayName,
int fulfilledPolicies,
boolean enforceOverlayable,
- int userId);
+ int userId,
+ in OverlayConstraint[] constraints);
@nullable @utf8InCpp String createIdmap(@utf8InCpp String targetApkPath,
@utf8InCpp String overlayApkPath,
@utf8InCpp String overlayName,
int fulfilledPolicies,
boolean enforceOverlayable,
- int userId);
+ int userId,
+ in OverlayConstraint[] constraints);
@nullable FabricatedOverlayInfo createFabricatedOverlay(in FabricatedOverlayInternal overlay);
boolean deleteFabricatedOverlay(@utf8InCpp String path);
diff --git a/cmds/idmap2/include/idmap2/BinaryStreamVisitor.h b/cmds/idmap2/include/idmap2/BinaryStreamVisitor.h
index 57af1b61c300..3009293bc4ab 100644
--- a/cmds/idmap2/include/idmap2/BinaryStreamVisitor.h
+++ b/cmds/idmap2/include/idmap2/BinaryStreamVisitor.h
@@ -32,6 +32,7 @@ class BinaryStreamVisitor : public Visitor {
~BinaryStreamVisitor() override = default;
void visit(const Idmap& idmap) override;
void visit(const IdmapHeader& header) override;
+ void visit(const IdmapConstraints& constraints) override;
void visit(const IdmapData& data) override;
void visit(const IdmapData::Header& header) override;
diff --git a/cmds/idmap2/include/idmap2/Idmap.h b/cmds/idmap2/include/idmap2/Idmap.h
index b0ba01957ab6..1f15daf1ba47 100644
--- a/cmds/idmap2/include/idmap2/Idmap.h
+++ b/cmds/idmap2/include/idmap2/Idmap.h
@@ -17,10 +17,11 @@
/*
* # idmap file format (current version)
*
- * idmap := header data*
+ * idmap := header constraints_count constraint* data*
* header := magic version target_crc overlay_crc fulfilled_policies
* enforce_overlayable target_path overlay_path overlay_name
* debug_info
+ * constraints := constraint_type constraint_value
* data := data_header target_entries target_inline_entries
target_inline_entry_value* config* overlay_entries string_pool
* data_header := target_entry_count target_inline_entry_count
@@ -67,6 +68,9 @@
* value_type := <uint8_t>
* value_data := <uint32_t>
* version := <uint32_t>
+ * constraints_count := <uint32_t>
+ * constraint_type := <uint32_t>
+ * constraint_value := <uint32_t>
*/
#ifndef IDMAP2_INCLUDE_IDMAP2_IDMAP_H_
@@ -76,6 +80,7 @@
#include <memory>
#include <string>
#include <string_view>
+#include <unordered_set>
#include <vector>
#include "android-base/macros.h"
@@ -171,6 +176,33 @@ class IdmapHeader {
friend Idmap;
DISALLOW_COPY_AND_ASSIGN(IdmapHeader);
};
+
+struct IdmapConstraint {
+ // Constraint type can be TYPE_DISPLAY_ID or TYP_DEVICE_ID, please refer
+ // to ConstraintType in OverlayConstraint.java
+ uint32_t constraint_type;
+ uint32_t constraint_value;
+
+ bool operator==(const IdmapConstraint&) const = default;
+};
+
+struct IdmapConstraints {
+ static std::unique_ptr<const IdmapConstraints> FromBinaryStream(std::istream& stream);
+
+ struct Hash {
+ static std::size_t operator()(const IdmapConstraint& constraint) {
+ return std::hash<int>()(constraint.constraint_type) * 31
+ + std::hash<int>()(constraint.constraint_value);
+ }
+ };
+
+ bool operator == (const IdmapConstraints& constraints) const = default;
+
+ void accept(Visitor* v) const;
+
+ std::unordered_set<IdmapConstraint, Hash> constraints;
+};
+
class IdmapData {
public:
class Header {
@@ -286,12 +318,16 @@ class Idmap {
static Result<std::unique_ptr<const Idmap>> FromContainers(
const TargetResourceContainer& target, const OverlayResourceContainer& overlay,
const std::string& overlay_name, const PolicyBitmask& fulfilled_policies,
- bool enforce_overlayable);
+ bool enforce_overlayable, std::unique_ptr<const IdmapConstraints>&& constraints);
const std::unique_ptr<const IdmapHeader>& GetHeader() const {
return header_;
}
+ const std::unique_ptr<const IdmapConstraints>& GetConstraints() const {
+ return constraints_;
+ }
+
const std::vector<std::unique_ptr<const IdmapData>>& GetData() const {
return data_;
}
@@ -302,6 +338,7 @@ class Idmap {
Idmap() = default;
std::unique_ptr<const IdmapHeader> header_;
+ std::unique_ptr<const IdmapConstraints> constraints_;
std::vector<std::unique_ptr<const IdmapData>> data_;
DISALLOW_COPY_AND_ASSIGN(Idmap);
@@ -312,6 +349,7 @@ class Visitor {
virtual ~Visitor() = default;
virtual void visit(const Idmap& idmap) = 0;
virtual void visit(const IdmapHeader& header) = 0;
+ virtual void visit(const IdmapConstraints& constraints) = 0;
virtual void visit(const IdmapData& data) = 0;
virtual void visit(const IdmapData::Header& header) = 0;
};
diff --git a/cmds/idmap2/include/idmap2/PrettyPrintVisitor.h b/cmds/idmap2/include/idmap2/PrettyPrintVisitor.h
index ed18d9cbf20f..033ef85f5133 100644
--- a/cmds/idmap2/include/idmap2/PrettyPrintVisitor.h
+++ b/cmds/idmap2/include/idmap2/PrettyPrintVisitor.h
@@ -36,6 +36,7 @@ class PrettyPrintVisitor : public Visitor {
~PrettyPrintVisitor() override = default;
void visit(const Idmap& idmap) override;
void visit(const IdmapHeader& header) override;
+ void visit(const IdmapConstraints& constraints) override;
void visit(const IdmapData& data) override;
void visit(const IdmapData::Header& header) override;
diff --git a/cmds/idmap2/include/idmap2/RawPrintVisitor.h b/cmds/idmap2/include/idmap2/RawPrintVisitor.h
index 849ba11aacff..bd27c0d62c0d 100644
--- a/cmds/idmap2/include/idmap2/RawPrintVisitor.h
+++ b/cmds/idmap2/include/idmap2/RawPrintVisitor.h
@@ -37,6 +37,7 @@ class RawPrintVisitor : public Visitor {
~RawPrintVisitor() override = default;
void visit(const Idmap& idmap) override;
void visit(const IdmapHeader& header) override;
+ void visit(const IdmapConstraints& constraints) override;
void visit(const IdmapData& data) override;
void visit(const IdmapData::Header& header) override;
diff --git a/cmds/idmap2/libidmap2/BinaryStreamVisitor.cpp b/cmds/idmap2/libidmap2/BinaryStreamVisitor.cpp
index 00ef0c7f8cf0..b029aea1a1bf 100644
--- a/cmds/idmap2/libidmap2/BinaryStreamVisitor.cpp
+++ b/cmds/idmap2/libidmap2/BinaryStreamVisitor.cpp
@@ -63,6 +63,14 @@ void BinaryStreamVisitor::visit(const IdmapHeader& header) {
WriteString(header.GetDebugInfo());
}
+void BinaryStreamVisitor::visit(const IdmapConstraints& constraints) {
+ Write32(static_cast<uint32_t>(constraints.constraints.size()));
+ for (const auto& constraint : constraints.constraints) {
+ Write32(constraint.constraint_type);
+ Write32(constraint.constraint_value);
+ }
+}
+
void BinaryStreamVisitor::visit(const IdmapData& data) {
for (const auto& target_entry : data.GetTargetEntries()) {
Write32(target_entry.target_id);
diff --git a/cmds/idmap2/libidmap2/Idmap.cpp b/cmds/idmap2/libidmap2/Idmap.cpp
index 7680109f1d54..556ca228e83d 100644
--- a/cmds/idmap2/libidmap2/Idmap.cpp
+++ b/cmds/idmap2/libidmap2/Idmap.cpp
@@ -182,6 +182,26 @@ Result<Unit> IdmapHeader::IsUpToDate(const std::string& target_path,
return Unit{};
}
+std::unique_ptr<const IdmapConstraints> IdmapConstraints::FromBinaryStream(std::istream& stream) {
+ auto idmap_constraints = std::make_unique<IdmapConstraints>();
+ uint32_t count = 0;
+ if (!Read32(stream, &count)) {
+ return nullptr;
+ }
+ for (size_t i = 0; i < count; i++) {
+ IdmapConstraint constraint{};
+ if (!Read32(stream, &constraint.constraint_type)) {
+ return nullptr;
+ }
+ if (!Read32(stream, &constraint.constraint_value)) {
+ return nullptr;
+ }
+ idmap_constraints->constraints.insert(constraint);
+ }
+
+ return idmap_constraints;
+}
+
std::unique_ptr<const IdmapData::Header> IdmapData::Header::FromBinaryStream(std::istream& stream) {
std::unique_ptr<IdmapData::Header> idmap_data_header(new IdmapData::Header());
if (!Read32(stream, &idmap_data_header->target_entry_count) ||
@@ -315,6 +335,10 @@ Result<std::unique_ptr<const Idmap>> Idmap::FromBinaryStream(std::istream& strea
if (!idmap->header_) {
return Error("failed to parse idmap header");
}
+ idmap->constraints_ = IdmapConstraints::FromBinaryStream(stream);
+ if (!idmap->constraints_) {
+ return Error("failed to parse idmap constraints");
+ }
// idmap version 0x01 does not specify the number of data blocks that follow
// the idmap header; assume exactly one data block
@@ -374,10 +398,9 @@ Result<std::unique_ptr<const IdmapData>> IdmapData::FromResourceMapping(
}
Result<std::unique_ptr<const Idmap>> Idmap::FromContainers(const TargetResourceContainer& target,
- const OverlayResourceContainer& overlay,
- const std::string& overlay_name,
- const PolicyBitmask& fulfilled_policies,
- bool enforce_overlayable) {
+ const OverlayResourceContainer& overlay, const std::string& overlay_name,
+ const PolicyBitmask& fulfilled_policies, bool enforce_overlayable,
+ std::unique_ptr<const IdmapConstraints>&& constraints) {
SYSTRACE << "Idmap::FromApkAssets";
std::unique_ptr<IdmapHeader> header(new IdmapHeader());
header->magic_ = kIdmapMagic;
@@ -424,6 +447,11 @@ Result<std::unique_ptr<const Idmap>> Idmap::FromContainers(const TargetResourceC
header->debug_info_ = log_info.GetString();
idmap->header_ = std::move(header);
idmap->data_.push_back(std::move(*idmap_data));
+ if (constraints == nullptr) {
+ idmap->constraints_ = std::make_unique<IdmapConstraints>();
+ } else {
+ idmap->constraints_ = std::move(constraints);
+ }
return {std::move(idmap)};
}
@@ -433,6 +461,11 @@ void IdmapHeader::accept(Visitor* v) const {
v->visit(*this);
}
+void IdmapConstraints::accept(Visitor* v) const {
+ assert(v != nullptr);
+ v->visit(*this);
+}
+
void IdmapData::Header::accept(Visitor* v) const {
assert(v != nullptr);
v->visit(*this);
@@ -447,6 +480,7 @@ void IdmapData::accept(Visitor* v) const {
void Idmap::accept(Visitor* v) const {
assert(v != nullptr);
header_->accept(v);
+ constraints_->accept(v);
v->visit(*this);
auto end = data_.cend();
for (auto iter = data_.cbegin(); iter != end; ++iter) {
diff --git a/cmds/idmap2/libidmap2/PrettyPrintVisitor.cpp b/cmds/idmap2/libidmap2/PrettyPrintVisitor.cpp
index eb9458268dad..0ec31f4f63f6 100644
--- a/cmds/idmap2/libidmap2/PrettyPrintVisitor.cpp
+++ b/cmds/idmap2/libidmap2/PrettyPrintVisitor.cpp
@@ -58,6 +58,19 @@ void PrettyPrintVisitor::visit(const IdmapHeader& header) {
if (auto overlay = OverlayResourceContainer::FromPath(header.GetOverlayPath())) {
overlay_ = std::move(*overlay);
}
+}
+
+void PrettyPrintVisitor::visit(const IdmapConstraints& constraints) {
+ stream_ << "Constraints:" << '\n';
+ if (constraints.constraints.empty()) {
+ stream_ << TAB << "None\n";
+ } else {
+ for (const IdmapConstraint& constraint : constraints.constraints) {
+ stream_ << TAB
+ << base::StringPrintf("Type: %d, Value: %d\n", constraint.constraint_type,
+ constraint.constraint_value);
+ }
+ }
stream_ << "Mapping:" << '\n';
}
diff --git a/cmds/idmap2/libidmap2/RawPrintVisitor.cpp b/cmds/idmap2/libidmap2/RawPrintVisitor.cpp
index 9d04a7f87400..41a3da39d872 100644
--- a/cmds/idmap2/libidmap2/RawPrintVisitor.cpp
+++ b/cmds/idmap2/libidmap2/RawPrintVisitor.cpp
@@ -55,6 +55,14 @@ void RawPrintVisitor::visit(const IdmapHeader& header) {
}
}
+void RawPrintVisitor::visit(const IdmapConstraints &idmapConstraints) {
+ print(static_cast<uint32_t>(idmapConstraints.constraints.size()), "constraints count");
+ for (const auto& constraint : idmapConstraints.constraints) {
+ print(constraint.constraint_type, "constraint type");
+ print(constraint.constraint_value, "constraint value");
+ }
+}
+
void RawPrintVisitor::visit(const IdmapData& data ATTRIBUTE_UNUSED) {
for (auto& target_entry : data.GetTargetEntries()) {
Result<std::string> target_name(Error(""));
diff --git a/cmds/idmap2/self_targeting/SelfTargeting.cpp b/cmds/idmap2/self_targeting/SelfTargeting.cpp
index 7f9c4686c55a..26888ab17d66 100644
--- a/cmds/idmap2/self_targeting/SelfTargeting.cpp
+++ b/cmds/idmap2/self_targeting/SelfTargeting.cpp
@@ -31,6 +31,7 @@ using PolicyBitmask = android::ResTable_overlayable_policy_header::PolicyBitmask
using PolicyFlags = android::ResTable_overlayable_policy_header::PolicyFlags;
using android::idmap2::BinaryStreamVisitor;
using android::idmap2::Idmap;
+using android::idmap2::IdmapConstraints;
using android::idmap2::OverlayResourceContainer;
namespace android::self_targeting {
@@ -155,9 +156,10 @@ CreateIdmapFile(std::string& out_err, const std::string& targetPath, const std::
// Overlay self target process. Only allow self-targeting types.
const auto fulfilled_policies = GetFulfilledPolicy(isSystem, isVendor, isProduct,
isTargetSignature, isOdm, isOem);
-
+ auto constraints = std::make_unique<const IdmapConstraints>();
const auto idmap = Idmap::FromContainers(**target, **overlay, overlayName,
- fulfilled_policies, true /* enforce_overlayable */);
+ fulfilled_policies, true /* enforce_overlayable */,
+ std::move(constraints));
if (!idmap) {
out_err = base::StringPrintf("Failed to create idmap because of %s",
idmap.GetErrorMessage().c_str());
diff --git a/cmds/idmap2/tests/BinaryStreamVisitorTests.cpp b/cmds/idmap2/tests/BinaryStreamVisitorTests.cpp
index f1eeab9c803b..76cccb556ca2 100644
--- a/cmds/idmap2/tests/BinaryStreamVisitorTests.cpp
+++ b/cmds/idmap2/tests/BinaryStreamVisitorTests.cpp
@@ -58,6 +58,8 @@ TEST(BinaryStreamVisitorTests, CreateBinaryStreamViaBinaryStreamVisitor) {
ASSERT_EQ(idmap1->GetData().size(), 1U);
ASSERT_EQ(idmap1->GetData().size(), idmap2->GetData().size());
+ ASSERT_EQ(idmap1->GetConstraints()->constraints, idmap2->GetConstraints()->constraints);
+
const std::vector<std::unique_ptr<const IdmapData>>& data_blocks1 = idmap1->GetData();
ASSERT_EQ(data_blocks1.size(), 1U);
const std::unique_ptr<const IdmapData>& data1 = data_blocks1[0];
diff --git a/cmds/idmap2/tests/Idmap2BinaryTests.cpp b/cmds/idmap2/tests/Idmap2BinaryTests.cpp
index 5a7fcd519cfd..760bbb3f72ba 100644
--- a/cmds/idmap2/tests/Idmap2BinaryTests.cpp
+++ b/cmds/idmap2/tests/Idmap2BinaryTests.cpp
@@ -105,6 +105,7 @@ TEST_F(Idmap2BinaryTests, Create) {
fin.close();
ASSERT_TRUE(idmap);
+ ASSERT_EQ((*idmap)->GetConstraints()->constraints.size(), 0);
ASSERT_IDMAP(**idmap, GetTargetApkPath(), GetOverlayApkPath());
unlink(GetIdmapPath().c_str());
diff --git a/cmds/idmap2/tests/IdmapTests.cpp b/cmds/idmap2/tests/IdmapTests.cpp
index 7093614f4047..4de2a6b7c125 100644
--- a/cmds/idmap2/tests/IdmapTests.cpp
+++ b/cmds/idmap2/tests/IdmapTests.cpp
@@ -68,7 +68,7 @@ TEST(IdmapTests, CreateIdmapHeaderFromBinaryStream) {
std::unique_ptr<const IdmapHeader> header = IdmapHeader::FromBinaryStream(stream);
ASSERT_THAT(header, NotNull());
ASSERT_EQ(header->GetMagic(), 0x504d4449U);
- ASSERT_EQ(header->GetVersion(), 10);
+ ASSERT_EQ(header->GetVersion(), 11);
ASSERT_EQ(header->GetTargetCrc(), 0x1234U);
ASSERT_EQ(header->GetOverlayCrc(), 0x5678U);
ASSERT_EQ(header->GetFulfilledPolicies(), 0x11);
@@ -96,6 +96,19 @@ TEST(IdmapTests, IdmapFailParsingDifferentMagic) {
ASSERT_FALSE(Idmap::FromBinaryStream(stream));
}
+TEST(IdmapTests, CreateIdmapConstraintsFromBinaryStream) {
+ std::string raw(reinterpret_cast<const char*>(kIdmapRawData), kIdmapRawDataLen);
+ std::istringstream stream(raw);
+ std::unique_ptr<const IdmapHeader> header = IdmapHeader::FromBinaryStream(stream);
+ std::unique_ptr<const IdmapConstraints> constraints = IdmapConstraints::FromBinaryStream(stream);
+ ASSERT_THAT(constraints, NotNull());
+ ASSERT_EQ(constraints->constraints.size(), 2);
+ IdmapConstraint constraint1{.constraint_type = 0, .constraint_value = 1};
+ IdmapConstraint constraint2{.constraint_type = 1, .constraint_value = 2};
+ ASSERT_NE(constraints->constraints.find(constraint1), constraints->constraints.end());
+ ASSERT_NE(constraints->constraints.find(constraint2), constraints->constraints.end());
+}
+
TEST(IdmapTests, CreateIdmapDataHeaderFromBinaryStream) {
const size_t offset = kIdmapRawDataOffset;
std::string raw(reinterpret_cast<const char*>(kIdmapRawData + offset), kIdmapRawDataLen - offset);
@@ -143,7 +156,7 @@ TEST(IdmapTests, CreateIdmapFromBinaryStream) {
ASSERT_THAT(idmap->GetHeader(), NotNull());
ASSERT_EQ(idmap->GetHeader()->GetMagic(), 0x504d4449U);
- ASSERT_EQ(idmap->GetHeader()->GetVersion(), 10);
+ ASSERT_EQ(idmap->GetHeader()->GetVersion(), 11);
ASSERT_EQ(idmap->GetHeader()->GetTargetCrc(), 0x1234U);
ASSERT_EQ(idmap->GetHeader()->GetOverlayCrc(), 0x5678U);
ASSERT_EQ(idmap->GetHeader()->GetFulfilledPolicies(), kIdmapRawDataPolicies);
@@ -195,16 +208,17 @@ TEST(IdmapTests, CreateIdmapHeaderFromApkAssets) {
auto overlay = OverlayResourceContainer::FromPath(overlay_apk_path);
ASSERT_TRUE(overlay);
+ auto constraints = std::make_unique<const IdmapConstraints>();
auto idmap_result = Idmap::FromContainers(
**target, **overlay, TestConstants::OVERLAY_NAME_ALL_POLICIES, PolicyFlags::PUBLIC,
- /* enforce_overlayable */ true);
+ /* enforce_overlayable */ true, std::move(constraints));
ASSERT_TRUE(idmap_result) << idmap_result.GetErrorMessage();
auto& idmap = *idmap_result;
ASSERT_THAT(idmap, NotNull());
ASSERT_THAT(idmap->GetHeader(), NotNull());
ASSERT_EQ(idmap->GetHeader()->GetMagic(), 0x504d4449U);
- ASSERT_EQ(idmap->GetHeader()->GetVersion(), 10);
+ ASSERT_EQ(idmap->GetHeader()->GetVersion(), 11);
ASSERT_EQ(idmap->GetHeader()->GetTargetCrc(), android::idmap2::TestConstants::TARGET_CRC);
ASSERT_EQ(idmap->GetHeader()->GetOverlayCrc(), android::idmap2::TestConstants::OVERLAY_CRC);
ASSERT_EQ(idmap->GetHeader()->GetFulfilledPolicies(), PolicyFlags::PUBLIC);
@@ -238,9 +252,10 @@ TEST(IdmapTests, CreateIdmapDataFromApkAssets) {
auto overlay = OverlayResourceContainer::FromPath(overlay_apk_path);
ASSERT_TRUE(overlay);
+ auto constraints = std::make_unique<const IdmapConstraints>();
auto idmap_result = Idmap::FromContainers(
**target, **overlay, TestConstants::OVERLAY_NAME_DEFAULT, PolicyFlags::PUBLIC,
- /* enforce_overlayable */ true);
+ /* enforce_overlayable */ true, std::move(constraints));
ASSERT_TRUE(idmap_result) << idmap_result.GetErrorMessage();
auto& idmap = *idmap_result;
ASSERT_THAT(idmap, NotNull());
@@ -296,8 +311,9 @@ TEST(IdmapTests, FabricatedOverlay) {
auto overlay = OverlayResourceContainer::FromPath(tf.path);
ASSERT_TRUE(overlay);
+ auto constraints = std::make_unique<const IdmapConstraints>();
auto idmap_result = Idmap::FromContainers(**target, **overlay, "SandTheme", PolicyFlags::PUBLIC,
- /* enforce_overlayable */ true);
+ /* enforce_overlayable */ true, std::move(constraints));
ASSERT_TRUE(idmap_result) << idmap_result.GetErrorMessage();
auto& idmap = *idmap_result;
ASSERT_THAT(idmap, NotNull());
@@ -341,13 +357,17 @@ TEST(IdmapTests, FailCreateIdmapInvalidName) {
ASSERT_TRUE(overlay);
{
+ auto constraints = std::make_unique<const IdmapConstraints>();
auto idmap_result = Idmap::FromContainers(**target, **overlay, "", PolicyFlags::PUBLIC,
- /* enforce_overlayable */ true);
+ /* enforce_overlayable */ true,
+ std::move(constraints));
ASSERT_FALSE(idmap_result);
}
{
+ auto constraints = std::make_unique<const IdmapConstraints>();
auto idmap_result = Idmap::FromContainers(**target, **overlay, "unknown", PolicyFlags::PUBLIC,
- /* enforce_overlayable */ true);
+ /* enforce_overlayable */ true,
+ std::move(constraints));
ASSERT_FALSE(idmap_result);
}
}
@@ -362,9 +382,10 @@ TEST(IdmapTests, CreateIdmapDataFromApkAssetsSharedLibOverlay) {
auto overlay = OverlayResourceContainer::FromPath(overlay_apk_path);
ASSERT_TRUE(overlay);
+ auto constraints = std::make_unique<const IdmapConstraints>();
auto idmap_result = Idmap::FromContainers(
**target, **overlay, TestConstants::OVERLAY_NAME_DEFAULT, PolicyFlags::PUBLIC,
- /* enforce_overlayable */ true);
+ /* enforce_overlayable */ true, std::move(constraints));
ASSERT_TRUE(idmap_result) << idmap_result.GetErrorMessage();
auto& idmap = *idmap_result;
ASSERT_THAT(idmap, NotNull());
@@ -634,6 +655,10 @@ class TestVisitor : public Visitor {
stream_ << "TestVisitor::visit(IdmapHeader)" << '\n';
}
+ void visit(const IdmapConstraints& idmap ATTRIBUTE_UNUSED) override {
+ stream_ << "TestVisitor::visit(IdmapConstraints)" << '\n';
+ }
+
void visit(const IdmapData& idmap ATTRIBUTE_UNUSED) override {
stream_ << "TestVisitor::visit(IdmapData)" << '\n';
}
@@ -659,6 +684,7 @@ TEST(IdmapTests, TestVisitor) {
ASSERT_EQ(test_stream.str(),
"TestVisitor::visit(IdmapHeader)\n"
+ "TestVisitor::visit(IdmapConstraints)\n"
"TestVisitor::visit(Idmap)\n"
"TestVisitor::visit(IdmapData::Header)\n"
"TestVisitor::visit(IdmapData)\n");
diff --git a/cmds/idmap2/tests/PrettyPrintVisitorTests.cpp b/cmds/idmap2/tests/PrettyPrintVisitorTests.cpp
index 3d3d82a8c7dd..2f42f798f64a 100644
--- a/cmds/idmap2/tests/PrettyPrintVisitorTests.cpp
+++ b/cmds/idmap2/tests/PrettyPrintVisitorTests.cpp
@@ -42,8 +42,10 @@ TEST(PrettyPrintVisitorTests, CreatePrettyPrintVisitor) {
auto overlay = OverlayResourceContainer::FromPath(overlay_apk_path);
ASSERT_TRUE(overlay);
+ auto constraints = std::make_unique<const IdmapConstraints>();
const auto idmap = Idmap::FromContainers(**target, **overlay, TestConstants::OVERLAY_NAME_DEFAULT,
- PolicyFlags::PUBLIC, /* enforce_overlayable */ true);
+ PolicyFlags::PUBLIC, /* enforce_overlayable */ true,
+ std::move(constraints));
ASSERT_TRUE(idmap);
std::stringstream stream;
diff --git a/cmds/idmap2/tests/RawPrintVisitorTests.cpp b/cmds/idmap2/tests/RawPrintVisitorTests.cpp
index 7fae1c64f014..d5aafe6b8d35 100644
--- a/cmds/idmap2/tests/RawPrintVisitorTests.cpp
+++ b/cmds/idmap2/tests/RawPrintVisitorTests.cpp
@@ -55,8 +55,10 @@ TEST(RawPrintVisitorTests, CreateRawPrintVisitor) {
auto overlay = OverlayResourceContainer::FromPath(overlay_apk_path);
ASSERT_TRUE(overlay);
+ auto constraints = std::make_unique<const IdmapConstraints>();
const auto idmap = Idmap::FromContainers(**target, **overlay, TestConstants::OVERLAY_NAME_DEFAULT,
- PolicyFlags::PUBLIC, /* enforce_overlayable */ true);
+ PolicyFlags::PUBLIC, /* enforce_overlayable */ true,
+ std::move(constraints));
ASSERT_TRUE(idmap);
std::stringstream stream;
@@ -64,7 +66,7 @@ TEST(RawPrintVisitorTests, CreateRawPrintVisitor) {
(*idmap)->accept(&visitor);
ASSERT_CONTAINS_REGEX(ADDRESS "504d4449 magic\n", stream.str());
- ASSERT_CONTAINS_REGEX(ADDRESS "0000000a version\n", stream.str());
+ ASSERT_CONTAINS_REGEX(ADDRESS "0000000b version\n", stream.str());
ASSERT_CONTAINS_REGEX(
StringPrintf(ADDRESS "%s target crc\n", android::idmap2::TestConstants::TARGET_CRC_STRING),
stream.str());
@@ -73,6 +75,7 @@ TEST(RawPrintVisitorTests, CreateRawPrintVisitor) {
stream.str());
ASSERT_CONTAINS_REGEX(ADDRESS "00000001 fulfilled policies: public\n", stream.str());
ASSERT_CONTAINS_REGEX(ADDRESS "00000001 enforce overlayable\n", stream.str());
+ ASSERT_CONTAINS_REGEX(ADDRESS "00000000 constraints count\n", stream.str());
ASSERT_CONTAINS_REGEX(ADDRESS "00000004 target entry count", stream.str());
ASSERT_CONTAINS_REGEX(ADDRESS "00000000 target inline entry count", stream.str());
ASSERT_CONTAINS_REGEX(ADDRESS "00000000 target inline entry value count", stream.str());
@@ -113,7 +116,7 @@ TEST(RawPrintVisitorTests, CreateRawPrintVisitorWithoutAccessToApks) {
(*idmap)->accept(&visitor);
ASSERT_CONTAINS_REGEX(ADDRESS "504d4449 magic\n", stream.str());
- ASSERT_CONTAINS_REGEX(ADDRESS "0000000a version\n", stream.str());
+ ASSERT_CONTAINS_REGEX(ADDRESS "0000000b version\n", stream.str());
ASSERT_CONTAINS_REGEX(ADDRESS "00001234 target crc\n", stream.str());
ASSERT_CONTAINS_REGEX(ADDRESS "00005678 overlay crc\n", stream.str());
ASSERT_CONTAINS_REGEX(ADDRESS "00000011 fulfilled policies: public|signature\n", stream.str());
@@ -124,6 +127,11 @@ TEST(RawPrintVisitorTests, CreateRawPrintVisitorWithoutAccessToApks) {
ASSERT_CONTAINS_REGEX(ADDRESS "........ overlay path: overlayX.apk\n", stream.str());
ASSERT_CONTAINS_REGEX(ADDRESS "0000000b overlay name size\n", stream.str());
ASSERT_CONTAINS_REGEX(ADDRESS "........ overlay name: OverlayName\n", stream.str());
+ ASSERT_CONTAINS_REGEX(ADDRESS "00000002 constraints count\n", stream.str());
+ ASSERT_CONTAINS_REGEX(ADDRESS "00000000 constraint type\n", stream.str());
+ ASSERT_CONTAINS_REGEX(ADDRESS "00000001 constraint value\n", stream.str());
+ ASSERT_CONTAINS_REGEX(ADDRESS "00000001 constraint type\n", stream.str());
+ ASSERT_CONTAINS_REGEX(ADDRESS "00000002 constraint value\n", stream.str());
ASSERT_CONTAINS_REGEX(ADDRESS "00000003 target entry count\n", stream.str());
ASSERT_CONTAINS_REGEX(ADDRESS "00000001 target inline entry count\n", stream.str());
ASSERT_CONTAINS_REGEX(ADDRESS "00000001 target inline entry value count", stream.str());
diff --git a/cmds/idmap2/tests/TestHelpers.h b/cmds/idmap2/tests/TestHelpers.h
index 2b4ebd1ae800..6f645bd01229 100644
--- a/cmds/idmap2/tests/TestHelpers.h
+++ b/cmds/idmap2/tests/TestHelpers.h
@@ -34,7 +34,7 @@ const unsigned char kIdmapRawData[] = {
0x49, 0x44, 0x4d, 0x50,
// 0x4: version
- 0x0a, 0x00, 0x00, 0x00,
+ 0x0b, 0x00, 0x00, 0x00,
// 0x8: target crc
0x34, 0x12, 0x00, 0x00,
@@ -73,131 +73,147 @@ const unsigned char kIdmapRawData[] = {
// 0x4c string contents "debug\0\0\0" (padded to word alignment)
0x64, 0x65, 0x62, 0x75, 0x67, 0x00, 0x00, 0x00,
+ // CONSTRAINTS
+ // 0x54: constraints_count
+ 0x02, 0x00, 0x00, 0x00,
+
+ // 0x58: constraint_type
+ 0x00, 0x00, 0x00, 0x00,
+
+ // 0x5c: constraint_value
+ 0x01, 0x00, 0x00, 0x00,
+
+ // 0x60: constraint_type
+ 0x01, 0x00, 0x00, 0x00,
+
+ // 0x64: constraint_value
+ 0x02, 0x00, 0x00, 0x00,
+
// DATA HEADER
- // 0x54: target_entry_count
+ // 0x68: target_entry_count
0x03, 0x00, 0x00, 0x00,
- // 0x58: target_inline_entry_count
+ // 0x6c: target_inline_entry_count
0x01, 0x00, 0x00, 0x00,
- // 0x5c: target_inline_entry_value_count
+ // 0x70: target_inline_entry_value_count
0x01, 0x00, 0x00, 0x00,
// 0x60: config_count
0x01, 0x00, 0x00, 0x00,
- // 0x64: overlay_entry_count
+ // 0x74: overlay_entry_count
0x03, 0x00, 0x00, 0x00,
- // 0x68: string_pool_offset
+ // 0x78: string_pool_offset
0x00, 0x00, 0x00, 0x00,
// TARGET ENTRIES
- // 0x6c: target id (0x7f020000)
+ // 0x7c: target id (0x7f020000)
0x00, 0x00, 0x02, 0x7f,
- // 0x70: target id (0x7f030000)
+ // 0x80: target id (0x7f030000)
0x00, 0x00, 0x03, 0x7f,
- // 0x74: target id (0x7f030002)
+ // 0x84: target id (0x7f030002)
0x02, 0x00, 0x03, 0x7f,
- // 0x78: overlay_id (0x7f020000)
+ // 0x88: overlay_id (0x7f020000)
0x00, 0x00, 0x02, 0x7f,
- // 0x7c: overlay_id (0x7f030000)
+ // 0x8c: overlay_id (0x7f030000)
0x00, 0x00, 0x03, 0x7f,
- // 0x80: overlay_id (0x7f030001)
+ // 0x90: overlay_id (0x7f030001)
0x01, 0x00, 0x03, 0x7f,
// INLINE TARGET ENTRIES
- // 0x84: target_id
+ // 0x94: target_id
0x00, 0x00, 0x04, 0x7f,
- // 0x88: start value index
+ // 0x98: start value index
0x00, 0x00, 0x00, 0x00,
- // 0x8c: value count
+ // 0x9c: value count
0x01, 0x00, 0x00, 0x00,
// INLINE TARGET ENTRY VALUES
- // 0x90: config index
+ // 0xa0: config index
0x00, 0x00, 0x00, 0x00,
- // 0x94: Res_value::size (value ignored by idmap)
+ // 0xa4: Res_value::size (value ignored by idmap)
0x08, 0x00,
- // 0x98: Res_value::res0 (value ignored by idmap)
+ // 0xa8: Res_value::res0 (value ignored by idmap)
0x00,
- // 0x9c: Res_value::dataType (TYPE_INT_HEX)
+ // 0xac: Res_value::dataType (TYPE_INT_HEX)
0x11,
- // 0xa0: Res_value::data
+ // 0xb0: Res_value::data
0x78, 0x56, 0x34, 0x12,
// CONFIGURATIONS
- // 0xa4: ConfigDescription
+ // 0xb4: ConfigDescription
// size
0x40, 0x00, 0x00, 0x00,
- // 0xa8: imsi
+ // 0xb8: imsi
0x00, 0x00, 0x00, 0x00,
- // 0xac: locale
+ // 0xbc: locale
0x00, 0x00, 0x00, 0x00,
- // 0xb0: screenType
+ // 0xc0: screenType
0x02, 0x00, 0xe0, 0x01,
- // 0xb4: input
+ // 0xc4: input
0x00, 0x00, 0x00, 0x00,
- // 0xb8: screenSize
+ // 0xc8: screenSize
0x00, 0x00, 0x00, 0x00,
- // 0xbc: version
+ // 0xcc: version
0x07, 0x00, 0x00, 0x00,
- // 0xc0: screenConfig
+ // 0xd0: screenConfig
0x00, 0x00, 0x00, 0x00,
- // 0xc4: screenSizeDp
+ // 0xd4: screenSizeDp
0x00, 0x00, 0x00, 0x00,
- // 0xc8: localeScript
+ // 0xd8: localeScript
0x00, 0x00, 0x00, 0x00,
- // 0xcc: localVariant(1)
+ // 0xdc: localVariant(1)
0x00, 0x00, 0x00, 0x00,
- // 0xd0: localVariant(2)
+ // 0xe0: localVariant(2)
0x00, 0x00, 0x00, 0x00,
- // 0xd4: screenConfig2
+ // 0xe4: screenConfig2
0x00, 0x00, 0x00, 0x00,
- // 0xd8: localeScriptWasComputed
+ // 0xe8: localeScriptWasComputed
0x00,
- // 0xd9: localeNumberingSystem(1)
+ // 0xe9: localeNumberingSystem(1)
0x00, 0x00, 0x00, 0x00,
- // 0xdd: localeNumberingSystem(2)
+ // 0xed: localeNumberingSystem(2)
0x00, 0x00, 0x00, 0x00,
- // 0xe1: padding
+ // 0xf1: padding
0x00, 0x00, 0x00,
// OVERLAY ENTRIES
- // 0xe4: 0x7f020000 -> ...
+ // 0xf4: 0x7f020000 -> ...
0x00, 0x00, 0x02, 0x7f,
- // 0xe8: 0x7f030000 -> ...
+ // 0xf8: 0x7f030000 -> ...
0x00, 0x00, 0x03, 0x7f,
- // 0xec: 0x7f030001 -> ...
+ // 0xfc: 0x7f030001 -> ...
0x01, 0x00, 0x03, 0x7f,
- // 0xf0: ... -> 0x7f020000
+ // 0x100: ... -> 0x7f020000
0x00, 0x00, 0x02, 0x7f,
- // 0xf4: ... -> 0x7f030000
+ // 0x104: ... -> 0x7f030000
0x00, 0x00, 0x03, 0x7f,
- // 0xf8: ... -> 0x7f030002
+ // 0x108: ... -> 0x7f030002
0x02, 0x00, 0x03, 0x7f,
- // 0xfc: string pool
+ // 0x10c: string pool
// string length,
0x04, 0x00, 0x00, 0x00,
- // 0x100 string contents "test"
+ // 0x110 string contents "test"
0x74, 0x65, 0x73, 0x74};
constexpr unsigned int kIdmapRawDataLen = std::size(kIdmapRawData);
-const unsigned int kIdmapRawDataOffset = 0x54;
+const unsigned int kIdmapRawDataOffset = 0x68;
const unsigned int kIdmapRawDataTargetCrc = 0x1234;
const unsigned int kIdmapRawOverlayCrc = 0x5678;
const unsigned int kIdmapRawDataPolicies = 0x11;
diff --git a/core/api/current.txt b/core/api/current.txt
index dd606774b770..050cad4e1a52 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -38774,7 +38774,7 @@ package android.provider {
}
public static final class Telephony.Carriers implements android.provider.BaseColumns {
- field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String ALWAYS_ON = "always_on";
+ field public static final String ALWAYS_ON = "always_on";
field public static final String APN = "apn";
field public static final String AUTH_TYPE = "authtype";
field @Deprecated public static final String BEARER = "bearer";
@@ -38788,8 +38788,8 @@ package android.provider {
field public static final String MMSPORT = "mmsport";
field public static final String MMSPROXY = "mmsproxy";
field @Deprecated public static final String MNC = "mnc";
- field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String MTU_V4 = "mtu_v4";
- field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String MTU_V6 = "mtu_v6";
+ field public static final String MTU_V4 = "mtu_v4";
+ field public static final String MTU_V6 = "mtu_v6";
field @Deprecated public static final String MVNO_MATCH_DATA = "mvno_match_data";
field @Deprecated public static final String MVNO_TYPE = "mvno_type";
field public static final String NAME = "name";
@@ -38805,8 +38805,8 @@ package android.provider {
field public static final String SUBSCRIPTION_ID = "sub_id";
field public static final String TYPE = "type";
field public static final String USER = "user";
- field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String USER_EDITABLE = "user_editable";
- field @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public static final String USER_VISIBLE = "user_visible";
+ field public static final String USER_EDITABLE = "user_editable";
+ field public static final String USER_VISIBLE = "user_visible";
}
public static final class Telephony.Mms implements android.provider.Telephony.BaseMmsColumns {
@@ -47776,7 +47776,7 @@ package android.telephony.data {
method public int getProxyPort();
method public int getRoamingProtocol();
method public String getUser();
- method @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") public boolean isAlwaysOn();
+ method public boolean isAlwaysOn();
method public boolean isEnabled();
method public boolean isPersistent();
method public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -47818,7 +47818,7 @@ package android.telephony.data {
public static class ApnSetting.Builder {
ctor public ApnSetting.Builder();
method public android.telephony.data.ApnSetting build();
- method @FlaggedApi("com.android.internal.telephony.flags.apn_setting_field_support_flag") @NonNull public android.telephony.data.ApnSetting.Builder setAlwaysOn(boolean);
+ method @NonNull public android.telephony.data.ApnSetting.Builder setAlwaysOn(boolean);
method @NonNull public android.telephony.data.ApnSetting.Builder setApnName(@Nullable String);
method @NonNull public android.telephony.data.ApnSetting.Builder setApnTypeBitmask(int);
method @NonNull public android.telephony.data.ApnSetting.Builder setAuthType(int);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 9a848d423c9a..76cce7439454 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3436,7 +3436,7 @@ package android.companion.virtual {
method public void close();
method @NonNull public android.content.Context createContext();
method @NonNull public android.companion.virtual.audio.VirtualAudioDevice createVirtualAudioDevice(@NonNull android.hardware.display.VirtualDisplay, @Nullable java.util.concurrent.Executor, @Nullable android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback);
- method @FlaggedApi("android.companion.virtual.flags.virtual_camera") @NonNull public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig);
+ method @NonNull public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig);
method @Deprecated @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull android.hardware.display.VirtualDisplayConfig, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
method @NonNull public android.hardware.input.VirtualDpad createVirtualDpad(@NonNull android.hardware.input.VirtualDpadConfig);
@@ -3499,7 +3499,7 @@ package android.companion.virtual {
field public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3
field public static final int POLICY_TYPE_AUDIO = 1; // 0x1
field @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public static final int POLICY_TYPE_BLOCKED_ACTIVITY = 6; // 0x6
- field @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final int POLICY_TYPE_CAMERA = 5; // 0x5
+ field public static final int POLICY_TYPE_CAMERA = 5; // 0x5
field public static final int POLICY_TYPE_CLIPBOARD = 4; // 0x4
field @FlaggedApi("android.companion.virtualdevice.flags.default_device_camera_access_policy") public static final int POLICY_TYPE_DEFAULT_DEVICE_CAMERA_ACCESS = 7; // 0x7
field public static final int POLICY_TYPE_RECENTS = 2; // 0x2
@@ -3577,18 +3577,18 @@ package android.companion.virtual.audio {
package android.companion.virtual.camera {
- @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCamera implements java.io.Closeable {
+ public final class VirtualCamera implements java.io.Closeable {
method public void close();
method @NonNull public android.companion.virtual.camera.VirtualCameraConfig getConfig();
}
- @FlaggedApi("android.companion.virtual.flags.virtual_camera") public interface VirtualCameraCallback {
+ public interface VirtualCameraCallback {
method public default void onProcessCaptureRequest(int, long);
method public void onStreamClosed(int);
method public void onStreamConfigured(int, @NonNull android.view.Surface, @IntRange(from=1) int, @IntRange(from=1) int, int);
}
- @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraConfig implements android.os.Parcelable {
+ public final class VirtualCameraConfig implements android.os.Parcelable {
method public int describeContents();
method public int getLensFacing();
method @NonNull public String getName();
@@ -3602,7 +3602,7 @@ package android.companion.virtual.camera {
field public static final int SENSOR_ORIENTATION_90 = 90; // 0x5a
}
- @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final class VirtualCameraConfig.Builder {
+ public static final class VirtualCameraConfig.Builder {
ctor public VirtualCameraConfig.Builder(@NonNull String);
method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int, @IntRange(from=1) int);
method @NonNull public android.companion.virtual.camera.VirtualCameraConfig build();
@@ -3611,7 +3611,7 @@ package android.companion.virtual.camera {
method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback);
}
- @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraStreamConfig implements android.os.Parcelable {
+ public final class VirtualCameraStreamConfig implements android.os.Parcelable {
method public int describeContents();
method public int getFormat();
method @IntRange(from=1) public int getHeight();
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 5453e735ce17..e8ff546cc61a 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -942,7 +942,7 @@ package android.companion.virtual {
package android.companion.virtual.camera {
- @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCamera implements java.io.Closeable {
+ public final class VirtualCamera implements java.io.Closeable {
method @NonNull public String getId();
}
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 2dead565fa85..f2e7e8513116 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -17,7 +17,6 @@
package android.app;
import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM;
-import static android.app.PropertyInvalidatedCache.createSystemCacheKey;
import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_COLORED;
import static android.app.admin.DevicePolicyResources.Drawables.Style.SOLID_NOT_COLORED;
import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON;
@@ -1146,12 +1145,16 @@ public class ApplicationPackageManager extends PackageManager {
}
}
- private static final String CACHE_KEY_PACKAGES_FOR_UID_PROPERTY =
- createSystemCacheKey("get_packages_for_uid");
- private static final PropertyInvalidatedCache<Integer, GetPackagesForUidResult>
- mGetPackagesForUidCache =
- new PropertyInvalidatedCache<Integer, GetPackagesForUidResult>(
- 1024, CACHE_KEY_PACKAGES_FOR_UID_PROPERTY) {
+ private static final String CACHE_KEY_PACKAGES_FOR_UID_API = "get_packages_for_uid";
+
+ /** @hide */
+ @VisibleForTesting
+ public static final PropertyInvalidatedCache<Integer, GetPackagesForUidResult>
+ sGetPackagesForUidCache = new PropertyInvalidatedCache<>(
+ new PropertyInvalidatedCache.Args(MODULE_SYSTEM)
+ .maxEntries(1024).api(CACHE_KEY_PACKAGES_FOR_UID_API).cacheNulls(true),
+ CACHE_KEY_PACKAGES_FOR_UID_API, null) {
+
@Override
public GetPackagesForUidResult recompute(Integer uid) {
try {
@@ -1170,17 +1173,17 @@ public class ApplicationPackageManager extends PackageManager {
@Override
public String[] getPackagesForUid(int uid) {
- return mGetPackagesForUidCache.query(uid).value();
+ return sGetPackagesForUidCache.query(uid).value();
}
/** @hide */
public static void disableGetPackagesForUidCache() {
- mGetPackagesForUidCache.disableLocal();
+ sGetPackagesForUidCache.disableLocal();
}
/** @hide */
public static void invalidateGetPackagesForUidCache() {
- PropertyInvalidatedCache.invalidateCache(CACHE_KEY_PACKAGES_FOR_UID_PROPERTY);
+ sGetPackagesForUidCache.invalidateCache();
}
@Override
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index bfb33f2b7cb1..c573161f30cc 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -1163,6 +1163,17 @@ public class PropertyInvalidatedCache<Query, Result> {
}
/**
+ * Return the current cache nonce.
+ * @hide
+ */
+ @VisibleForTesting
+ public long getNonce() {
+ synchronized (mLock) {
+ return mNonce.getNonce();
+ }
+ }
+
+ /**
* Complete key prefixes.
*/
private static final String PREFIX_TEST = CACHE_KEY_PREFIX + "." + MODULE_TEST + ".";
@@ -1314,7 +1325,7 @@ public class PropertyInvalidatedCache<Query, Result> {
/**
* Burst a property name into module and api. Throw if the key is invalid. This method is
- * used in to transition legacy cache constructors to the args constructor.
+ * used to transition legacy cache constructors to the Args constructor.
*/
private static Args argsFromProperty(@NonNull String name) {
throwIfInvalidCacheKey(name);
@@ -1327,6 +1338,15 @@ public class PropertyInvalidatedCache<Query, Result> {
}
/**
+ * Return the API porting of a legacy property. This method is used to transition caches to
+ * the Args constructor.
+ * @hide
+ */
+ public static String apiFromProperty(@NonNull String name) {
+ return argsFromProperty(name).mApi;
+ }
+
+ /**
* Make a new property invalidated cache. This constructor names the cache after the
* property name. New clients should prefer the constructor that takes an explicit
* cache name.
@@ -2036,11 +2056,11 @@ public class PropertyInvalidatedCache<Query, Result> {
}
/**
- * Disable all caches in the local process. This is primarily useful for testing when
- * the test needs to bypass the cache or when the test is for a server, and the test
- * process does not have privileges to write SystemProperties. Once disabled it is not
- * possible to re-enable caching in the current process. If a client wants to
- * temporarily disable caching, use the corking mechanism.
+ * Disable all caches in the local process. This is primarily useful for testing when the
+ * test needs to bypass the cache or when the test is for a server, and the test process does
+ * not have privileges to write the nonce. Once disabled it is not possible to re-enable
+ * caching in the current process. See {@link #testPropertyName} for a more focused way to
+ * bypass caches when the test is for a server.
* @hide
*/
public static void disableForTestMode() {
diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java
index 0a3891fe47a1..a66d59ba9cb6 100644
--- a/core/java/android/app/appfunctions/AppFunctionManager.java
+++ b/core/java/android/app/appfunctions/AppFunctionManager.java
@@ -27,6 +27,7 @@ import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.annotation.UserHandleAware;
+import android.app.appfunctions.AppFunctionManagerHelper.AppFunctionNotFoundException;
import android.app.appsearch.AppSearchManager;
import android.content.Context;
import android.os.CancellationSignal;
@@ -325,8 +326,28 @@ public final class AppFunctionManager {
return;
}
+ // Wrap the callback to convert AppFunctionNotFoundException to IllegalArgumentException
+ // to match the documentation.
+ OutcomeReceiver<Boolean, Exception> callbackWithExceptionInterceptor =
+ new OutcomeReceiver<>() {
+ @Override
+ public void onResult(@NonNull Boolean result) {
+ callback.onResult(result);
+ }
+
+ @Override
+ public void onError(@NonNull Exception exception) {
+ if (exception instanceof AppFunctionNotFoundException) {
+ exception = new IllegalArgumentException(exception);
+ }
+ callback.onError(exception);
+ }
+ };
+
AppFunctionManagerHelper.isAppFunctionEnabled(
- functionIdentifier, targetPackage, appSearchManager, executor, callback);
+ functionIdentifier, targetPackage, appSearchManager, executor,
+ callbackWithExceptionInterceptor);
+
}
private static class CallbackWrapper extends IAppFunctionEnabledCallback.Stub {
diff --git a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
index cc3ca03f423d..83abc048af8a 100644
--- a/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
+++ b/core/java/android/app/appfunctions/AppFunctionManagerHelper.java
@@ -60,8 +60,8 @@ public class AppFunctionManagerHelper {
* <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors:
*
* <ul>
- * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not
- * have access to it.
+ * <li>{@link AppFunctionNotFoundException}, if the function is not found or the caller does
+ * not have access to it.
* </ul>
*
* @param functionIdentifier the identifier of the app function to check (unique within the
@@ -216,7 +216,7 @@ public class AppFunctionManagerHelper {
private static @NonNull Exception failedResultToException(
@NonNull AppSearchResult appSearchResult) {
return switch (appSearchResult.getResultCode()) {
- case AppSearchResult.RESULT_INVALID_ARGUMENT -> new IllegalArgumentException(
+ case AppSearchResult.RESULT_INVALID_ARGUMENT -> new AppFunctionNotFoundException(
appSearchResult.getErrorMessage());
case AppSearchResult.RESULT_IO_ERROR -> new IOException(
appSearchResult.getErrorMessage());
@@ -225,4 +225,15 @@ public class AppFunctionManagerHelper {
default -> new IllegalStateException(appSearchResult.getErrorMessage());
};
}
+
+ /**
+ * Throws when the app function is not found.
+ *
+ * @hide
+ */
+ public static class AppFunctionNotFoundException extends RuntimeException {
+ private AppFunctionNotFoundException(@NonNull String errorMessage) {
+ super(errorMessage);
+ }
+ }
}
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 252db824c69f..ab52db4b7a30 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -37,8 +37,8 @@ import android.companion.virtual.audio.VirtualAudioDevice;
import android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback;
import android.companion.virtual.camera.VirtualCamera;
import android.companion.virtual.camera.VirtualCameraConfig;
-import android.companion.virtual.flags.Flags;
import android.companion.virtual.sensor.VirtualSensor;
+import android.companion.virtualdevice.flags.Flags;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -624,7 +624,7 @@ public final class VirtualDeviceManager {
* @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
* @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
public void goToSleep() {
mVirtualDeviceInternal.goToSleep();
}
@@ -642,7 +642,7 @@ public final class VirtualDeviceManager {
* @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
* @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
public void wakeUp() {
mVirtualDeviceInternal.wakeUp();
}
@@ -838,7 +838,7 @@ public final class VirtualDeviceManager {
* @see #removeActivityPolicyExemption(ActivityPolicyExemption)
* @see #setDevicePolicy
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
public void addActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
mVirtualDeviceInternal.addActivityPolicyExemption(Objects.requireNonNull(exemption));
}
@@ -853,7 +853,7 @@ public final class VirtualDeviceManager {
* @see #addActivityPolicyExemption(ActivityPolicyExemption)
* @see #setDevicePolicy
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
public void removeActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
mVirtualDeviceInternal.removeActivityPolicyExemption(Objects.requireNonNull(exemption));
}
@@ -875,7 +875,7 @@ public final class VirtualDeviceManager {
* @see VirtualDeviceParams#POLICY_TYPE_RECENTS
* @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
public void setDevicePolicy(
@VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
@VirtualDeviceParams.DevicePolicy int devicePolicy,
@@ -1037,10 +1037,10 @@ public final class VirtualDeviceManager {
* @see android.view.InputDevice#SOURCE_ROTARY_ENCODER
*/
@NonNull
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_VIRTUAL_ROTARY)
+ @FlaggedApi(Flags.FLAG_VIRTUAL_ROTARY)
public VirtualRotaryEncoder createVirtualRotaryEncoder(
@NonNull VirtualRotaryEncoderConfig config) {
- if (!android.companion.virtualdevice.flags.Flags.virtualRotary()) {
+ if (!Flags.virtualRotary()) {
throw new UnsupportedOperationException("Virtual rotary support not enabled");
}
return mVirtualDeviceInternal.createVirtualRotaryEncoder(config);
@@ -1084,12 +1084,7 @@ public final class VirtualDeviceManager {
* @see VirtualDeviceParams#POLICY_TYPE_CAMERA
*/
@NonNull
- @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public VirtualCamera createVirtualCamera(@NonNull VirtualCameraConfig config) {
- if (!Flags.virtualCamera()) {
- throw new UnsupportedOperationException(
- "Flag is not enabled: %s".formatted(Flags.FLAG_VIRTUAL_CAMERA));
- }
return mVirtualDeviceInternal.createVirtualCamera(Objects.requireNonNull(config));
}
@@ -1252,7 +1247,7 @@ public final class VirtualDeviceManager {
* @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
* @see VirtualDevice#addActivityPolicyExemption(ActivityPolicyExemption)
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
default void onActivityLaunchBlocked(int displayId, @NonNull ComponentName componentName,
@NonNull UserHandle user, @Nullable IntentSender intentSender) {}
@@ -1268,7 +1263,7 @@ public final class VirtualDeviceManager {
* @see Display#FLAG_SECURE
* @see WindowManager.LayoutParams#FLAG_SECURE
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
default void onSecureWindowShown(int displayId, @NonNull ComponentName componentName,
@NonNull UserHandle user) {}
@@ -1284,7 +1279,7 @@ public final class VirtualDeviceManager {
* @see Display#FLAG_SECURE
* @see WindowManager.LayoutParams#FLAG_SECURE
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
default void onSecureWindowHidden(int displayId) {}
}
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 95dee9b72a88..699494790f35 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -29,12 +29,12 @@ import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
-import android.companion.virtual.flags.Flags;
import android.companion.virtual.sensor.IVirtualSensorCallback;
import android.companion.virtual.sensor.VirtualSensor;
import android.companion.virtual.sensor.VirtualSensorCallback;
import android.companion.virtual.sensor.VirtualSensorConfig;
import android.companion.virtual.sensor.VirtualSensorDirectChannelCallback;
+import android.companion.virtualdevice.flags.Flags;
import android.content.ComponentName;
import android.content.Context;
import android.hardware.display.VirtualDisplayConfig;
@@ -279,7 +279,6 @@ public final class VirtualDeviceParams implements Parcelable {
*
* @see Context#getDeviceId
*/
- @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public static final int POLICY_TYPE_CAMERA = 5;
/**
@@ -296,7 +295,7 @@ public final class VirtualDeviceParams implements Parcelable {
* </ul>
*/
// TODO(b/333443509): Link to POLICY_TYPE_ACTIVITY
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
+ @FlaggedApi(Flags.FLAG_ACTIVITY_CONTROL_API)
public static final int POLICY_TYPE_BLOCKED_ACTIVITY = 6;
/**
@@ -310,8 +309,7 @@ public final class VirtualDeviceParams implements Parcelable {
*
* @see Context#DEVICE_ID_DEFAULT
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags
- .FLAG_DEFAULT_DEVICE_CAMERA_ACCESS_POLICY)
+ @FlaggedApi(Flags.FLAG_DEFAULT_DEVICE_CAMERA_ACCESS_POLICY)
public static final int POLICY_TYPE_DEFAULT_DEVICE_CAMERA_ACCESS = 7;
private final int mLockState;
@@ -407,7 +405,7 @@ public final class VirtualDeviceParams implements Parcelable {
*
* @see Builder#setDimDuration(Duration)
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
public @NonNull Duration getDimDuration() {
return Duration.ofMillis(mDimDuration);
}
@@ -417,7 +415,7 @@ public final class VirtualDeviceParams implements Parcelable {
*
* @see Builder#setDimDuration(Duration)
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
public @NonNull Duration getScreenOffTimeout() {
return Duration.ofMillis(mScreenOffTimeout);
}
@@ -876,7 +874,7 @@ public final class VirtualDeviceParams implements Parcelable {
* @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
* @see #setScreenOffTimeout
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
@NonNull
public Builder setDimDuration(@NonNull Duration dimDuration) {
if (Objects.requireNonNull(dimDuration).compareTo(Duration.ZERO) < 0) {
@@ -901,7 +899,7 @@ public final class VirtualDeviceParams implements Parcelable {
* @see #setDimDuration
* @see VirtualDeviceManager.VirtualDevice#goToSleep()
*/
- @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
@NonNull
public Builder setScreenOffTimeout(@NonNull Duration screenOffTimeout) {
if (Objects.requireNonNull(screenOffTimeout).compareTo(Duration.ZERO) < 0) {
@@ -1311,15 +1309,11 @@ public final class VirtualDeviceParams implements Parcelable {
mScreenOffTimeout = INFINITE_TIMEOUT;
}
- if (!Flags.virtualCamera()) {
- mDevicePolicies.delete(POLICY_TYPE_CAMERA);
- }
-
- if (!android.companion.virtualdevice.flags.Flags.defaultDeviceCameraAccessPolicy()) {
+ if (!Flags.defaultDeviceCameraAccessPolicy()) {
mDevicePolicies.delete(POLICY_TYPE_DEFAULT_DEVICE_CAMERA_ACCESS);
}
- if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (!Flags.activityControlApi()) {
mDevicePolicies.delete(POLICY_TYPE_BLOCKED_ACTIVITY);
}
diff --git a/core/java/android/companion/virtual/camera/VirtualCamera.java b/core/java/android/companion/virtual/camera/VirtualCamera.java
index ece048d3a95b..b7bcc29a39cb 100644
--- a/core/java/android/companion/virtual/camera/VirtualCamera.java
+++ b/core/java/android/companion/virtual/camera/VirtualCamera.java
@@ -16,14 +16,12 @@
package android.companion.virtual.camera;
-import android.annotation.FlaggedApi;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.companion.virtual.IVirtualDevice;
import android.companion.virtual.VirtualDeviceManager;
import android.companion.virtual.VirtualDeviceParams;
-import android.companion.virtual.flags.Flags;
import android.hardware.camera2.CameraDevice;
import android.os.RemoteException;
@@ -51,7 +49,6 @@ import java.util.concurrent.Executor;
* @hide
*/
@SystemApi
-@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public final class VirtualCamera implements Closeable {
private final IVirtualDevice mVirtualDevice;
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
index c894de428b10..d326be83c404 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
@@ -16,11 +16,9 @@
package android.companion.virtual.camera;
-import android.annotation.FlaggedApi;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SystemApi;
-import android.companion.virtual.flags.Flags;
import android.graphics.ImageFormat;
import android.view.Surface;
@@ -34,7 +32,6 @@ import java.util.concurrent.Executor;
* @hide
*/
@SystemApi
-@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public interface VirtualCameraCallback {
/**
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
index 769b658c78ce..6c88ec99349e 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
@@ -18,14 +18,12 @@ package android.companion.virtual.camera;
import static java.util.Objects.requireNonNull;
-import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.companion.virtual.VirtualDevice;
-import android.companion.virtual.flags.Flags;
import android.graphics.ImageFormat;
import android.graphics.PixelFormat;
import android.hardware.camera2.CameraMetadata;
@@ -47,7 +45,6 @@ import java.util.concurrent.Executor;
* @hide
*/
@SystemApi
-@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public final class VirtualCameraConfig implements Parcelable {
private static final int LENS_FACING_UNKNOWN = -1;
@@ -198,7 +195,6 @@ public final class VirtualCameraConfig implements Parcelable {
* VirtualCameraCallback)}
* <li>A lens facing must be set with {@link #setLensFacing(int)}
*/
- @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public static final class Builder {
private final String mName;
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
index 6ab66b3d2309..be498806697c 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
@@ -16,11 +16,9 @@
package android.companion.virtual.camera;
-import android.annotation.FlaggedApi;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.SystemApi;
-import android.companion.virtual.flags.Flags;
import android.graphics.ImageFormat;
import android.os.Parcel;
import android.os.Parcelable;
@@ -35,7 +33,6 @@ import java.util.Objects;
* @hide
*/
@SystemApi
-@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public final class VirtualCameraStreamConfig implements Parcelable {
// TODO(b/310857519): Check if we should increase the fps upper limit in future.
static final int MAX_FPS_UPPER_LIMIT = 60;
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 1cf42820f356..c3dc257e6535 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -11,13 +11,6 @@ package: "android.companion.virtualdevice.flags"
container: "system"
flag {
- namespace: "virtual_devices"
- name: "virtual_camera_service_discovery"
- description: "Enable discovery of the Virtual Camera HAL without a VINTF entry"
- bug: "305170199"
-}
-
-flag {
namespace: "virtual_devices"
name: "virtual_display_insets"
description: "APIs for specifying virtual display insets (via cutout)"
@@ -34,13 +27,6 @@ flag {
}
flag {
- namespace: "virtual_devices"
- name: "camera_device_awareness"
- description: "Enable device awareness in camera service"
- bug: "305170199"
-}
-
-flag {
name: "virtual_rotary"
is_exported: true
namespace: "virtual_devices"
@@ -49,14 +35,6 @@ flag {
}
flag {
- namespace: "virtual_devices"
- name: "device_aware_drm"
- description: "Makes MediaDrm APIs device-aware"
- bug: "303535376"
- is_fixed_read_only: true
-}
-
-flag {
namespace: "virtual_devices"
name: "enforce_remote_device_opt_out_on_all_virtual_displays"
description: "Respect canDisplayOnRemoteDevices on all virtual displays"
@@ -147,3 +125,11 @@ flag {
description: "Show virtual devices in Settings"
bug: "338974320"
}
+
+flag {
+ name: "migrate_viewconfiguration_constants_to_resources"
+ namespace: "virtual_devices"
+ description: "Use resources instead of constants in ViewConfiguration"
+ is_fixed_read_only: true
+ bug: "370928384"
+}
diff --git a/core/java/android/content/om/IOverlayManager.aidl b/core/java/android/content/om/IOverlayManager.aidl
index 122ab486948f..d865ba749adc 100644
--- a/core/java/android/content/om/IOverlayManager.aidl
+++ b/core/java/android/content/om/IOverlayManager.aidl
@@ -16,10 +16,13 @@
package android.content.om;
+import android.content.om.OverlayConstraint;
import android.content.om.OverlayIdentifier;
import android.content.om.OverlayInfo;
import android.content.om.OverlayManagerTransaction;
+import java.util.List;
+
/**
* Api for getting information about overlay packages.
*
@@ -103,6 +106,22 @@ interface IOverlayManager {
boolean setEnabled(in String packageName, in boolean enable, in int userId);
/**
+ * Enable an overlay package for a specific set of constraints. In case of multiple constraints,
+ * the overlay would be enabled when any of the given constraints are satisfied.
+ *
+ * Re-enabling an overlay with new constraints updates the constraints for the overlay.
+ *
+ * The caller must pass the actor requirements specified in the class comment.
+ *
+ * @param packageName the name of the overlay package to enable.
+ * @param user The user for which to change the overlay.
+ * @param constraints list of {@link OverlayConstraint} for enabling the overlay.
+ * @return true if the system successfully registered the request, false otherwise.
+ */
+ boolean enableWithConstraints(in String packageName, in int userId,
+ in List<OverlayConstraint> constraints);
+
+ /**
* Request that an overlay package is enabled and any other overlay packages with the same
* target package are disabled.
*
diff --git a/core/java/android/content/om/OverlayConstraint.aidl b/core/java/android/content/om/OverlayConstraint.aidl
new file mode 100644
index 000000000000..95aac8069617
--- /dev/null
+++ b/core/java/android/content/om/OverlayConstraint.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.om;
+
+parcelable OverlayConstraint;
diff --git a/core/java/android/content/om/OverlayConstraint.java b/core/java/android/content/om/OverlayConstraint.java
new file mode 100644
index 000000000000..c1902def882f
--- /dev/null
+++ b/core/java/android/content/om/OverlayConstraint.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.om;
+
+import android.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Constraint for enabling a RRO. Currently this can be a displayId or a deviceId, i.e.,
+ * the overlay would be applied only when a target package is running on the given displayId
+ * or deviceId.
+ *
+ * @hide
+ */
+public final class OverlayConstraint implements Parcelable {
+
+ /**
+ * Constraint type for enabling a RRO for a specific display id. For contexts associated with
+ * the default display, this would be {@link android.view.Display#DEFAULT_DISPLAY}, and
+ * for contexts associated with a virtual display, this would be the id of the virtual display.
+ */
+ public static final int TYPE_DISPLAY_ID = 0;
+
+ /**
+ * Constraint type for enabling a RRO for a specific device id. For contexts associated with
+ * the default device, this would be {@link android.content.Context#DEVICE_ID_DEFAULT}, and
+ * for contexts associated with virtual device, this would be the id of the virtual device.
+ */
+ public static final int TYPE_DEVICE_ID = 1;
+
+ @IntDef(prefix = "TYPE_", value = {
+ TYPE_DISPLAY_ID,
+ TYPE_DEVICE_ID,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ConstraintType {
+ }
+
+ @ConstraintType
+ private final int mType;
+ private final int mValue;
+
+ public OverlayConstraint(int type, int value) {
+ if (type != TYPE_DEVICE_ID && type != TYPE_DISPLAY_ID) {
+ throw new IllegalArgumentException(
+ "Type must be either TYPE_DISPLAY_ID or TYPE_DEVICE_ID");
+ }
+ if (value < 0) {
+ throw new IllegalArgumentException("Value must be greater than 0");
+ }
+ this.mType = type;
+ this.mValue = value;
+ }
+
+ private OverlayConstraint(Parcel in) {
+ this(in.readInt(), in.readInt());
+ }
+
+ /**
+ * Returns the type of the constraint.
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Returns the value of the constraint.
+ */
+ public int getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String toString() {
+ return "{type: " + typeToString(mType) + ", value: " + mValue + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof OverlayConstraint that)) {
+ return false;
+ }
+ return mType == that.mType && mValue == that.mValue;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mType, mValue);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mType);
+ dest.writeInt(mValue);
+ }
+
+ public static final Creator<OverlayConstraint> CREATOR = new Creator<>() {
+ @Override
+ public OverlayConstraint createFromParcel(Parcel in) {
+ return new OverlayConstraint(in);
+ }
+
+ @Override
+ public OverlayConstraint[] newArray(int size) {
+ return new OverlayConstraint[size];
+ }
+ };
+
+ /**
+ * Returns a string description for a list of constraints.
+ */
+ public static String constraintsToString(final List<OverlayConstraint> overlayConstraints) {
+ if (overlayConstraints == null || overlayConstraints.isEmpty()) {
+ return "None";
+ }
+ return "[" + TextUtils.join(",", overlayConstraints) + "]";
+ }
+
+ private static String typeToString(@ConstraintType int type) {
+ return type == TYPE_DEVICE_ID ? "DEVICE_ID" : "DISPLAY_ID";
+ }
+}
diff --git a/core/java/android/content/om/OverlayInfo.java b/core/java/android/content/om/OverlayInfo.java
index 2e898562655b..4977c820ba55 100644
--- a/core/java/android/content/om/OverlayInfo.java
+++ b/core/java/android/content/om/OverlayInfo.java
@@ -30,6 +30,9 @@ import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
import java.util.Objects;
/**
@@ -230,12 +233,17 @@ public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
private OverlayIdentifier mIdentifierCached;
/**
- *
* @hide
*/
public final boolean isFabricated;
/**
+ * @hide
+ */
+ @NonNull
+ public final List<OverlayConstraint> constraints;
+
+ /**
* Create a new OverlayInfo based on source with an updated state.
*
* @param source the source OverlayInfo to base the new instance on
@@ -246,7 +254,8 @@ public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
public OverlayInfo(@NonNull OverlayInfo source, @State int state) {
this(source.packageName, source.overlayName, source.targetPackageName,
source.targetOverlayableName, source.category, source.baseCodePath, state,
- source.userId, source.priority, source.isMutable, source.isFabricated);
+ source.userId, source.priority, source.isMutable, source.isFabricated,
+ source.constraints);
}
/** @hide */
@@ -264,6 +273,17 @@ public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
@NonNull String targetPackageName, @Nullable String targetOverlayableName,
@Nullable String category, @NonNull String baseCodePath, int state, int userId,
int priority, boolean isMutable, boolean isFabricated) {
+ this(packageName, overlayName, targetPackageName, targetOverlayableName, category,
+ baseCodePath, state, userId, priority, isMutable, isFabricated,
+ Collections.emptyList() /* constraints */);
+ }
+
+ /** @hide */
+ public OverlayInfo(@NonNull String packageName, @Nullable String overlayName,
+ @NonNull String targetPackageName, @Nullable String targetOverlayableName,
+ @Nullable String category, @NonNull String baseCodePath, int state, int userId,
+ int priority, boolean isMutable, boolean isFabricated,
+ @NonNull List<OverlayConstraint> constraints) {
this.packageName = packageName;
this.overlayName = overlayName;
this.targetPackageName = targetPackageName;
@@ -275,6 +295,7 @@ public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
this.priority = priority;
this.isMutable = isMutable;
this.isFabricated = isFabricated;
+ this.constraints = constraints;
ensureValidState();
}
@@ -291,6 +312,7 @@ public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
priority = source.readInt();
isMutable = source.readBoolean();
isFabricated = source.readBoolean();
+ constraints = Arrays.asList(source.createTypedArray(OverlayConstraint.CREATOR));
ensureValidState();
}
@@ -395,6 +417,17 @@ public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
return mIdentifierCached;
}
+ /**
+ * Returns the currently applied constraints (if any) for the overlay. An overlay
+ * may have constraints only when it is enabled.
+ *
+ * @hide
+ */
+ @NonNull
+ public List<OverlayConstraint> getConstraints() {
+ return constraints;
+ }
+
@SuppressWarnings("ConstantConditions")
private void ensureValidState() {
if (packageName == null) {
@@ -406,6 +439,9 @@ public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
if (baseCodePath == null) {
throw new IllegalArgumentException("baseCodePath must not be null");
}
+ if (constraints == null) {
+ throw new IllegalArgumentException("constraints must not be null");
+ }
switch (state) {
case STATE_UNKNOWN:
case STATE_MISSING_TARGET:
@@ -439,20 +475,21 @@ public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
dest.writeInt(priority);
dest.writeBoolean(isMutable);
dest.writeBoolean(isFabricated);
+ dest.writeTypedArray(constraints.toArray(new OverlayConstraint[0]), flags);
}
public static final @NonNull Parcelable.Creator<OverlayInfo> CREATOR =
- new Parcelable.Creator<OverlayInfo>() {
- @Override
- public OverlayInfo createFromParcel(Parcel source) {
- return new OverlayInfo(source);
- }
+ new Parcelable.Creator<>() {
+ @Override
+ public OverlayInfo createFromParcel(Parcel source) {
+ return new OverlayInfo(source);
+ }
- @Override
- public OverlayInfo[] newArray(int size) {
- return new OverlayInfo[size];
- }
- };
+ @Override
+ public OverlayInfo[] newArray(int size) {
+ return new OverlayInfo[size];
+ }
+ };
/**
* Return true if this overlay is enabled, i.e. should be used to overlay
@@ -461,6 +498,7 @@ public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
* Disabled overlay packages are installed but are currently not in use.
*
* @return true if the overlay is enabled, else false.
+ *
* @hide
*/
@SystemApi
@@ -479,6 +517,7 @@ public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
* debugging purposes.
*
* @return a human readable String representing the state.
+ *
* @hide
*/
public static String stateToString(@State int state) {
@@ -522,6 +561,7 @@ public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
: targetOverlayableName.hashCode());
result = prime * result + ((category == null) ? 0 : category.hashCode());
result = prime * result + ((baseCodePath == null) ? 0 : baseCodePath.hashCode());
+ result = prime * result + (constraints.isEmpty() ? 0 : constraints.hashCode());
return result;
}
@@ -566,7 +606,7 @@ public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
if (!baseCodePath.equals(other.baseCodePath)) {
return false;
}
- return true;
+ return Objects.equals(constraints, other.constraints);
}
/**
@@ -584,6 +624,7 @@ public final class OverlayInfo implements CriticalOverlayInfo, Parcelable {
+ ", targetOverlayable=" + targetOverlayableName
+ ", state=" + state + " (" + stateToString(state) + "),"
+ ", userId=" + userId
+ + ", constraints=" + OverlayConstraint.constraintsToString(constraints)
+ " }";
}
}
diff --git a/core/java/android/content/om/OverlayManager.java b/core/java/android/content/om/OverlayManager.java
index 6db7dfe4f705..fd9bfa274289 100644
--- a/core/java/android/content/om/OverlayManager.java
+++ b/core/java/android/content/om/OverlayManager.java
@@ -59,7 +59,6 @@ import java.util.List;
public class OverlayManager {
private final IOverlayManager mService;
- private final Context mContext;
private final OverlayManagerImpl mOverlayManagerImpl;
/**
@@ -137,7 +136,6 @@ public class OverlayManager {
*/
@SuppressLint("ReferencesHidden")
public OverlayManager(@NonNull Context context, @Nullable IOverlayManager service) {
- mContext = context;
mService = service;
mOverlayManagerImpl = new OverlayManagerImpl(context);
}
@@ -161,7 +159,7 @@ public class OverlayManager {
* @param packageName the name of the overlay package to enable.
* @param user The user for which to change the overlay.
*
- * @throws SecurityException when caller is not allowed to enable {@param packageName}
+ * @throws SecurityException when caller is not allowed to enable {@code packageName}
* @throws IllegalStateException when enabling fails otherwise
*
* @hide
@@ -196,7 +194,7 @@ public class OverlayManager {
* @param enable {@code false} if the overlay should be turned off.
* @param user The user for which to change the overlay.
*
- * @throws SecurityException when caller is not allowed to enable/disable {@param packageName}
+ * @throws SecurityException when caller is not allowed to enable/disable {@code packageName}
* @throws IllegalStateException when enabling/disabling fails otherwise
*
* @hide
@@ -220,6 +218,43 @@ public class OverlayManager {
}
/**
+ * Enable an overlay package for a specific set of constraints. In case of multiple constraints,
+ * the overlay would be enabled when any of the given constraints are satisfied.
+ *
+ * Re-enabling an overlay with new constraints updates the constraints for the overlay.
+ *
+ * The caller must pass the actor requirements specified in the class comment.
+ *
+ * @param packageName the name of the overlay package to enable.
+ * @param user The user for which to change the overlay.
+ * @param constraints list of {@link OverlayConstraint} for enabling the overlay.
+ *
+ * @throws SecurityException when caller is not allowed to enable {@code packageName}
+ * @throws IllegalStateException when enabling fails otherwise
+ *
+ * @see OverlayConstraint
+ *
+ * @hide
+ */
+ @RequiresPermission(anyOf = {
+ "android.permission.INTERACT_ACROSS_USERS",
+ "android.permission.INTERACT_ACROSS_USERS_FULL"
+ })
+ public void enableWithConstraints(@NonNull final String packageName, @NonNull UserHandle user,
+ @Nullable final List<OverlayConstraint> constraints)
+ throws SecurityException, IllegalStateException {
+ try {
+ if (!mService.enableWithConstraints(packageName, user.getIdentifier(), constraints)) {
+ throw new IllegalStateException("enableWithConstraints failed");
+ }
+ } catch (SecurityException e) {
+ rethrowSecurityException(e);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns information about the overlay with the given package name for
* the specified user.
*
@@ -299,7 +334,6 @@ public class OverlayManager {
@RequiresPermission(anyOf = {
"android.permission.INTERACT_ACROSS_USERS",
})
- @NonNull
public void invalidateCachesForOverlay(@NonNull final String targetPackageName,
@NonNull UserHandle user) {
try {
diff --git a/core/java/android/content/om/OverlayManagerTransaction.java b/core/java/android/content/om/OverlayManagerTransaction.java
index 87b2e9350aa1..f9eb5e010d71 100644
--- a/core/java/android/content/om/OverlayManagerTransaction.java
+++ b/core/java/android/content/om/OverlayManagerTransaction.java
@@ -18,8 +18,6 @@ package android.content.om;
import static android.annotation.SystemApi.Client.SYSTEM_SERVER;
-import static com.android.internal.util.Preconditions.checkNotNull;
-
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -29,13 +27,15 @@ import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.UserHandle;
+import android.text.TextUtils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.Iterator;
import java.util.List;
-import java.util.Locale;
import java.util.Objects;
/**
@@ -106,7 +106,9 @@ public final class OverlayManagerTransaction implements Parcelable {
final OverlayIdentifier overlay = source.readParcelable(null, android.content.om.OverlayIdentifier.class);
final int userId = source.readInt();
final Bundle extras = source.readBundle(null);
- mRequests.add(new Request(request, overlay, userId, extras));
+ OverlayConstraint[] constraints = source.createTypedArray(OverlayConstraint.CREATOR);
+ mRequests.add(new Request(request, overlay, userId, extras,
+ Arrays.asList(constraints)));
}
mSelfTargeting = false;
}
@@ -115,6 +117,7 @@ public final class OverlayManagerTransaction implements Parcelable {
* Get the iterator of requests
*
* @return the iterator of request
+ *
* @hide
*/
@SuppressLint("ReferencesHidden")
@@ -145,6 +148,8 @@ public final class OverlayManagerTransaction implements Parcelable {
@IntDef(prefix = "TYPE_", value = {
TYPE_SET_ENABLED,
TYPE_SET_DISABLED,
+ TYPE_REGISTER_FABRICATED,
+ TYPE_UNREGISTER_FABRICATED,
})
@Retention(RetentionPolicy.SOURCE)
@interface RequestType {}
@@ -166,23 +171,51 @@ public final class OverlayManagerTransaction implements Parcelable {
@Nullable
public final Bundle extras;
+ /**
+ * @hide
+ */
+ @NonNull
+ public final List<OverlayConstraint> constraints;
+
public Request(@RequestType final int type, @NonNull final OverlayIdentifier overlay,
final int userId) {
- this(type, overlay, userId, null /* extras */);
+ this(type, overlay, userId, null /* extras */,
+ Collections.emptyList() /* constraints */);
}
public Request(@RequestType final int type, @NonNull final OverlayIdentifier overlay,
final int userId, @Nullable Bundle extras) {
+ this(type, overlay, userId, extras, Collections.emptyList() /* constraints */);
+ }
+
+ /**
+ * @hide
+ */
+ public Request(@RequestType final int type, @NonNull final OverlayIdentifier overlay,
+ final int userId, @NonNull List<OverlayConstraint> constraints) {
+ this(type, overlay, userId, null /* extras */, constraints);
+ }
+
+ /**
+ * @hide
+ */
+ public Request(@RequestType final int type, @NonNull final OverlayIdentifier overlay,
+ final int userId, @Nullable Bundle extras,
+ @NonNull List<OverlayConstraint> constraints) {
this.type = type;
this.overlay = overlay;
this.userId = userId;
this.extras = extras;
+ Objects.requireNonNull(constraints);
+ this.constraints = constraints;
}
@Override
public String toString() {
- return String.format(Locale.US, "Request{type=0x%02x (%s), overlay=%s, userId=%d}",
- type, typeToString(), overlay, userId);
+ return TextUtils.formatSimple(
+ "Request{type=0x%02x (%s), overlay=%s, userId=%d, constraints=%s}",
+ type, typeToString(), overlay, userId,
+ OverlayConstraint.constraintsToString(constraints));
}
/**
@@ -205,6 +238,7 @@ public final class OverlayManagerTransaction implements Parcelable {
/**
* Builder class for OverlayManagerTransaction objects.
* TODO(b/269197647): mark the API used by the systemUI.
+ *
* @hide
*/
public static final class Builder {
@@ -238,11 +272,27 @@ public final class OverlayManagerTransaction implements Parcelable {
/**
* @hide
*/
+ public Builder setEnabled(@NonNull OverlayIdentifier overlay, boolean enable,
+ @NonNull List<OverlayConstraint> constraints) {
+ return setEnabled(overlay, enable, UserHandle.myUserId(), constraints);
+ }
+
+ /**
+ * @hide
+ */
public Builder setEnabled(@NonNull OverlayIdentifier overlay, boolean enable, int userId) {
- checkNotNull(overlay);
+ return setEnabled(overlay, enable, userId, Collections.emptyList() /* constraints */);
+ }
+
+ /**
+ * @hide
+ */
+ public Builder setEnabled(@NonNull OverlayIdentifier overlay, boolean enable, int userId,
+ @NonNull List<OverlayConstraint> constraints) {
+ Objects.requireNonNull(overlay);
@Request.RequestType final int type =
enable ? Request.TYPE_SET_ENABLED : Request.TYPE_SET_DISABLED;
- mRequests.add(new Request(type, overlay, userId));
+ mRequests.add(new Request(type, overlay, userId, constraints));
return this;
}
@@ -251,6 +301,7 @@ public final class OverlayManagerTransaction implements Parcelable {
* applications to overlay on itself resources. The overlay target is itself, or the Android
* package, and the work range is only in caller application.
* @param selfTargeting whether the overlay is self-targeting, the default is false.
+ *
* @hide
*/
public Builder setSelfTargeting(boolean selfTargeting) {
@@ -324,23 +375,24 @@ public final class OverlayManagerTransaction implements Parcelable {
dest.writeParcelable(req.overlay, flags);
dest.writeInt(req.userId);
dest.writeBundle(req.extras);
+ dest.writeTypedArray(req.constraints.toArray(new OverlayConstraint[0]), flags);
}
}
@NonNull
public static final Parcelable.Creator<OverlayManagerTransaction> CREATOR =
- new Parcelable.Creator<OverlayManagerTransaction>() {
-
- @Override
- public OverlayManagerTransaction createFromParcel(Parcel source) {
- return new OverlayManagerTransaction(source);
- }
-
- @Override
- public OverlayManagerTransaction[] newArray(int size) {
- return new OverlayManagerTransaction[size];
- }
- };
+ new Parcelable.Creator<>() {
+
+ @Override
+ public OverlayManagerTransaction createFromParcel(Parcel source) {
+ return new OverlayManagerTransaction(source);
+ }
+
+ @Override
+ public OverlayManagerTransaction[] newArray(int size) {
+ return new OverlayManagerTransaction[size];
+ }
+ };
private static Request generateRegisterFabricatedOverlayRequest(
@NonNull FabricatedOverlay overlay) {
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 0369b7d9bc28..6ae2df2cd7a2 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -16,6 +16,7 @@
package android.content.pm;
+import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM;
import static android.content.pm.SigningInfo.AppSigningSchemeVersion;
import static android.media.audio.Flags.FLAG_FEATURE_SPATIAL_AUDIO_HEADTRACKING_LOW_LATENCY;
@@ -11659,11 +11660,22 @@ public abstract class PackageManager {
}
}
- private static final PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo>
- sApplicationInfoCache =
- new PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo>(
- 2048, PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE,
- "getApplicationInfo") {
+ private static String packageInfoApi() {
+ return PropertyInvalidatedCache.apiFromProperty(
+ PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE);
+ }
+
+ // The maximum number of entries to keep in the packageInfo and applicationInfo caches.
+ private final static int MAX_INFO_CACHE_ENTRIES = 2048;
+
+ /** @hide */
+ @VisibleForTesting
+ public static final PropertyInvalidatedCache<ApplicationInfoQuery, ApplicationInfo>
+ sApplicationInfoCache = new PropertyInvalidatedCache<>(
+ new PropertyInvalidatedCache.Args(MODULE_SYSTEM)
+ .maxEntries(MAX_INFO_CACHE_ENTRIES).api(packageInfoApi()).cacheNulls(true),
+ "getApplicationInfo", null) {
+
@Override
public ApplicationInfo recompute(ApplicationInfoQuery query) {
return getApplicationInfoAsUserUncached(
@@ -11749,10 +11761,11 @@ public abstract class PackageManager {
}
private static final PropertyInvalidatedCache<PackageInfoQuery, PackageInfo>
- sPackageInfoCache =
- new PropertyInvalidatedCache<PackageInfoQuery, PackageInfo>(
- 2048, PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE,
- "getPackageInfo") {
+ sPackageInfoCache = new PropertyInvalidatedCache<>(
+ new PropertyInvalidatedCache.Args(MODULE_SYSTEM)
+ .maxEntries(MAX_INFO_CACHE_ENTRIES).api(packageInfoApi()).cacheNulls(true),
+ "getPackageInfo", null) {
+
@Override
public PackageInfo recompute(PackageInfoQuery query) {
return getPackageInfoAsUserUncached(
diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig
index e24f1a8155ef..027eb9df1d9e 100644
--- a/core/java/android/content/res/flags.aconfig
+++ b/core/java/android/content/res/flags.aconfig
@@ -76,6 +76,14 @@ flag {
}
flag {
+ name: "rro_constraints"
+ is_exported: false
+ namespace: "resource_manager"
+ description: "Feature flag for setting constraints for a RRO"
+ bug: "371801644"
+}
+
+flag {
name: "rro_control_for_android_no_overlayable"
is_exported: true
namespace: "resource_manager"
diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java
index ca3e3d2ad61b..6ec70045f1f4 100644
--- a/core/java/android/hardware/Camera.java
+++ b/core/java/android/hardware/Camera.java
@@ -358,8 +358,7 @@ public class Camera {
CameraInfo cameraInfo);
private static int getDevicePolicyFromContext(Context context) {
- if (context.getDeviceId() == DEVICE_ID_DEFAULT
- || !android.companion.virtual.flags.Flags.virtualCamera()) {
+ if (context.getDeviceId() == DEVICE_ID_DEFAULT) {
return DEVICE_POLICY_DEFAULT;
}
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index bfaff941939c..bcb7ebfb286f 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -67,6 +67,7 @@ import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.HandlerThread;
import android.os.IBinder;
+import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
@@ -591,8 +592,7 @@ public final class CameraManager {
/** @hide */
public int getDevicePolicyFromContext(@NonNull Context context) {
- if (context.getDeviceId() == DEVICE_ID_DEFAULT
- || !android.companion.virtual.flags.Flags.virtualCamera()) {
+ if (context.getDeviceId() == DEVICE_ID_DEFAULT) {
return DEVICE_POLICY_DEFAULT;
}
@@ -1705,7 +1705,9 @@ public final class CameraManager {
return ICameraService.ROTATION_OVERRIDE_NONE;
}
- if (context != null) {
+ // Isolated process does not have access to ActivityTaskManager service, which is used
+ // indirectly in `ActivityManager.getAppTasks()`.
+ if (context != null && !Process.isIsolated()) {
final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
if (activityManager != null) {
for (ActivityManager.AppTask appTask : activityManager.getAppTasks()) {
@@ -2576,11 +2578,6 @@ public final class CameraManager {
private boolean shouldHideCamera(int currentDeviceId, int devicePolicy,
DeviceCameraInfo info) {
- if (!android.companion.virtualdevice.flags.Flags.cameraDeviceAwareness()) {
- // Don't hide any cameras if the device-awareness feature flag is disabled.
- return false;
- }
-
if (devicePolicy == DEVICE_POLICY_DEFAULT && info.mDeviceId == DEVICE_ID_DEFAULT) {
// Don't hide default-device cameras for a default-policy virtual device.
return false;
diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl
index 7fc7e4d81afa..1c2150f3c09f 100644
--- a/core/java/android/hardware/input/IInputManager.aidl
+++ b/core/java/android/hardware/input/IInputManager.aidl
@@ -34,6 +34,7 @@ import android.hardware.input.KeyboardLayoutSelectionResult;
import android.hardware.input.TouchCalibration;
import android.os.CombinedVibration;
import android.hardware.input.IInputSensorEventListener;
+import android.hardware.input.IKeyEventActivityListener;
import android.hardware.input.InputSensorInfo;
import android.hardware.input.KeyGlyphMap;
import android.hardware.lights.Light;
@@ -213,6 +214,16 @@ interface IInputManager {
void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener);
+ @EnforcePermission("LISTEN_FOR_KEY_ACTIVITY")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY)")
+ boolean registerKeyEventActivityListener(IKeyEventActivityListener listener);
+
+ @EnforcePermission("LISTEN_FOR_KEY_ACTIVITY")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+ + "android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY)")
+ boolean unregisterKeyEventActivityListener(IKeyEventActivityListener listener);
+
// Get the bluetooth address of an input device if known, returning null if it either is not
// connected via bluetooth or if the address cannot be determined.
@EnforcePermission("BLUETOOTH")
diff --git a/core/java/android/hardware/input/IKeyEventActivityListener.aidl b/core/java/android/hardware/input/IKeyEventActivityListener.aidl
new file mode 100644
index 000000000000..b3097d409a2d
--- /dev/null
+++ b/core/java/android/hardware/input/IKeyEventActivityListener.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.input;
+
+/** @hide */
+oneway interface IKeyEventActivityListener
+{
+ void onKeyEventActivity();
+}
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index cf41e138047a..0ead8232c5b1 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1776,4 +1776,41 @@ public final class InputManager {
*/
boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType);
}
+
+ /** @hide */
+ public interface KeyEventActivityListener {
+ /**
+ * Reports a change for user activeness.
+ *
+ * This listener will be triggered any time a user presses a key.
+ */
+ void onKeyEventActivity();
+ }
+
+
+ /**
+ * Registers a listener for updates to key event activeness
+ *
+ * @param listener to be registered
+ * @return true if listener registered successfully
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY)
+ public boolean registerKeyEventActivityListener(@NonNull KeyEventActivityListener listener) {
+ return mGlobal.registerKeyEventActivityListener(listener);
+ }
+
+ /**
+ * Unregisters a listener for updates to key event activeness
+ *
+ * @param listener to be unregistered
+ * @return true if listener unregistered successfully, also returns true if
+ * invoked but listener was not present
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY)
+ public boolean unregisterKeyEventActivityListener(@NonNull KeyEventActivityListener listener) {
+ return mGlobal.unregisterKeyEventActivityListener(listener);
+ }
+
}
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index e79416162fc2..a9a45ae45ec3 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -26,6 +26,7 @@ import android.hardware.SensorManager;
import android.hardware.input.InputManager.InputDeviceBatteryListener;
import android.hardware.input.InputManager.InputDeviceListener;
import android.hardware.input.InputManager.KeyGestureEventHandler;
+import android.hardware.input.InputManager.KeyEventActivityListener;
import android.hardware.input.InputManager.KeyGestureEventListener;
import android.hardware.input.InputManager.KeyboardBacklightListener;
import android.hardware.input.InputManager.OnTabletModeChangedListener;
@@ -124,6 +125,13 @@ public final class InputManagerGlobal {
@Nullable
private IKeyGestureEventListener mKeyGestureEventListener;
+ private final Object mKeyEventActivityLock = new Object();
+ @GuardedBy("mKeyEventActivityLock")
+ private ArrayList<KeyEventActivityListener> mKeyEventActivityListeners;
+ @GuardedBy("mKeyEventActivityLock")
+ @Nullable
+ private IKeyEventActivityListener mKeyEventActivityListener;
+
private final Object mKeyGestureEventHandlerLock = new Object();
@GuardedBy("mKeyGestureEventHandlerLock")
@Nullable
@@ -1257,6 +1265,63 @@ public final class InputManagerGlobal {
}
}
+ private class LocalKeyEventActivityListener extends IKeyEventActivityListener.Stub {
+ @Override
+ public void onKeyEventActivity() {
+ synchronized (mKeyEventActivityLock) {
+ final int numListeners = mKeyEventActivityListeners.size();
+ for (int i = 0; i < numListeners; i++) {
+ KeyEventActivityListener listener = mKeyEventActivityListeners.get(i);
+ listener.onKeyEventActivity();
+ }
+ }
+ }
+ }
+
+ boolean registerKeyEventActivityListener(@NonNull KeyEventActivityListener listener) {
+ Objects.requireNonNull(listener, "listener should not be null");
+ boolean success = false;
+ synchronized (mKeyEventActivityLock) {
+ if (mKeyEventActivityListener == null) {
+ mKeyEventActivityListeners = new ArrayList<>();
+ mKeyEventActivityListener = new LocalKeyEventActivityListener();
+
+ try {
+ success = mIm.registerKeyEventActivityListener(mKeyEventActivityListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ if (mKeyEventActivityListeners.contains(listener)) {
+ throw new IllegalArgumentException("Listener has already been registered!");
+ }
+ mKeyEventActivityListeners.add(listener);
+ return success;
+ }
+ }
+
+ boolean unregisterKeyEventActivityListener(@NonNull KeyEventActivityListener listener) {
+ Objects.requireNonNull(listener, "listener should not be null");
+
+ boolean success = true;
+ synchronized (mKeyEventActivityLock) {
+ if (mKeyEventActivityListeners == null) {
+ return success;
+ }
+ mKeyEventActivityListeners.remove(listener);
+ if (mKeyEventActivityListeners.isEmpty()) {
+ try {
+ success = mIm.unregisterKeyEventActivityListener(mKeyEventActivityListener);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mKeyEventActivityListeners = null;
+ mKeyEventActivityListener = null;
+ }
+ }
+ return success;
+ }
+
/**
* Sets the keyboard layout override for the specified input device. This will set the
* keyboard layout as the default for the input device irrespective of the underlying IME
diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java
index e79b2e7becce..26044545b8d1 100644
--- a/core/java/android/os/BaseBundle.java
+++ b/core/java/android/os/BaseBundle.java
@@ -45,7 +45,8 @@ import java.util.function.BiFunction;
* {@link PersistableBundle} subclass.
*/
@android.ravenwood.annotation.RavenwoodKeepWholeClass
-public class BaseBundle {
+@SuppressWarnings("HiddenSuperclass")
+public class BaseBundle implements Parcel.ClassLoaderProvider {
/** @hide */
protected static final String TAG = "Bundle";
static final boolean DEBUG = false;
@@ -311,8 +312,9 @@ public class BaseBundle {
/**
* Return the ClassLoader currently associated with this Bundle.
+ * @hide
*/
- ClassLoader getClassLoader() {
+ public ClassLoader getClassLoader() {
return mClassLoader;
}
@@ -426,6 +428,9 @@ public class BaseBundle {
if ((mFlags & Bundle.FLAG_VERIFY_TOKENS_PRESENT) != 0) {
Intent.maybeMarkAsMissingCreatorToken(object);
}
+ } else if (object instanceof Bundle) {
+ Bundle bundle = (Bundle) object;
+ bundle.setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(this);
}
return (clazz != null) ? clazz.cast(object) : (T) object;
}
@@ -499,7 +504,7 @@ public class BaseBundle {
int[] numLazyValues = new int[]{0};
try {
parcelledData.readArrayMap(map, count, !parcelledByNative,
- /* lazy */ ownsParcel, mClassLoader, numLazyValues);
+ /* lazy */ ownsParcel, this, numLazyValues);
} catch (BadParcelableException e) {
if (sShouldDefuse) {
Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e);
diff --git a/core/java/android/os/BatteryUsageStats.java b/core/java/android/os/BatteryUsageStats.java
index 86b8fad16275..739908ef0dfc 100644
--- a/core/java/android/os/BatteryUsageStats.java
+++ b/core/java/android/os/BatteryUsageStats.java
@@ -827,12 +827,12 @@ public final class BatteryUsageStats implements Parcelable, Closeable {
parser.getAttributeLong(null, XML_ATTR_DURATION));
builder.setBatteryCapacity(
parser.getAttributeDouble(null, XML_ATTR_BATTERY_CAPACITY));
- builder.setDischargePercentage(
+ builder.addDischargePercentage(
parser.getAttributeInt(null, XML_ATTR_DISCHARGE_PERCENT));
- builder.setDischargedPowerRange(
+ builder.addDischargedPowerRange(
parser.getAttributeDouble(null, XML_ATTR_DISCHARGE_LOWER),
parser.getAttributeDouble(null, XML_ATTR_DISCHARGE_UPPER));
- builder.setDischargeDurationMs(
+ builder.addDischargeDurationMs(
parser.getAttributeLong(null, XML_ATTR_DISCHARGE_DURATION));
builder.setBatteryTimeRemainingMs(
parser.getAttributeLong(null, XML_ATTR_BATTERY_REMAINING));
@@ -1044,23 +1044,22 @@ public final class BatteryUsageStats implements Parcelable, Closeable {
}
/**
- * Sets the battery discharge amount since BatteryStats reset as percentage of the full
- * charge.
+ * Accumulates the battery discharge amount as percentage of the full charge. Can exceed 100
*/
@NonNull
- public Builder setDischargePercentage(int dischargePercentage) {
- mDischargePercentage = dischargePercentage;
+ public Builder addDischargePercentage(int dischargePercentage) {
+ mDischargePercentage += dischargePercentage;
return this;
}
/**
- * Sets the estimated battery discharge range.
+ * Accumulates the estimated battery discharge range.
*/
@NonNull
- public Builder setDischargedPowerRange(double dischargedPowerLowerBoundMah,
+ public Builder addDischargedPowerRange(double dischargedPowerLowerBoundMah,
double dischargedPowerUpperBoundMah) {
- mDischargedPowerLowerBoundMah = dischargedPowerLowerBoundMah;
- mDischargedPowerUpperBoundMah = dischargedPowerUpperBoundMah;
+ mDischargedPowerLowerBoundMah += dischargedPowerLowerBoundMah;
+ mDischargedPowerUpperBoundMah += dischargedPowerUpperBoundMah;
return this;
}
@@ -1068,8 +1067,8 @@ public final class BatteryUsageStats implements Parcelable, Closeable {
* Sets the total battery discharge time, in milliseconds.
*/
@NonNull
- public Builder setDischargeDurationMs(long durationMs) {
- mDischargeDurationMs = durationMs;
+ public Builder addDischargeDurationMs(long durationMs) {
+ mDischargeDurationMs += durationMs;
return this;
}
diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java
index a24dc5739b7e..c0591e6899b6 100644
--- a/core/java/android/os/Bundle.java
+++ b/core/java/android/os/Bundle.java
@@ -141,6 +141,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
STRIPPED.putInt("STRIPPED", 1);
}
+ private boolean isFirstRetrievedFromABundle = false;
+
/**
* Constructs a new, empty Bundle.
*/
@@ -382,7 +384,15 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
bundle.unparcel();
mOwnsLazyValues = false;
bundle.mOwnsLazyValues = false;
- mMap.putAll(bundle.mMap);
+ int N = bundle.mMap.size();
+ for (int i = 0; i < N; i++) {
+ String key = bundle.mMap.keyAt(i);
+ Object value = bundle.mMap.valueAt(i);
+ if (value instanceof Bundle) {
+ ((Bundle) value).isFirstRetrievedFromABundle = true;
+ }
+ mMap.put(key, value);
+ }
// FD and Binders state is now known if and only if both bundles already knew
if ((bundle.mFlags & FLAG_HAS_FDS) != 0) {
@@ -592,6 +602,8 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
mFlags &= ~FLAG_HAS_BINDERS_KNOWN;
if (intentClass != null && intentClass.isInstance(value)) {
setHasIntent(true);
+ } else if (value instanceof Bundle) {
+ ((Bundle) value).isFirstRetrievedFromABundle = true;
}
}
@@ -793,6 +805,9 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
*/
public void putBundle(@Nullable String key, @Nullable Bundle value) {
unparcel();
+ if (value != null) {
+ value.isFirstRetrievedFromABundle = true;
+ }
mMap.put(key, value);
}
@@ -1020,7 +1035,9 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
return null;
}
try {
- return (Bundle) o;
+ Bundle bundle = (Bundle) o;
+ bundle.setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(this);
+ return bundle;
} catch (ClassCastException e) {
typeWarning(key, o, "Bundle", e);
return null;
@@ -1028,6 +1045,21 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
}
/**
+ * Set the ClassLoader of a bundle to its container bundle. This is necessary so that when a
+ * bundle's ClassLoader is changed, it can be propagated to its children. Do this only when it
+ * is retrieved from the container bundle first time though. Once it is accessed outside of its
+ * container, its ClassLoader should no longer be changed by its container anymore.
+ *
+ * @param containerBundle the bundle this bundle is retrieved from.
+ */
+ void setClassLoaderSameAsContainerBundleWhenRetrievedFirstTime(BaseBundle containerBundle) {
+ if (!isFirstRetrievedFromABundle) {
+ setClassLoader(containerBundle.getClassLoader());
+ isFirstRetrievedFromABundle = true;
+ }
+ }
+
+ /**
* Returns the value associated with the given key, or {@code null} if
* no mapping of the desired type exists for the given key or a {@code null}
* value is explicitly associated with the key.
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index e58934746c14..49d3f06eb80f 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -46,6 +46,7 @@ import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import dalvik.annotation.optimization.CriticalNative;
@@ -4661,7 +4662,7 @@ public final class Parcel {
* @hide
*/
@Nullable
- public Object readLazyValue(@Nullable ClassLoader loader) {
+ private Object readLazyValue(@Nullable ClassLoaderProvider loaderProvider) {
int start = dataPosition();
int type = readInt();
if (isLengthPrefixed(type)) {
@@ -4672,12 +4673,17 @@ public final class Parcel {
int end = MathUtils.addOrThrow(dataPosition(), objectLength);
int valueLength = end - start;
setDataPosition(end);
- return new LazyValue(this, start, valueLength, type, loader);
+ return new LazyValue(this, start, valueLength, type, loaderProvider);
} else {
- return readValue(type, loader, /* clazz */ null);
+ return readValue(type, getClassLoader(loaderProvider), /* clazz */ null);
}
}
+ @Nullable
+ private static ClassLoader getClassLoader(@Nullable ClassLoaderProvider loaderProvider) {
+ return loaderProvider == null ? null : loaderProvider.getClassLoader();
+ }
+
private static final class LazyValue implements BiFunction<Class<?>, Class<?>[], Object> {
/**
@@ -4691,7 +4697,12 @@ public final class Parcel {
private final int mPosition;
private final int mLength;
private final int mType;
- @Nullable private final ClassLoader mLoader;
+ // this member is set when a bundle that includes a LazyValue is unparceled. But it is used
+ // when apply method is called. Between these 2 events, the bundle's ClassLoader could have
+ // changed. Let the bundle be a ClassLoaderProvider allows the bundle provides its current
+ // ClassLoader at the time apply method is called.
+ @NonNull
+ private final ClassLoaderProvider mLoaderProvider;
@Nullable private Object mObject;
/**
@@ -4702,12 +4713,13 @@ public final class Parcel {
*/
@Nullable private volatile Parcel mSource;
- LazyValue(Parcel source, int position, int length, int type, @Nullable ClassLoader loader) {
+ LazyValue(Parcel source, int position, int length, int type,
+ @NonNull ClassLoaderProvider loaderProvider) {
mSource = requireNonNull(source);
mPosition = position;
mLength = length;
mType = type;
- mLoader = loader;
+ mLoaderProvider = loaderProvider;
}
@Override
@@ -4720,7 +4732,8 @@ public final class Parcel {
int restore = source.dataPosition();
try {
source.setDataPosition(mPosition);
- mObject = source.readValue(mLoader, clazz, itemTypes);
+ mObject = source.readValue(mLoaderProvider.getClassLoader(), clazz,
+ itemTypes);
} finally {
source.setDataPosition(restore);
}
@@ -4758,6 +4771,12 @@ public final class Parcel {
return Parcel.hasFileDescriptors(mObject);
}
+ /** @hide */
+ @VisibleForTesting
+ public ClassLoader getClassLoader() {
+ return mLoaderProvider.getClassLoader();
+ }
+
@Override
public String toString() {
return (mSource != null)
@@ -4793,7 +4812,8 @@ public final class Parcel {
return Objects.equals(mObject, value.mObject);
}
// Better safely fail here since this could mean we get different objects.
- if (!Objects.equals(mLoader, value.mLoader)) {
+ if (!Objects.equals(mLoaderProvider.getClassLoader(),
+ value.mLoaderProvider.getClassLoader())) {
return false;
}
// Otherwise compare metadata prior to comparing payload.
@@ -4807,10 +4827,24 @@ public final class Parcel {
@Override
public int hashCode() {
// Accessing mSource first to provide memory barrier for mObject
- return Objects.hash(mSource == null, mObject, mLoader, mType, mLength);
+ return Objects.hash(mSource == null, mObject, mLoaderProvider.getClassLoader(), mType,
+ mLength);
}
}
+ /**
+ * Provides a ClassLoader.
+ * @hide
+ */
+ public interface ClassLoaderProvider {
+ /**
+ * Returns a ClassLoader.
+ *
+ * @return ClassLoader
+ */
+ ClassLoader getClassLoader();
+ }
+
/** Same as {@link #readValue(ClassLoader, Class, Class[])} without any item types. */
private <T> T readValue(int type, @Nullable ClassLoader loader, @Nullable Class<T> clazz) {
// Avoids allocating Class[0] array
@@ -5551,8 +5585,8 @@ public final class Parcel {
}
private void readArrayMapInternal(@NonNull ArrayMap<? super String, Object> outVal,
- int size, @Nullable ClassLoader loader) {
- readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loader, null);
+ int size, @Nullable ClassLoaderProvider loaderProvider) {
+ readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loaderProvider, null);
}
/**
@@ -5566,11 +5600,12 @@ public final class Parcel {
* @hide
*/
void readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted,
- boolean lazy, @Nullable ClassLoader loader, int[] lazyValueCount) {
+ boolean lazy, @Nullable ClassLoaderProvider loaderProvider, int[] lazyValueCount) {
ensureWithinMemoryLimit(SIZE_COMPLEX_TYPE, size);
while (size > 0) {
String key = readString();
- Object value = (lazy) ? readLazyValue(loader) : readValue(loader);
+ Object value = (lazy) ? readLazyValue(loaderProvider) : readValue(
+ getClassLoader(loaderProvider));
if (value instanceof LazyValue) {
lazyValueCount[0]++;
}
@@ -5591,12 +5626,12 @@ public final class Parcel {
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public void readArrayMap(@NonNull ArrayMap<? super String, Object> outVal,
- @Nullable ClassLoader loader) {
+ @Nullable ClassLoaderProvider loaderProvider) {
final int N = readInt();
if (N < 0) {
return;
}
- readArrayMapInternal(outVal, N, loader);
+ readArrayMapInternal(outVal, N, loaderProvider);
}
/**
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index a480a3b013bb..daa5584462ba 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -499,3 +499,11 @@ flag {
description: "Collect sqlite performance metrics for discrete ops."
bug: "377584611"
}
+
+flag {
+ name: "app_ops_service_handler_fix"
+ is_fixed_read_only: true
+ namespace: "permissions"
+ description: "Use IoThread handler for AppOpsService background/IO work."
+ bug: "394380603"
+}
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java
index 6eaef78ff608..f7f4eeca58e2 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -17,7 +17,6 @@
package android.provider;
import android.Manifest;
-import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
@@ -50,7 +49,6 @@ import android.text.TextUtils;
import android.util.Patterns;
import com.android.internal.telephony.SmsApplication;
-import com.android.internal.telephony.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -3196,7 +3194,6 @@ public final class Telephony {
* See 3GPP TS 23.501 section 5.6.13
* <P>Type: INTEGER</P>
*/
- @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
public static final String ALWAYS_ON = "always_on";
/**
@@ -3307,7 +3304,6 @@ public final class Telephony {
* connected, in bytes.
* <p>Type: INTEGER </p>
*/
- @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
public static final String MTU_V4 = "mtu_v4";
/**
@@ -3315,7 +3311,6 @@ public final class Telephony {
* connected, in bytes.
* <p>Type: INTEGER </p>
*/
- @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
public static final String MTU_V6 = "mtu_v6";
/**
@@ -3338,14 +3333,12 @@ public final class Telephony {
* {@code true} if this APN visible to the user, {@code false} otherwise.
* <p>Type: INTEGER (boolean)</p>
*/
- @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
public static final String USER_VISIBLE = "user_visible";
/**
* {@code true} if the user allowed to edit this APN, {@code false} otherwise.
* <p>Type: INTEGER (boolean)</p>
*/
- @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
public static final String USER_EDITABLE = "user_editable";
/**
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index e8e66210bca6..945975a88cd5 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -32,6 +32,7 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACK
import android.annotation.IntDef;
import android.annotation.Nullable;
+import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
@@ -168,8 +169,9 @@ public class InsetsSourceConsumer {
// Reset the applier to the default one which has the most lightweight implementation.
setSurfaceParamsApplier(InsetsAnimationControlRunner.SurfaceParamsApplier.DEFAULT);
} else {
- if (lastControl != null && InsetsSource.getInsetSide(lastControl.getInsetsHint())
- != InsetsSource.getInsetSide(control.getInsetsHint())) {
+ if (lastControl != null && !Insets.NONE.equals(lastControl.getInsetsHint())
+ && InsetsSource.getInsetSide(lastControl.getInsetsHint())
+ != InsetsSource.getInsetSide(control.getInsetsHint())) {
// The source has been moved to a different side. The coordinates are stale.
// Canceling existing animation if there is any.
cancelTypes[0] |= mType;
diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java
index 9e97a8eb58aa..2895bf3f846a 100644
--- a/core/java/android/view/ViewConfiguration.java
+++ b/core/java/android/view/ViewConfiguration.java
@@ -21,7 +21,9 @@ import android.annotation.NonNull;
import android.annotation.TestApi;
import android.annotation.UiContext;
import android.app.Activity;
+import android.app.ActivityThread;
import android.app.AppGlobals;
+import android.app.Application;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.res.Configuration;
@@ -39,14 +41,13 @@ import android.util.SparseArray;
import android.util.TypedValue;
import android.view.flags.Flags;
+import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
/**
* Contains methods to standard constants used in the UI for timeouts, sizes, and distances.
*/
public class ViewConfiguration {
- private static final String TAG = "ViewConfiguration";
-
/**
* Defines the width of the horizontal scrollbar and the height of the vertical scrollbar in
* dips
@@ -349,6 +350,8 @@ public class ViewConfiguration {
*/
private static final int SMART_SELECTION_INITIALIZING_TIMEOUT_IN_MILLISECOND = 500;
+ private static ResourceCache sResourceCache = new ResourceCache();
+
private final boolean mConstructedWithContext;
private final int mEdgeSlop;
private final int mFadingEdgeLength;
@@ -374,7 +377,6 @@ public class ViewConfiguration {
private final int mOverscrollDistance;
private final int mOverflingDistance;
private final boolean mViewTouchScreenHapticScrollFeedbackEnabled;
- @UnsupportedAppUsage
private final boolean mFadingMarqueeEnabled;
private final long mGlobalActionsKeyTimeout;
private final float mVerticalScrollFactor;
@@ -468,14 +470,12 @@ public class ViewConfiguration {
mEdgeSlop = (int) (sizeAndDensity * EDGE_SLOP + 0.5f);
mFadingEdgeLength = (int) (sizeAndDensity * FADING_EDGE_LENGTH + 0.5f);
- mScrollbarSize = res.getDimensionPixelSize(
- com.android.internal.R.dimen.config_scrollbarSize);
+ mScrollbarSize = res.getDimensionPixelSize(R.dimen.config_scrollbarSize);
mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f);
mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f);
final TypedValue multiplierValue = new TypedValue();
- res.getValue(
- com.android.internal.R.dimen.config_ambiguousGestureMultiplier,
+ res.getValue(R.dimen.config_ambiguousGestureMultiplier,
multiplierValue,
true /*resolveRefs*/);
mAmbiguousGestureMultiplier = Math.max(1.0f, multiplierValue.getFloat());
@@ -488,8 +488,7 @@ public class ViewConfiguration {
mOverflingDistance = (int) (sizeAndDensity * OVERFLING_DISTANCE + 0.5f);
if (!sHasPermanentMenuKeySet) {
- final int configVal = res.getInteger(
- com.android.internal.R.integer.config_overrideHasPermanentMenuKey);
+ final int configVal = res.getInteger(R.integer.config_overrideHasPermanentMenuKey);
switch (configVal) {
default:
@@ -516,32 +515,27 @@ public class ViewConfiguration {
}
}
- mFadingMarqueeEnabled = res.getBoolean(
- com.android.internal.R.bool.config_ui_enableFadingMarquee);
- mTouchSlop = res.getDimensionPixelSize(
- com.android.internal.R.dimen.config_viewConfigurationTouchSlop);
+ mFadingMarqueeEnabled = res.getBoolean(R.bool.config_ui_enableFadingMarquee);
+ mTouchSlop = res.getDimensionPixelSize(R.dimen.config_viewConfigurationTouchSlop);
mHandwritingSlop = res.getDimensionPixelSize(
- com.android.internal.R.dimen.config_viewConfigurationHandwritingSlop);
- mHoverSlop = res.getDimensionPixelSize(
- com.android.internal.R.dimen.config_viewConfigurationHoverSlop);
+ R.dimen.config_viewConfigurationHandwritingSlop);
+ mHoverSlop = res.getDimensionPixelSize(R.dimen.config_viewConfigurationHoverSlop);
mMinScrollbarTouchTarget = res.getDimensionPixelSize(
- com.android.internal.R.dimen.config_minScrollbarTouchTarget);
+ R.dimen.config_minScrollbarTouchTarget);
mPagingTouchSlop = mTouchSlop * 2;
mDoubleTapTouchSlop = mTouchSlop;
mHandwritingGestureLineMargin = res.getDimensionPixelSize(
- com.android.internal.R.dimen.config_viewConfigurationHandwritingGestureLineMargin);
+ R.dimen.config_viewConfigurationHandwritingGestureLineMargin);
- mMinimumFlingVelocity = res.getDimensionPixelSize(
- com.android.internal.R.dimen.config_viewMinFlingVelocity);
- mMaximumFlingVelocity = res.getDimensionPixelSize(
- com.android.internal.R.dimen.config_viewMaxFlingVelocity);
+ mMinimumFlingVelocity = res.getDimensionPixelSize(R.dimen.config_viewMinFlingVelocity);
+ mMaximumFlingVelocity = res.getDimensionPixelSize(R.dimen.config_viewMaxFlingVelocity);
int configMinRotaryEncoderFlingVelocity = res.getDimensionPixelSize(
- com.android.internal.R.dimen.config_viewMinRotaryEncoderFlingVelocity);
+ R.dimen.config_viewMinRotaryEncoderFlingVelocity);
int configMaxRotaryEncoderFlingVelocity = res.getDimensionPixelSize(
- com.android.internal.R.dimen.config_viewMaxRotaryEncoderFlingVelocity);
+ R.dimen.config_viewMaxRotaryEncoderFlingVelocity);
if (configMinRotaryEncoderFlingVelocity < 0 || configMaxRotaryEncoderFlingVelocity < 0) {
mMinimumRotaryEncoderFlingVelocity = NO_FLING_MIN_VELOCITY;
mMaximumRotaryEncoderFlingVelocity = NO_FLING_MAX_VELOCITY;
@@ -551,8 +545,7 @@ public class ViewConfiguration {
}
int configRotaryEncoderHapticScrollFeedbackTickIntervalPixels =
- res.getDimensionPixelSize(
- com.android.internal.R.dimen
+ res.getDimensionPixelSize(R.dimen
.config_rotaryEncoderAxisScrollTickInterval);
mRotaryEncoderHapticScrollFeedbackTickIntervalPixels =
configRotaryEncoderHapticScrollFeedbackTickIntervalPixels > 0
@@ -560,41 +553,31 @@ public class ViewConfiguration {
: NO_HAPTIC_SCROLL_TICK_INTERVAL;
mRotaryEncoderHapticScrollFeedbackEnabled =
- res.getBoolean(
- com.android.internal.R.bool
+ res.getBoolean(R.bool
.config_viewRotaryEncoderHapticScrollFedbackEnabled);
- mGlobalActionsKeyTimeout = res.getInteger(
- com.android.internal.R.integer.config_globalActionsKeyTimeout);
+ mGlobalActionsKeyTimeout = res.getInteger(R.integer.config_globalActionsKeyTimeout);
- mHorizontalScrollFactor = res.getDimensionPixelSize(
- com.android.internal.R.dimen.config_horizontalScrollFactor);
- mVerticalScrollFactor = res.getDimensionPixelSize(
- com.android.internal.R.dimen.config_verticalScrollFactor);
+ mHorizontalScrollFactor = res.getDimensionPixelSize(R.dimen.config_horizontalScrollFactor);
+ mVerticalScrollFactor = res.getDimensionPixelSize(R.dimen.config_verticalScrollFactor);
mShowMenuShortcutsWhenKeyboardPresent = res.getBoolean(
- com.android.internal.R.bool.config_showMenuShortcutsWhenKeyboardPresent);
+ R.bool.config_showMenuShortcutsWhenKeyboardPresent);
- mMinScalingSpan = res.getDimensionPixelSize(
- com.android.internal.R.dimen.config_minScalingSpan);
+ mMinScalingSpan = res.getDimensionPixelSize(R.dimen.config_minScalingSpan);
- mScreenshotChordKeyTimeout = res.getInteger(
- com.android.internal.R.integer.config_screenshotChordKeyTimeout);
+ mScreenshotChordKeyTimeout = res.getInteger(R.integer.config_screenshotChordKeyTimeout);
mSmartSelectionInitializedTimeout = res.getInteger(
- com.android.internal.R.integer.config_smartSelectionInitializedTimeoutMillis);
+ R.integer.config_smartSelectionInitializedTimeoutMillis);
mSmartSelectionInitializingTimeout = res.getInteger(
- com.android.internal.R.integer.config_smartSelectionInitializingTimeoutMillis);
- mPreferKeepClearForFocusEnabled = res.getBoolean(
- com.android.internal.R.bool.config_preferKeepClearForFocus);
+ R.integer.config_smartSelectionInitializingTimeoutMillis);
+ mPreferKeepClearForFocusEnabled = res.getBoolean(R.bool.config_preferKeepClearForFocus);
mViewBasedRotaryEncoderScrollHapticsEnabledConfig =
- res.getBoolean(
- com.android.internal.R.bool.config_viewBasedRotaryEncoderHapticsEnabled);
+ res.getBoolean(R.bool.config_viewBasedRotaryEncoderHapticsEnabled);
mViewTouchScreenHapticScrollFeedbackEnabled =
Flags.enableScrollFeedbackForTouch()
- ? res.getBoolean(
- com.android.internal.R.bool
- .config_viewTouchScreenHapticScrollFeedbackEnabled)
+ ? res.getBoolean(R.bool.config_viewTouchScreenHapticScrollFeedbackEnabled)
: false;
}
@@ -632,6 +615,7 @@ public class ViewConfiguration {
@VisibleForTesting
public static void resetCacheForTesting() {
sConfigurations.clear();
+ sResourceCache = new ResourceCache();
}
/**
@@ -707,7 +691,7 @@ public class ViewConfiguration {
* components.
*/
public static int getPressedStateDuration() {
- return PRESSED_STATE_DURATION;
+ return sResourceCache.getPressedStateDuration();
}
/**
@@ -752,7 +736,7 @@ public class ViewConfiguration {
* considered to be a tap.
*/
public static int getTapTimeout() {
- return TAP_TIMEOUT;
+ return sResourceCache.getTapTimeout();
}
/**
@@ -761,7 +745,7 @@ public class ViewConfiguration {
* considered to be a tap.
*/
public static int getJumpTapTimeout() {
- return JUMP_TAP_TIMEOUT;
+ return sResourceCache.getJumpTapTimeout();
}
/**
@@ -770,7 +754,7 @@ public class ViewConfiguration {
* double-tap.
*/
public static int getDoubleTapTimeout() {
- return DOUBLE_TAP_TIMEOUT;
+ return sResourceCache.getDoubleTapTimeout();
}
/**
@@ -782,7 +766,7 @@ public class ViewConfiguration {
*/
@UnsupportedAppUsage
public static int getDoubleTapMinTime() {
- return DOUBLE_TAP_MIN_TIME;
+ return sResourceCache.getDoubleTapMinTime();
}
/**
@@ -792,7 +776,7 @@ public class ViewConfiguration {
* @hide
*/
public static int getHoverTapTimeout() {
- return HOVER_TAP_TIMEOUT;
+ return sResourceCache.getHoverTapTimeout();
}
/**
@@ -803,7 +787,7 @@ public class ViewConfiguration {
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public static int getHoverTapSlop() {
- return HOVER_TAP_SLOP;
+ return sResourceCache.getHoverTapSlop();
}
/**
@@ -1044,7 +1028,7 @@ public class ViewConfiguration {
* in milliseconds.
*/
public static long getZoomControlsTimeout() {
- return ZOOM_CONTROLS_TIMEOUT;
+ return sResourceCache.getZoomControlsTimeout();
}
/**
@@ -1113,14 +1097,14 @@ public class ViewConfiguration {
* friction.
*/
public static float getScrollFriction() {
- return SCROLL_FRICTION;
+ return sResourceCache.getScrollFriction();
}
/**
* @return the default duration in milliseconds for {@link ActionMode#hide(long)}.
*/
public static long getDefaultActionModeHideDuration() {
- return ACTION_MODE_HIDE_DURATION_DEFAULT;
+ return sResourceCache.getDefaultActionModeHideDuration();
}
/**
@@ -1471,8 +1455,137 @@ public class ViewConfiguration {
return HOVER_TOOLTIP_HIDE_SHORT_TIMEOUT;
}
- private static final int getDisplayDensity(Context context) {
+ private static int getDisplayDensity(Context context) {
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
return (int) (100.0f * metrics.density);
}
+
+ /**
+ * Fetches resource values statically and caches them locally for fast lookup. Note that these
+ * values will not be updated during the lifetime of a process, even if resource overlays are
+ * applied.
+ */
+ private static final class ResourceCache {
+
+ private int mPressedStateDuration = -1;
+ private int mTapTimeout = -1;
+ private int mJumpTapTimeout = -1;
+ private int mDoubleTapTimeout = -1;
+ private int mDoubleTapMinTime = -1;
+ private int mHoverTapTimeout = -1;
+ private int mHoverTapSlop = -1;
+ private long mZoomControlsTimeout = -1L;
+ private float mScrollFriction = -1f;
+ private long mDefaultActionModeHideDuration = -1L;
+
+ public int getPressedStateDuration() {
+ if (mPressedStateDuration < 0) {
+ Resources resources = getCurrentResources();
+ mPressedStateDuration = resources != null
+ ? resources.getInteger(R.integer.config_pressedStateDurationMillis)
+ : PRESSED_STATE_DURATION;
+ }
+ return mPressedStateDuration;
+ }
+
+ public int getTapTimeout() {
+ if (mTapTimeout < 0) {
+ Resources resources = getCurrentResources();
+ mTapTimeout = resources != null
+ ? resources.getInteger(R.integer.config_tapTimeoutMillis)
+ : TAP_TIMEOUT;
+ }
+ return mTapTimeout;
+ }
+
+ public int getJumpTapTimeout() {
+ if (mJumpTapTimeout < 0) {
+ Resources resources = getCurrentResources();
+ mJumpTapTimeout = resources != null
+ ? resources.getInteger(R.integer.config_jumpTapTimeoutMillis)
+ : JUMP_TAP_TIMEOUT;
+ }
+ return mJumpTapTimeout;
+ }
+
+ public int getDoubleTapTimeout() {
+ if (mDoubleTapTimeout < 0) {
+ Resources resources = getCurrentResources();
+ mDoubleTapTimeout = resources != null
+ ? resources.getInteger(R.integer.config_doubleTapTimeoutMillis)
+ : DOUBLE_TAP_TIMEOUT;
+ }
+ return mDoubleTapTimeout;
+ }
+
+ public int getDoubleTapMinTime() {
+ if (mDoubleTapMinTime < 0) {
+ Resources resources = getCurrentResources();
+ mDoubleTapMinTime = resources != null
+ ? resources.getInteger(R.integer.config_doubleTapMinTimeMillis)
+ : DOUBLE_TAP_MIN_TIME;
+ }
+ return mDoubleTapMinTime;
+ }
+
+ public int getHoverTapTimeout() {
+ if (mHoverTapTimeout < 0) {
+ Resources resources = getCurrentResources();
+ mHoverTapTimeout = resources != null
+ ? resources.getInteger(R.integer.config_hoverTapTimeoutMillis)
+ : HOVER_TAP_TIMEOUT;
+ }
+ return mHoverTapTimeout;
+ }
+
+ public int getHoverTapSlop() {
+ if (mHoverTapSlop < 0) {
+ Resources resources = getCurrentResources();
+ mHoverTapSlop = resources != null
+ ? resources.getDimensionPixelSize(R.dimen.config_hoverTapSlop)
+ : HOVER_TAP_SLOP;
+ }
+ return mHoverTapSlop;
+ }
+
+ public long getZoomControlsTimeout() {
+ if (mZoomControlsTimeout < 0) {
+ Resources resources = getCurrentResources();
+ mZoomControlsTimeout = resources != null
+ ? resources.getInteger(R.integer.config_zoomControlsTimeoutMillis)
+ : ZOOM_CONTROLS_TIMEOUT;
+ }
+ return mZoomControlsTimeout;
+ }
+
+ public float getScrollFriction() {
+ if (mScrollFriction < 0) {
+ Resources resources = getCurrentResources();
+ mScrollFriction = resources != null
+ ? resources.getFloat(R.dimen.config_scrollFriction)
+ : SCROLL_FRICTION;
+ }
+ return mScrollFriction;
+ }
+
+ public long getDefaultActionModeHideDuration() {
+ if (mDefaultActionModeHideDuration < 0) {
+ Resources resources = getCurrentResources();
+ mDefaultActionModeHideDuration = resources != null
+ ? resources.getInteger(R.integer.config_defaultActionModeHideDurationMillis)
+ : ACTION_MODE_HIDE_DURATION_DEFAULT;
+ }
+ return mDefaultActionModeHideDuration;
+ }
+
+ private static Resources getCurrentResources() {
+ if (!android.companion.virtualdevice.flags.Flags
+ .migrateViewconfigurationConstantsToResources()) {
+ return null;
+ }
+ Application application = ActivityThread.currentApplication();
+ Context context = application != null ? application.getApplicationContext() : null;
+ return context != null ? context.getResources() : null;
+ }
+ }
}
diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java
index 949b667f0b7a..7e0818a0a58b 100644
--- a/core/java/android/view/ViewGroup.java
+++ b/core/java/android/view/ViewGroup.java
@@ -2836,7 +2836,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
} else {
predecessor.next = next;
}
- target.recycle();
+ if (!target.isRecycled()) {
+ target.recycle();
+ }
target = next;
continue;
}
@@ -9050,6 +9052,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
return target;
}
+ public boolean isRecycled() {
+ return child == null;
+ }
+
public void recycle() {
if (child == null) {
throw new IllegalStateException("already recycled once");
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 101d5c950b71..edfa1d5aea1f 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/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index 37f393ec6511..49a11cab1de9 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -28,6 +28,14 @@ flag {
}
flag {
+ name: "a11y_is_visited_api"
+ namespace: "accessibility"
+ description: "Adds an API to indicate whether a URL has been visited or not."
+ bug: "391469786"
+ is_exported: true
+}
+
+flag {
name: "a11y_overlay_callbacks"
is_exported: true
namespace: "accessibility"
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index ddbf9e49bb8d..512113692c76 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -29,6 +29,7 @@ import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_NONE;
@@ -405,7 +406,8 @@ public final class TransitionInfo implements Parcelable {
*/
public boolean hasChangesOrSideEffects() {
return !mChanges.isEmpty() || isKeyguardGoingAway()
- || (mFlags & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0;
+ || (mFlags & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0
+ || (mFlags & TRANSIT_FLAG_AOD_APPEARING) != 0;
}
/**
diff --git a/core/java/com/android/internal/content/om/OverlayManagerImpl.java b/core/java/com/android/internal/content/om/OverlayManagerImpl.java
index 5d4e6a083af4..4b3365221bf5 100644
--- a/core/java/com/android/internal/content/om/OverlayManagerImpl.java
+++ b/core/java/com/android/internal/content/om/OverlayManagerImpl.java
@@ -21,7 +21,6 @@ import static android.content.om.OverlayManagerTransaction.Request.BUNDLE_FABRIC
import static android.content.om.OverlayManagerTransaction.Request.TYPE_REGISTER_FABRICATED;
import static android.content.om.OverlayManagerTransaction.Request.TYPE_UNREGISTER_FABRICATED;
-import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
import static com.android.internal.content.om.OverlayConfig.DEFAULT_PRIORITY;
@@ -85,7 +84,6 @@ public class OverlayManagerImpl {
*
* @param context the context to create overlay environment
*/
- @VisibleForTesting(visibility = PACKAGE)
public OverlayManagerImpl(@NonNull Context context) {
mContext = Objects.requireNonNull(context);
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index 7c5335cc753c..9085bbec949f 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -306,8 +306,15 @@ public class Cuj {
/** Track work utility view animation shrinking when scrolling down app list. */
public static final int CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK = 127;
+ /**
+ * Track task transitions
+ *
+ * <p>Tracking starts and ends with the animation.</p>
+ */
+ public static final int CUJ_DEFAULT_TASK_TO_TASK_ANIMATION = 128;
+
// When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
- @VisibleForTesting static final int LAST_CUJ = CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK;
+ @VisibleForTesting static final int LAST_CUJ = CUJ_DEFAULT_TASK_TO_TASK_ANIMATION;
/** @hide */
@IntDef({
@@ -426,7 +433,8 @@ public class Cuj {
CUJ_DESKTOP_MODE_APP_LAUNCH_FROM_ICON,
CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH,
CUJ_LAUNCHER_WORK_UTILITY_VIEW_EXPAND,
- CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK
+ CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK,
+ CUJ_DEFAULT_TASK_TO_TASK_ANIMATION
})
@Retention(RetentionPolicy.SOURCE)
public @interface CujType {}
@@ -556,6 +564,7 @@ public class Cuj {
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DESKTOP_MODE_KEYBOARD_QUICK_SWITCH_APP_LAUNCH;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_WORK_UTILITY_VIEW_EXPAND] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_WORK_UTILITY_VIEW_EXPAND;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_WORK_UTILITY_VIEW_SHRINK;
+ CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_DEFAULT_TASK_TO_TASK_ANIMATION] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__DEFAULT_TASK_TO_TASK_ANIMATION;
}
private Cuj() {
@@ -806,6 +815,8 @@ public class Cuj {
return "LAUNCHER_WORK_UTILITY_VIEW_EXPAND";
case CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK:
return "LAUNCHER_WORK_UTILITY_VIEW_SHRINK";
+ case CUJ_DEFAULT_TASK_TO_TASK_ANIMATION:
+ return "CUJ_DEFAULT_TASK_TO_TASK_ANIMATION";
}
return "UNKNOWN";
}
diff --git a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
index b7e68bacd143..260619ec0b23 100644
--- a/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
+++ b/core/java/com/android/internal/policy/GestureNavigationSettingsObserver.java
@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.os.Handler;
+import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.provider.Settings;
@@ -160,8 +161,13 @@ public class GestureNavigationSettingsObserver extends ContentObserver {
}
public boolean areNavigationButtonForcedVisible() {
- return Settings.Secure.getIntForUser(mContext.getContentResolver(),
- Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) == 0;
+ String SUWTheme = SystemProperties.get("setupwizard.theme", "");
+ boolean isExpressiveThemeEnabled = SUWTheme.equals("glif_expressive")
+ || SUWTheme.equals("glif_expressive_light");
+ // The back gesture is enabled if using the expressive theme
+ return !isExpressiveThemeEnabled
+ && Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) == 0;
}
private float getUnscaledInset(Resources userRes) {
diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp
index 1ca8e2023cb2..a0c8f30c9356 100644
--- a/core/jni/android_util_Binder.cpp
+++ b/core/jni/android_util_Binder.cpp
@@ -13,10 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-#undef ANDROID_UTILS_REF_BASE_DISABLE_IMPLICIT_CONSTRUCTION // TODO:remove this and fix code
-
#define LOG_TAG "JavaBinder"
-//#define LOG_NDEBUG 0
+// #define LOG_NDEBUG 0
#include "android_util_Binder.h"
@@ -479,7 +477,7 @@ public:
if (b) return b;
// b/360067751: constructor may trigger GC, so call outside lock
- b = new JavaBBinder(env, obj);
+ b = sp<JavaBBinder>::make(env, obj);
{
AutoMutex _l(mLock);
@@ -646,11 +644,17 @@ public:
} else {
mObject = env->NewGlobalRef(object);
}
+ }
+
+ void onFirstRef() override {
+ T::onFirstRef();
+
+ sp<RecipientList<T>> list = mList.promote();
// These objects manage their own lifetimes so are responsible for final bookkeeping.
// The list holds a strong reference to this object.
LOG_DEATH_FREEZE("%s Adding JavaRecipient %p to RecipientList %p", logPrefix<T>(), this,
list.get());
- list->add(this);
+ list->add(sp<JavaRecipient>::fromExisting(this));
}
void clearReference() {
@@ -658,7 +662,7 @@ public:
if (list != NULL) {
LOG_DEATH_FREEZE("%s Removing JavaRecipient %p from RecipientList %p", logPrefix<T>(),
this, list.get());
- list->remove(this);
+ list->remove(sp<JavaRecipient>::fromExisting(this));
} else {
LOG_DEATH_FREEZE("%s clearReference() on JavaRecipient %p but RecipientList wp purged",
logPrefix<T>(), this);
@@ -940,7 +944,7 @@ struct BinderProxyNativeData {
// Frozen state change callbacks for mObject. Reference counted only because
// JavaFrozenStateChangeCallback hold a weak reference that can be
// temporarily promoted.
- sp<FrozenStateChangeCallbackList> mFrozenStateChangCallbackList;
+ sp<FrozenStateChangeCallbackList> mFrozenStateChangeCallbackList;
};
BinderProxyNativeData* getBPNativeData(JNIEnv* env, jobject obj) {
@@ -965,8 +969,8 @@ jobject javaObjectForIBinder(JNIEnv* env, const sp<IBinder>& val)
}
BinderProxyNativeData* nativeData = new BinderProxyNativeData();
- nativeData->mOrgue = new DeathRecipientList;
- nativeData->mFrozenStateChangCallbackList = new FrozenStateChangeCallbackList;
+ nativeData->mOrgue = sp<DeathRecipientList>::make();
+ nativeData->mFrozenStateChangeCallbackList = sp<FrozenStateChangeCallbackList>::make();
nativeData->mObject = val;
jobject object = env->CallStaticObjectMethod(gBinderProxyOffsets.mClass,
@@ -1572,8 +1576,8 @@ static void android_os_BinderProxy_linkToDeath(JNIEnv* env, jobject obj,
LOG_DEATH_FREEZE("linkToDeath: binder=%p recipient=%p\n", target, recipient);
if (!target->localBinder()) {
- DeathRecipientList* list = nd->mOrgue.get();
- sp<JavaDeathRecipient> jdr = new JavaDeathRecipient(env, recipient, list);
+ sp<DeathRecipientList> list = nd->mOrgue;
+ sp<JavaDeathRecipient> jdr = sp<JavaDeathRecipient>::make(env, recipient, list);
status_t err = target->linkToDeath(jdr, NULL, flags);
if (err != NO_ERROR) {
// Failure adding the death recipient, so clear its reference
@@ -1649,7 +1653,7 @@ static void android_os_BinderProxy_addFrozenStateChangeCallback(
LOG_DEATH_FREEZE("addFrozenStateChangeCallback: binder=%p callback=%p\n", target, callback);
if (!target->localBinder()) {
- FrozenStateChangeCallbackList* list = nd->mFrozenStateChangCallbackList.get();
+ sp<FrozenStateChangeCallbackList> list = nd->mFrozenStateChangeCallbackList;
auto jfscc = sp<JavaFrozenStateChangeCallback>::make(env, callback, list);
status_t err = target->addFrozenStateChangeCallback(jfscc);
if (err != NO_ERROR) {
@@ -1683,7 +1687,7 @@ static jboolean android_os_BinderProxy_removeFrozenStateChangeCallback(JNIEnv* e
status_t err = NAME_NOT_FOUND;
// If we find the matching callback, proceed to unlink using that
- FrozenStateChangeCallbackList* list = nd->mFrozenStateChangCallbackList.get();
+ FrozenStateChangeCallbackList* list = nd->mFrozenStateChangeCallbackList.get();
sp<JavaRecipient<IBinder::FrozenStateChangeCallback> > origJFSCC = list->find(callback);
LOG_DEATH_FREEZE(" removeFrozenStateChangeCallback found list %p and JFSCC %p", list,
origJFSCC.get());
@@ -1716,7 +1720,7 @@ static void BinderProxy_destroy(void* rawNativeData)
BinderProxyNativeData * nativeData = (BinderProxyNativeData *) rawNativeData;
LOG_DEATH_FREEZE("Destroying BinderProxy: binder=%p drl=%p fsccl=%p\n",
nativeData->mObject.get(), nativeData->mOrgue.get(),
- nativeData->mFrozenStateChangCallbackList.get());
+ nativeData->mFrozenStateChangeCallbackList.get());
delete nativeData;
IPCThreadState::self()->flushCommands();
}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 8db94a420e4c..37e553ea3827 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3061,6 +3061,43 @@
{@link MotionEvent#ACTION_SCROLL} event. -->
<dimen name="config_scrollFactor">64dp</dimen>
+ <!-- Duration in milliseconds of the pressed state in child components. -->
+ <integer name="config_pressedStateDurationMillis">64</integer>
+
+ <!-- Duration in milliseconds we will wait to see if a touch event is a tap or a scroll.
+ If the user does not move within this interval, it is considered to be a tap. -->
+ <integer name="config_tapTimeoutMillis">100</integer>
+
+ <!-- Duration in milliseconds we will wait to see if a touch event is a jump tap.
+ If the user does not move within this interval, it is considered to be a tap. -->
+ <integer name="config_jumpTapTimeoutMillis">500</integer>
+
+ <!-- Duration in milliseconds between the first tap's up event and the second tap's down
+ event for an interaction to be considered a double-tap. -->
+ <integer name="config_doubleTapTimeoutMillis">300</integer>
+
+ <!-- Minimum duration in milliseconds between the first tap's up event and the second tap's
+ down event for an interaction to be considered a double-tap. -->
+ <integer name="config_doubleTapMinTimeMillis">40</integer>
+
+ <!-- Maximum duration in milliseconds between a touch pad touch and release for a given touch
+ to be considered a tap (click) as opposed to a hover movement gesture. -->
+ <integer name="config_hoverTapTimeoutMillis">150</integer>
+
+ <!-- The amount of time in milliseconds that the zoom controls should be displayed on the
+ screen. -->
+ <integer name="config_zoomControlsTimeoutMillis">3000</integer>
+
+ <!-- Default duration in milliseconds for {@link ActionMode#hide(long)}. -->
+ <integer name="config_defaultActionModeHideDurationMillis">2000</integer>
+
+ <!-- Maximum distance in pixels that a touch pad touch can move before being released
+ for it to be considered a tap (click) as opposed to a hover movement gesture. -->
+ <dimen name="config_hoverTapSlop">20px</dimen>
+
+ <!-- The amount of friction applied to scrolls and flings. -->
+ <item name="config_scrollFriction" format="float" type="dimen">0.015</item>
+
<!-- Maximum number of grid columns permitted in the ResolverActivity
used for picking activities to handle an intent. -->
<integer name="config_maxResolverActivityColumns">3</integer>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 8c2ca97af493..a34fbb89c629 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4153,6 +4153,17 @@
<java-symbol type="string" name="config_headlineFontFamily" />
<java-symbol type="string" name="config_headlineFontFamilyMedium" />
+ <java-symbol type="integer" name="config_pressedStateDurationMillis" />
+ <java-symbol type="integer" name="config_tapTimeoutMillis" />
+ <java-symbol type="integer" name="config_jumpTapTimeoutMillis" />
+ <java-symbol type="integer" name="config_doubleTapTimeoutMillis" />
+ <java-symbol type="integer" name="config_doubleTapMinTimeMillis" />
+ <java-symbol type="integer" name="config_hoverTapTimeoutMillis" />
+ <java-symbol type="integer" name="config_zoomControlsTimeoutMillis" />
+ <java-symbol type="integer" name="config_defaultActionModeHideDurationMillis" />
+ <java-symbol type="dimen" name="config_hoverTapSlop" />
+ <java-symbol type="dimen" name="config_scrollFriction" />
+
<java-symbol type="drawable" name="stat_sys_vitals" />
<java-symbol type="color" name="text_color_primary" />
diff --git a/core/tests/coretests/src/android/content/IntentTest.java b/core/tests/coretests/src/android/content/IntentTest.java
index fdfb0c34cdeb..fa1948d9786c 100644
--- a/core/tests/coretests/src/android/content/IntentTest.java
+++ b/core/tests/coretests/src/android/content/IntentTest.java
@@ -23,6 +23,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.os.Binder;
+import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.platform.test.annotations.Presubmit;
@@ -238,4 +239,42 @@ public class IntentTest {
// Not all keys from intent are kept - clip data keys are dropped.
assertFalse(intent.getExtraIntentKeys().containsAll(originalIntentKeys));
}
+
+ @Test
+ public void testSetIntentExtrasClassLoaderEffectiveAfterExtraBundleUnparcel() {
+ Intent intent = new Intent();
+ intent.putExtra("bundle", new Bundle());
+
+ final Parcel parcel = Parcel.obtain();
+ intent.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ final Intent target = new Intent();
+ target.readFromParcel(parcel);
+ target.collectExtraIntentKeys();
+ ClassLoader cl = new ClassLoader() {
+ };
+ target.setExtrasClassLoader(cl);
+ assertThat(target.getBundleExtra("bundle").getClassLoader()).isEqualTo(cl);
+ }
+
+ @Test
+ public void testBundlePutAllClassLoader() {
+ Intent intent = new Intent();
+ Bundle b1 = new Bundle();
+ b1.putBundle("bundle", new Bundle());
+ intent.putExtra("b1", b1);
+ final Parcel parcel = Parcel.obtain();
+ intent.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ final Intent target = new Intent();
+ target.readFromParcel(parcel);
+
+ ClassLoader cl = new ClassLoader() {
+ };
+ target.setExtrasClassLoader(cl);
+ Bundle b2 = new Bundle();
+ b2.putAll(target.getBundleExtra("b1"));
+ assertThat(b2.getBundle("bundle").getClassLoader()).isEqualTo(cl);
+ }
+
}
diff --git a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
index 45d66e8ee3a9..e6361e10cfa7 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceConsumerTest.java
@@ -123,15 +123,21 @@ public class InsetsSourceConsumerTest {
@Test
public void testSetControl_cancelAnimation() {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- final InsetsSourceControl newControl = new InsetsSourceControl(mConsumer.getControl());
+ final int[] cancelTypes = {0};
- // Change the side of the insets hint.
- newControl.setInsetsHint(Insets.of(0, 0, 0, 100));
+ // Change the side of the insets hint from NONE to BOTTOM.
+ final InsetsSourceControl newControl1 = new InsetsSourceControl(mConsumer.getControl());
+ newControl1.setInsetsHint(Insets.of(0, 0, 0, 100));
+ mConsumer.setControl(newControl1, new int[1], new int[1], cancelTypes, new int[1]);
- int[] cancelTypes = {0};
- mConsumer.setControl(newControl, new int[1], new int[1], cancelTypes, new int[1]);
+ assertEquals("The animation must not be cancelled", 0, cancelTypes[0]);
- assertEquals(statusBars(), cancelTypes[0]);
+ // Change the side of the insets hint from BOTTOM to TOP.
+ final InsetsSourceControl newControl2 = new InsetsSourceControl(mConsumer.getControl());
+ newControl2.setInsetsHint(Insets.of(0, 100, 0, 0));
+ mConsumer.setControl(newControl2, new int[1], new int[1], cancelTypes, new int[1]);
+
+ assertEquals("The animation must be cancelled", statusBars(), cancelTypes[0]);
});
}
diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS
index f01e8d665d12..ab2f3ef94eb6 100644
--- a/libs/WindowManager/Shell/OWNERS
+++ b/libs/WindowManager/Shell/OWNERS
@@ -1,7 +1,7 @@
-xutan@google.com
+jorgegil@google.com
pbdr@google.com
pragyabajoria@google.com
# Give submodule owners in shell resource approval
-per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com
+per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com
per-file res*/*/tv_*.xml = bronger@google.com
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index b1fedce5597e..50c08732543a 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -46,17 +46,10 @@
android:contentDescription="@string/app_icon_text"
android:importantForAccessibility="no"/>
- <TextView
+ <com.android.wm.shell.windowdecor.MarqueedTextView
android:id="@+id/application_name"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
tools:text="Gmail"
- android:textColor="@androidprv:color/materialColorOnSurface"
- android:textSize="14sp"
- android:textFontWeight="500"
- android:lineHeight="20dp"
- android:textStyle="normal"
- android:layout_weight="1"/>
+ style="@style/DesktopModeHandleMenuActionButtonTextView"/>
<com.android.wm.shell.windowdecor.HandleMenuImageButton
android:id="@+id/collapse_menu_button"
@@ -133,37 +126,77 @@
android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation"
android:background="@drawable/desktop_mode_decor_handle_menu_background">
- <Button
+ <LinearLayout
android:id="@+id/screenshot_button"
android:contentDescription="@string/screenshot_text"
- android:text="@string/screenshot_text"
- android:drawableStart="@drawable/desktop_mode_ic_handle_menu_screenshot"
- android:drawableTint="@androidprv:color/materialColorOnSurface"
- style="@style/DesktopModeHandleMenuActionButton"/>
+ style="@style/DesktopModeHandleMenuActionButtonLayout">
+
+ <ImageView
+ android:id="@+id/image"
+ android:src="@drawable/desktop_mode_ic_handle_menu_screenshot"
+ android:importantForAccessibility="no"
+ style="@style/DesktopModeHandleMenuActionButtonImage"/>
+
+ <com.android.wm.shell.windowdecor.MarqueedTextView
+ android:id="@+id/label"
+ android:text="@string/screenshot_text"
+ style="@style/DesktopModeHandleMenuActionButtonTextView"/>
- <Button
+ </LinearLayout>
+
+ <LinearLayout
android:id="@+id/new_window_button"
android:contentDescription="@string/new_window_text"
- android:text="@string/new_window_text"
- android:drawableStart="@drawable/desktop_mode_ic_handle_menu_new_window"
- android:drawableTint="@androidprv:color/materialColorOnSurface"
- style="@style/DesktopModeHandleMenuActionButton" />
+ style="@style/DesktopModeHandleMenuActionButtonLayout">
+
+ <ImageView
+ android:id="@+id/image"
+ android:src="@drawable/desktop_mode_ic_handle_menu_new_window"
+ android:importantForAccessibility="no"
+ style="@style/DesktopModeHandleMenuActionButtonImage"/>
- <Button
+ <com.android.wm.shell.windowdecor.MarqueedTextView
+ android:id="@+id/label"
+ android:text="@string/new_window_text"
+ style="@style/DesktopModeHandleMenuActionButtonTextView"/>
+
+ </LinearLayout>
+
+ <LinearLayout
android:id="@+id/manage_windows_button"
android:contentDescription="@string/manage_windows_text"
- android:text="@string/manage_windows_text"
- android:drawableStart="@drawable/desktop_mode_ic_handle_menu_manage_windows"
- android:drawableTint="@androidprv:color/materialColorOnSurface"
- style="@style/DesktopModeHandleMenuActionButton" />
+ style="@style/DesktopModeHandleMenuActionButtonLayout">
+
+ <ImageView
+ android:id="@+id/image"
+ android:src="@drawable/desktop_mode_ic_handle_menu_manage_windows"
+ android:importantForAccessibility="no"
+ style="@style/DesktopModeHandleMenuActionButtonImage"/>
+
+ <com.android.wm.shell.windowdecor.MarqueedTextView
+ android:id="@+id/label"
+ android:text="@string/manage_windows_text"
+ style="@style/DesktopModeHandleMenuActionButtonTextView"/>
- <Button
+ </LinearLayout>
+
+ <LinearLayout
android:id="@+id/change_aspect_ratio_button"
android:contentDescription="@string/change_aspect_ratio_text"
- android:text="@string/change_aspect_ratio_text"
- android:drawableStart="@drawable/desktop_mode_ic_handle_menu_change_aspect_ratio"
- android:drawableTint="@androidprv:color/materialColorOnSurface"
- style="@style/DesktopModeHandleMenuActionButton" />
+ style="@style/DesktopModeHandleMenuActionButtonLayout">
+
+ <ImageView
+ android:id="@+id/image"
+ android:src="@drawable/desktop_mode_ic_handle_menu_change_aspect_ratio"
+ android:importantForAccessibility="no"
+ style="@style/DesktopModeHandleMenuActionButtonImage"/>
+
+ <com.android.wm.shell.windowdecor.MarqueedTextView
+ android:id="@+id/label"
+ android:text="@string/change_aspect_ratio_text"
+ style="@style/DesktopModeHandleMenuActionButtonTextView"/>
+
+ </LinearLayout>
</LinearLayout>
<LinearLayout
@@ -176,22 +209,37 @@
android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation"
android:background="@drawable/desktop_mode_decor_handle_menu_background">
- <Button
+ <LinearLayout
android:id="@+id/open_in_app_or_browser_button"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
android:layout_weight="1"
+ android:layout_marginEnd="8dp"
+ android:gravity="start|center_vertical"
+ android:paddingStart="16dp"
android:contentDescription="@string/open_in_browser_text"
- android:text="@string/open_in_browser_text"
- android:drawableStart="@drawable/desktop_mode_ic_handle_menu_open_in_browser"
- android:drawableTint="@androidprv:color/materialColorOnSurface"
- style="@style/DesktopModeHandleMenuActionButton"/>
+ android:background="?android:selectableItemBackground">
+
+ <ImageView
+ android:id="@+id/image"
+ android:src="@drawable/desktop_mode_ic_handle_menu_open_in_browser"
+ android:importantForAccessibility="no"
+ style="@style/DesktopModeHandleMenuActionButtonImage"/>
+
+ <com.android.wm.shell.windowdecor.MarqueedTextView
+ android:id="@+id/label"
+ android:text="@string/open_in_browser_text"
+ style="@style/DesktopModeHandleMenuActionButtonTextView"/>
+
+ </LinearLayout>
<ImageButton
android:id="@+id/open_by_default_button"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="end|center_vertical"
+ android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
- android:layout_marginStart="10dp"
android:contentDescription="@string/open_by_default_settings_text"
android:src="@drawable/desktop_mode_ic_handle_menu_open_by_default_settings"
android:tint="@androidprv:color/materialColorOnSurface"/>
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index 4ebb7dc6ff37..035004bfd322 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -40,19 +40,34 @@
<item name="android:activityCloseExitAnimation">@anim/forced_resizable_exit</item>
</style>
- <style name="DesktopModeHandleMenuActionButton">
+ <style name="DesktopModeHandleMenuActionButtonLayout">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">52dp</item>
+ <item name="android:layout_weight">1</item>
<item name="android:gravity">start|center_vertical</item>
- <item name="android:paddingStart">16dp</item>
- <item name="android:paddingEnd">0dp</item>
- <item name="android:textSize">14sp</item>
- <item name="android:textFontWeight">500</item>
- <item name="android:textColor">@androidprv:color/materialColorOnSurface</item>
- <item name="android:drawablePadding">16dp</item>
+ <item name="android:paddingHorizontal">16dp</item>
<item name="android:background">?android:selectableItemBackground</item>
</style>
+ <style name="DesktopModeHandleMenuActionButtonImage">
+ <item name="android:layout_width">20dp</item>
+ <item name="android:layout_height">20dp</item>
+ <item name="android:layout_marginEnd">16dp</item>
+ </style>
+
+ <style name="DesktopModeHandleMenuActionButtonTextView">
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:lineHeight">20sp</item>
+ <item name="android:textFontWeight">500</item>
+ <item name="android:textColor">@androidprv:color/materialColorOnSurface</item>
+ <item name="android:ellipsize">marquee</item>
+ <item name="android:scrollHorizontally">true</item>
+ <item name="android:singleLine">true</item>
+ </style>
+
<style name="DesktopModeHandleMenuWindowingButton">
<item name="android:layout_width">48dp</item>
<item name="android:layout_height">48dp</item>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java
index 4127adc1f901..12938db07ece 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java
@@ -24,4 +24,9 @@ public class DragAndDropConstants {
* ignore drag events.
*/
public static final String EXTRA_DISALLOW_HIT_REGION = "DISALLOW_HIT_REGION";
+
+ /**
+ * An Intent extra that Launcher can use to specify the {@link android.content.pm.ShortcutInfo}
+ */
+ public static final String EXTRA_SHORTCUT_INFO = "EXTRA_SHORTCUT_INFO";
}
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 ddcdf9f8c617..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
@@ -54,6 +54,7 @@ import com.android.launcher3.icons.BubbleIconFactory;
import com.android.wm.shell.Flags;
import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
+import com.android.wm.shell.common.ComponentUtils;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.bubbles.BubbleInfo;
@@ -282,6 +283,29 @@ public class Bubble implements BubbleViewProvider {
mPackageName = intent.getPackage();
}
+ private Bubble(
+ PendingIntent intent,
+ UserHandle user,
+ String key,
+ @ShellMainThread Executor mainExecutor,
+ @ShellBackgroundThread Executor bgExecutor) {
+ mGroupKey = null;
+ mLocusId = null;
+ mFlags = 0;
+ mUser = user;
+ mIcon = null;
+ mType = BubbleType.TYPE_APP;
+ mKey = key;
+ mShowBubbleUpdateDot = false;
+ mMainExecutor = mainExecutor;
+ mBgExecutor = bgExecutor;
+ mTaskId = INVALID_TASK_ID;
+ mPendingIntent = intent;
+ mIntent = null;
+ mDesiredHeight = Integer.MAX_VALUE;
+ mPackageName = ComponentUtils.getPackageName(intent);
+ }
+
private Bubble(ShortcutInfo info, @ShellMainThread Executor mainExecutor,
@ShellBackgroundThread Executor bgExecutor) {
mGroupKey = null;
@@ -336,6 +360,15 @@ public class Bubble implements BubbleViewProvider {
}
/** Creates an app bubble. */
+ public static Bubble createAppBubble(PendingIntent intent, UserHandle user,
+ @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
+ return new Bubble(intent,
+ user,
+ /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user),
+ mainExecutor, bgExecutor);
+ }
+
+ /** Creates an app bubble. */
public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon,
@ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
return new Bubble(intent,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index b93b7b86e661..8cf2370df48d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -46,6 +46,7 @@ import android.app.NotificationChannel;
import android.app.PendingIntent;
import android.app.TaskInfo;
import android.content.BroadcastReceiver;
+import android.content.ClipDescription;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -118,6 +119,7 @@ import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.bubbles.BubbleBarUpdate;
import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider;
import com.android.wm.shell.shared.bubbles.DeviceConfig;
+import com.android.wm.shell.shared.draganddrop.DragAndDropConstants;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
@@ -872,11 +874,19 @@ public class BubbleController implements ConfigurationChangeListener,
}
@Override
- public void onItemDroppedOverBubbleBarDragZone(BubbleBarLocation location, Intent appIntent,
- UserHandle userHandle) {
- if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
- hideBubbleBarExpandedViewDropTarget();
- expandStackAndSelectBubble(appIntent, userHandle, location);
+ public void onItemDroppedOverBubbleBarDragZone(BubbleBarLocation location, Intent itemIntent) {
+ hideBubbleBarExpandedViewDropTarget();
+ ShortcutInfo shortcutInfo = (ShortcutInfo) itemIntent
+ .getExtra(DragAndDropConstants.EXTRA_SHORTCUT_INFO);
+ if (shortcutInfo != null) {
+ expandStackAndSelectBubble(shortcutInfo, location);
+ return;
+ }
+ UserHandle user = (UserHandle) itemIntent.getExtra(Intent.EXTRA_USER);
+ PendingIntent pendingIntent = (PendingIntent) itemIntent
+ .getExtra(ClipDescription.EXTRA_PENDING_INTENT);
+ if (pendingIntent != null && user != null) {
+ expandStackAndSelectBubble(pendingIntent, user, location);
}
}
@@ -1506,9 +1516,16 @@ public class BubbleController implements ConfigurationChangeListener,
* Expands and selects a bubble created or found via the provided shortcut info.
*
* @param info the shortcut info for the bubble.
+ * @param bubbleBarLocation optional location in case bubble bar should be repositioned.
*/
- public void expandStackAndSelectBubble(ShortcutInfo info) {
+ public void expandStackAndSelectBubble(ShortcutInfo info,
+ @Nullable BubbleBarLocation bubbleBarLocation) {
if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return;
+ if (bubbleBarLocation != null) {
+ //TODO (b/388894910) combine location update with the setSelectedBubbleAndExpandStack &
+ // fix bubble bar flicking
+ setBubbleBarLocation(bubbleBarLocation, BubbleBarLocation.UpdateSource.APP_ICON_DRAG);
+ }
Bubble b = mBubbleData.getOrCreateBubble(info); // Removes from overflow
ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - shortcut=%s", info);
if (b.isInflated()) {
@@ -1524,7 +1541,25 @@ public class BubbleController implements ConfigurationChangeListener,
*
* @param intent the intent for the bubble.
*/
- public void expandStackAndSelectBubble(Intent intent, UserHandle user,
+ public void expandStackAndSelectBubble(Intent intent, UserHandle user) {
+ if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return;
+ Bubble b = mBubbleData.getOrCreateBubble(intent, user); // Removes from overflow
+ ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent);
+ if (b.isInflated()) {
+ mBubbleData.setSelectedBubbleAndExpandStack(b);
+ } else {
+ b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
+ inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
+ }
+ }
+
+ /**
+ * Expands and selects a bubble created or found for this app.
+ *
+ * @param pendingIntent the intent for the bubble.
+ * @param bubbleBarLocation optional location in case bubble bar should be repositioned.
+ */
+ public void expandStackAndSelectBubble(PendingIntent pendingIntent, UserHandle user,
@Nullable BubbleBarLocation bubbleBarLocation) {
if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return;
if (bubbleBarLocation != null) {
@@ -1532,8 +1567,9 @@ public class BubbleController implements ConfigurationChangeListener,
// fix bubble bar flicking
setBubbleBarLocation(bubbleBarLocation, BubbleBarLocation.UpdateSource.APP_ICON_DRAG);
}
- Bubble b = mBubbleData.getOrCreateBubble(intent, user); // Removes from overflow
- ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent);
+ Bubble b = mBubbleData.getOrCreateBubble(pendingIntent, user);
+ ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - pendingIntent=%s",
+ pendingIntent);
if (b.isInflated()) {
mBubbleData.setSelectedBubbleAndExpandStack(b);
} else {
@@ -2756,13 +2792,13 @@ public class BubbleController implements ConfigurationChangeListener,
@Override
public void showShortcutBubble(ShortcutInfo info) {
- mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(info));
+ mMainExecutor.execute(() -> mController
+ .expandStackAndSelectBubble(info, /* bubbleBarLocation = */ null));
}
@Override
public void showAppBubble(Intent intent, UserHandle user) {
- mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent,
- user, /* bubbleBarLocation = */ null));
+ mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent, user));
}
@Override
@@ -2983,9 +3019,10 @@ public class BubbleController implements ConfigurationChangeListener,
@Override
public void expandStackAndSelectBubble(ShortcutInfo info) {
- mMainExecutor.execute(() -> {
- BubbleController.this.expandStackAndSelectBubble(info);
- });
+ mMainExecutor.execute(() ->
+ BubbleController.this
+ .expandStackAndSelectBubble(info, /* bubbleBarLocation = */ null)
+ );
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index 96d0f6d5654e..f97133a4c3d1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -471,6 +471,16 @@ public class BubbleData {
return bubbleToReturn;
}
+ Bubble getOrCreateBubble(PendingIntent pendingIntent, UserHandle user) {
+ String bubbleKey = Bubble.getAppBubbleKeyForApp(pendingIntent.getCreatorPackage(), user);
+ Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey);
+ if (bubbleToReturn == null) {
+ bubbleToReturn = Bubble.createAppBubble(pendingIntent, user, mMainExecutor,
+ mBgExecutor);
+ }
+ return bubbleToReturn;
+ }
+
Bubble getOrCreateBubble(TaskInfo taskInfo) {
UserHandle user = UserHandle.of(mCurrentUserId);
String bubbleKey = Bubble.getAppBubbleKeyForTask(taskInfo);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 4a0eee861d21..e47ac61a53dd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -117,15 +117,24 @@ public class BubbleTaskViewHelper {
Context context =
mContext.createContextAsUser(
mBubble.getUser(), Context.CONTEXT_RESTRICTED);
- PendingIntent pi = PendingIntent.getActivity(
- context,
- /* requestCode= */ 0,
- mBubble.getIntent()
- .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
- PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT,
- /* options= */ null);
- mTaskView.startActivity(pi, /* fillInIntent= */ null, options,
- launchBounds);
+ Intent fillInIntent = null;
+ //first try get pending intent from the bubble
+ PendingIntent pi = mBubble.getPendingIntent();
+ if (pi == null) {
+ // if null - create new one
+ pi = PendingIntent.getActivity(
+ context,
+ /* requestCode= */ 0,
+ mBubble.getIntent()
+ .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK),
+ PendingIntent.FLAG_IMMUTABLE
+ | PendingIntent.FLAG_UPDATE_CURRENT,
+ /* options= */ null);
+ } else {
+ fillInIntent = new Intent(pi.getIntent());
+ fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+ mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
} else if (isShortcutBubble) {
options.setLaunchedFromBubble(true);
options.setApplyActivityFlagsForBubbles(true);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt
index afe5c87604d9..3ff80b5ab8ac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt
@@ -18,7 +18,6 @@ package com.android.wm.shell.bubbles.bar
import android.content.Intent
import android.graphics.Rect
-import android.os.UserHandle
import com.android.wm.shell.shared.bubbles.BubbleBarLocation
/** Controller that takes care of the bubble bar drag events. */
@@ -31,11 +30,7 @@ interface BubbleBarDragListener {
fun onItemDraggedOutsideBubbleBarDropZone()
/** Called when the drop event happens over the bubble bar drop zone. */
- fun onItemDroppedOverBubbleBarDragZone(
- location: BubbleBarLocation,
- intent: Intent,
- userHandle: UserHandle
- )
+ fun onItemDroppedOverBubbleBarDragZone(location: BubbleBarLocation, itemIntent: Intent)
/**
* Returns mapping of the bubble bar locations to the corresponding
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 78c6cf377d8e..6798a88a6da7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -180,6 +180,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
// Ideally this would be package private, but we have to set this in a fake for test and we
// don't yet have dagger set up for tests, so have to set manually
+ @VisibleForTesting
@Inject
public BubbleLogger bubbleLogger;
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 3c7780711a14..f48b34d0d646 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
@@ -3074,6 +3074,7 @@ class DesktopTasksController(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
pendingIntentLaunchFlags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+ splashScreenStyle = SPLASH_SCREEN_STYLE_ICON
}
if (windowingMode == WINDOWING_MODE_FULLSCREEN) {
dragAndDropFullscreenCookie = Binder()
@@ -3082,7 +3083,12 @@ class DesktopTasksController(
val wct = WindowContainerTransaction()
wct.sendPendingIntent(launchIntent, null, opts.toBundle())
if (windowingMode == WINDOWING_MODE_FREEFORM) {
- desktopModeDragAndDropTransitionHandler.handleDropEvent(wct)
+ if (DesktopModeFlags.ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX.isTrue()) {
+ // TODO b/376389593: Use a custom tab tearing transition/animation
+ startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null)
+ } else {
+ desktopModeDragAndDropTransitionHandler.handleDropEvent(wct)
+ }
} else {
transitions.startTransition(TRANSIT_OPEN, wct, null)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index 2571e0e36cd9..b3c1a92f5e1d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -47,7 +47,6 @@ import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
-import android.os.UserHandle;
import android.view.DragEvent;
import android.view.SurfaceControl;
import android.view.View;
@@ -70,6 +69,7 @@ import com.android.wm.shell.bubbles.bar.BubbleBarDragListener;
import com.android.wm.shell.common.split.SplitScreenUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.shared.animation.Interpolators;
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -627,8 +627,7 @@ public class DragLayout extends LinearLayout
@Nullable
private BubbleBarLocation getBubbleBarLocation(int x, int y) {
Intent appData = mSession.appData;
- if (appData == null || appData.getExtra(Intent.EXTRA_INTENT) == null
- || appData.getExtra(Intent.EXTRA_USER) == null) {
+ if (appData == null) {
// there is no app data, so drop event over the bubble bar can not be handled
return null;
}
@@ -686,11 +685,10 @@ public class DragLayout extends LinearLayout
// Process the drop exclusive by DropTarget OR by the BubbleBar
if (mCurrentTarget != null) {
mPolicy.onDropped(mCurrentTarget, hideTaskToken);
- } else if (appData != null && mCurrentBubbleBarTarget != null) {
- Intent appIntent = (Intent) appData.getExtra(Intent.EXTRA_INTENT);
- UserHandle user = (UserHandle) appData.getExtra(Intent.EXTRA_USER);
+ } else if (appData != null && mCurrentBubbleBarTarget != null
+ && BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
mBubbleBarDragListener.onItemDroppedOverBubbleBarDragZone(mCurrentBubbleBarTarget,
- appIntent, user);
+ appData);
}
// Start animating the drop UI out with the drag surface
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
index d666126b91ba..c0a0f469add4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
@@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.service.dreams.Flags.dismissDreamOnKeyguardDismiss;
import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS;
+import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
@@ -200,7 +201,8 @@ public class KeyguardTransitionHandler
transition, info, startTransaction, finishTransaction, finishCallback);
}
- if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0) {
+ if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0
+ || (info.getFlags() & TRANSIT_FLAG_AOD_APPEARING) != 0) {
return startAnimation(mAppearTransition, "appearing",
transition, info, startTransaction, finishTransaction, finishCallback);
}
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 0689205a1110..a5a5fd73d5c0 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
@@ -39,9 +39,12 @@ import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED;
import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
+import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_RELAUNCH;
import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL;
import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL;
import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
@@ -55,6 +58,7 @@ import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
+import static com.android.internal.jank.Cuj.CUJ_DEFAULT_TASK_TO_TASK_ANIMATION;
import static com.android.internal.policy.TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
@@ -101,6 +105,7 @@ import android.window.WindowContainerTransaction;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.ProtoLog;
@@ -144,6 +149,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
private Drawable mEnterpriseThumbnailDrawable;
+ static final InteractionJankMonitor sInteractionJankMonitor =
+ InteractionJankMonitor.getInstance();
+
private BroadcastReceiver mEnterpriseResourceUpdatedReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -321,8 +329,17 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
final ArrayList<Animator> animations = new ArrayList<>();
mAnimations.put(transition, animations);
+ final boolean isTaskTransition = isTaskTransition(info);
+ if (isTaskTransition) {
+ sInteractionJankMonitor.begin(info.getRoot(0).getLeash(), mContext,
+ mMainHandler, CUJ_DEFAULT_TASK_TO_TASK_ANIMATION);
+ }
+
final Runnable onAnimFinish = () -> {
if (!animations.isEmpty()) return;
+ if (isTaskTransition) {
+ sInteractionJankMonitor.end(CUJ_DEFAULT_TASK_TO_TASK_ANIMATION);
+ }
mAnimations.remove(transition);
finishCallback.onTransitionFinished(null /* wct */);
};
@@ -678,6 +695,30 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
}
/**
+ * A task transition is defined as a transition where there is exaclty one open/to_front task
+ * and one close/to_back task. Nothing else is allowed to be included in the transition
+ */
+ public static boolean isTaskTransition(@NonNull TransitionInfo info) {
+ if (info.getChanges().size() != 2) {
+ return false;
+ }
+ boolean hasOpeningTask = false;
+ boolean hasClosingTask = false;
+
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change.getTaskInfo() == null) {
+ // A non-task is in the transition
+ return false;
+ }
+ int mode = change.getMode();
+ hasOpeningTask |= mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT;
+ hasClosingTask |= mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK;
+ }
+ return hasOpeningTask && hasClosingTask;
+ }
+
+ /**
* Does `info` only contain translucent visibility changes (CHANGEs are ignored). We select
* different animations and z-orders for these
*/
@@ -986,4 +1027,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
|| animType == ANIM_CLIP_REVEAL || animType == ANIM_OPEN_CROSS_PROFILE_APPS
|| animType == ANIM_FROM_STYLE;
}
+
+ @Override
+ public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
+ @Nullable SurfaceControl.Transaction finishTransaction) {
+ sInteractionJankMonitor.cancel(CUJ_DEFAULT_TASK_TO_TASK_ANIMATION);
+ }
}
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 1d9564948772..c92e67f1a0c0 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
@@ -473,7 +473,7 @@ class HandleMenu(
@VisibleForTesting
val appIconView = appInfoPill.requireViewById<ImageView>(R.id.application_icon)
@VisibleForTesting
- val appNameView = appInfoPill.requireViewById<TextView>(R.id.application_name)
+ val appNameView = appInfoPill.requireViewById<MarqueedTextView>(R.id.application_name)
// Windowing Pill.
private val windowingPill = rootView.requireViewById<View>(R.id.windowing_pill)
@@ -486,17 +486,17 @@ class HandleMenu(
// More Actions Pill.
private val moreActionsPill = rootView.requireViewById<View>(R.id.more_actions_pill)
- private val screenshotBtn = moreActionsPill.requireViewById<Button>(R.id.screenshot_button)
- private val newWindowBtn = moreActionsPill.requireViewById<Button>(R.id.new_window_button)
+ private val screenshotBtn = moreActionsPill.requireViewById<View>(R.id.screenshot_button)
+ private val newWindowBtn = moreActionsPill.requireViewById<View>(R.id.new_window_button)
private val manageWindowBtn = moreActionsPill
- .requireViewById<Button>(R.id.manage_windows_button)
+ .requireViewById<View>(R.id.manage_windows_button)
private val changeAspectRatioBtn = moreActionsPill
- .requireViewById<Button>(R.id.change_aspect_ratio_button)
+ .requireViewById<View>(R.id.change_aspect_ratio_button)
// Open in Browser/App Pill.
private val openInAppOrBrowserPill = rootView.requireViewById<View>(
R.id.open_in_app_or_browser_pill)
- private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<Button>(
+ private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<View>(
R.id.open_in_app_or_browser_button)
private val openByDefaultBtn = openInAppOrBrowserPill.requireViewById<ImageButton>(
R.id.open_by_default_button)
@@ -658,6 +658,7 @@ class HandleMenu(
this.taskInfo = this@HandleMenuView.taskInfo
}
appNameView.setTextColor(style.textColor)
+ appNameView.startMarquee()
}
private fun bindWindowingPill(style: MenuStyle) {
@@ -693,11 +694,15 @@ class HandleMenu(
).forEach {
val button = it.first
val shouldShow = it.second
- button.apply {
- isGone = !shouldShow
+ val label = button.requireViewById<MarqueedTextView>(R.id.label)
+ val image = button.requireViewById<ImageView>(R.id.image)
+
+ button.isGone = !shouldShow
+ label.apply {
setTextColor(style.textColor)
- compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
+ startMarquee()
}
+ image.imageTintList = ColorStateList.valueOf(style.textColor)
}
}
@@ -712,12 +717,17 @@ class HandleMenu(
} else {
getString(R.string.open_in_browser_text)
}
- openInAppOrBrowserBtn.apply {
+
+ val label = openInAppOrBrowserBtn.requireViewById<MarqueedTextView>(R.id.label)
+ val image = openInAppOrBrowserBtn.requireViewById<ImageView>(R.id.image)
+ openInAppOrBrowserBtn.contentDescription = btnText
+ label.apply {
text = btnText
- contentDescription = btnText
setTextColor(style.textColor)
- compoundDrawableTintList = ColorStateList.valueOf(style.textColor)
+ startMarquee()
}
+ image.imageTintList = ColorStateList.valueOf(style.textColor)
+
openByDefaultBtn.isGone = isBrowserApp
openByDefaultBtn.imageTintList = ColorStateList.valueOf(style.textColor)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
index 470e5a1d88b4..75f90bb9c38e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt
@@ -327,7 +327,7 @@ class HandleMenuAnimator(
}
// Open in Browser Button Opacity Animation
- val button = openInAppOrBrowserPill.requireViewById<Button>(R.id.open_in_app_or_browser_button)
+ val button = openInAppOrBrowserPill.requireViewById<View>(R.id.open_in_app_or_browser_button)
animators +=
ObjectAnimator.ofFloat(button, ALPHA, 1f).apply {
startDelay = BODY_ALPHA_OPEN_DELAY
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt
new file mode 100644
index 000000000000..733b6221ac0e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.windowdecor
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.TextView
+
+/** A custom [TextView] that allows better control over marquee animation used to ellipsize text. */
+class MarqueedTextView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = android.R.attr.textViewStyle
+) : TextView(context, attrs, defStyleAttr) {
+
+ /**
+ * Starts marquee animation if the layout attributes for this object include
+ * `android:ellipsize=marquee`, `android:singleLine=true`, and
+ * `android:scrollHorizontally=true`.
+ */
+ override public fun startMarquee() {
+ super.startMarquee()
+ }
+
+ /**
+ * Must always return [true] since [TextView.startMarquee()] requires view to be selected or
+ * focused in order to start the marquee animation.
+ *
+ * We are not using [TextView.setSelected()] as this would dispatch undesired accessibility
+ * events.
+ */
+ override fun isSelected() : Boolean {
+ return true
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS
index 3f828f547920..992402528f4f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS
@@ -1,3 +1,2 @@
-jorgegil@google.com
mattsziklay@google.com
mdehaini@google.com
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 bc2be901d320..4762bc21d79c 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
@@ -93,6 +93,9 @@ class AppHeaderViewHolder(
private val lightColors = dynamicLightColorScheme(context)
private val darkColors = dynamicDarkColorScheme(context)
+ private val headerButtonOpenMenuA11yText = context.resources
+ .getString(R.string.desktop_mode_app_header_chip_text)
+
/**
* The corner radius to apply to the app chip, maximize and close button's background drawable.
**/
@@ -228,6 +231,18 @@ class AppHeaderViewHolder(
}
}
+ val a11yActionOpenHeaderMenu = AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK,
+ headerButtonOpenMenuA11yText)
+ openMenuButton.accessibilityDelegate = object : View.AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfo
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ info.addAction(a11yActionOpenHeaderMenu)
+ }
+ }
+
with(context.resources) {
// Update a11y read out to say "double tap to maximize or restore window size"
ViewCompat.replaceAccessibilityAction(
@@ -260,6 +275,7 @@ class AppHeaderViewHolder(
/** Sets the app's name in the header. */
fun setAppName(name: CharSequence) {
appNameTextView.text = name
+ openMenuButton.contentDescription = name
}
/** Sets the app's icon in the header. */
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt
index 33e8d78d6a15..7b7d96c4294c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt
@@ -19,6 +19,7 @@ package com.android.wm.shell.common
import android.testing.AndroidTestingRunner
import android.view.IWindowSession
import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
import org.junit.Test
import org.junit.runner.RunWith
@@ -30,10 +31,10 @@ import org.junit.runner.RunWith
*/
@RunWith(AndroidTestingRunner::class)
@SmallTest
-class WindowSessionSupplierTest {
+class WindowSessionSupplierTest : ShellTestCase() {
@Test
- fun `InputChannelSupplier supplies an InputChannel`() {
+ fun `WindowSessionSupplierTest supplies an IWindowSession`() {
val supplier = WindowSessionSupplier()
SuppliersUtilsTest.assertSupplierProvidesValue(supplier) {
it is IWindowSession
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 edb9b2d2fede..cd1c16a93475 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
@@ -5427,38 +5427,90 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun onUnhandledDrag_newFreeformIntent() {
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFreeformIntent_tabTearingAnimationBugfixFlagEnabled() {
testOnUnhandledDrag(
DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR,
PointF(1200f, 700f),
Rect(240, 700, 2160, 1900),
+ tabTearingAnimationFlagEnabled = true,
)
}
@Test
- fun onUnhandledDrag_newFreeformIntentSplitLeft() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFreeformIntent_tabTearingAnimationBugfixFlagDisabled() {
+ testOnUnhandledDrag(
+ DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR,
+ PointF(1200f, 700f),
+ Rect(240, 700, 2160, 1900),
+ tabTearingAnimationFlagEnabled = false,
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFreeformIntentSplitLeft_tabTearingAnimationBugfixFlagEnabled() {
+ testOnUnhandledDrag(
+ DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR,
+ PointF(50f, 700f),
+ Rect(0, 0, 500, 1000),
+ tabTearingAnimationFlagEnabled = true,
+ )
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFreeformIntentSplitLeft_tabTearingAnimationBugfixFlagDisabled() {
testOnUnhandledDrag(
DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR,
PointF(50f, 700f),
Rect(0, 0, 500, 1000),
+ tabTearingAnimationFlagEnabled = false,
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFreeformIntentSplitRight_tabTearingAnimationBugfixFlagEnabled() {
+ testOnUnhandledDrag(
+ DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR,
+ PointF(2500f, 700f),
+ Rect(500, 0, 1000, 1000),
+ tabTearingAnimationFlagEnabled = true,
)
}
@Test
- fun onUnhandledDrag_newFreeformIntentSplitRight() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFreeformIntentSplitRight_tabTearingAnimationBugfixFlagDisabled() {
testOnUnhandledDrag(
DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR,
PointF(2500f, 700f),
Rect(500, 0, 1000, 1000),
+ tabTearingAnimationFlagEnabled = false,
)
}
@Test
- fun onUnhandledDrag_newFullscreenIntent() {
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFullscreenIntent_tabTearingAnimationBugfixFlagEnabled() {
testOnUnhandledDrag(
DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR,
PointF(1200f, 50f),
Rect(),
+ tabTearingAnimationFlagEnabled = true,
+ )
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX)
+ fun onUnhandledDrag_newFullscreenIntent_tabTearingAnimationBugfixFlagDisabled() {
+ testOnUnhandledDrag(
+ DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR,
+ PointF(1200f, 50f),
+ Rect(),
+ tabTearingAnimationFlagEnabled = false,
)
}
@@ -5812,6 +5864,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
indicatorType: DesktopModeVisualIndicator.IndicatorType,
inputCoordinate: PointF,
expectedBounds: Rect,
+ tabTearingAnimationFlagEnabled: Boolean,
) {
setUpLandscapeDisplay()
val task = setUpFreeformTask()
@@ -5842,6 +5895,16 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
anyOrNull(),
eq(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT),
)
+ whenever(
+ desktopMixedTransitionHandler.startLaunchTransition(
+ eq(TRANSIT_OPEN),
+ any(),
+ anyOrNull(),
+ anyOrNull(),
+ anyOrNull(),
+ )
+ )
+ .thenReturn(Binder())
spyController.onUnhandledDrag(
mockPendingIntent,
@@ -5858,8 +5921,19 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
verify(transitions).startTransition(any(), capture(arg), anyOrNull())
} else {
expectedWindowingMode = WINDOWING_MODE_FREEFORM
- // All other launches use a special handler.
- verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg))
+ if (tabTearingAnimationFlagEnabled) {
+ verify(desktopMixedTransitionHandler)
+ .startLaunchTransition(
+ eq(TRANSIT_OPEN),
+ capture(arg),
+ anyOrNull(),
+ anyOrNull(),
+ anyOrNull(),
+ )
+ } else {
+ // All other launches use a special handler.
+ verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg))
+ }
}
assertThat(
ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions)
diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp
index 3ecd82b074a1..095be57a5dc8 100644
--- a/libs/androidfw/Idmap.cpp
+++ b/libs/androidfw/Idmap.cpp
@@ -55,6 +55,13 @@ struct Idmap_header {
// without having to read/store each header entry separately.
};
+struct Idmap_constraint {
+ // Constraint type can be TYPE_DISPLAY_ID or TYP_DEVICE_ID, please refer
+ // to ConstraintType in OverlayConstraint.java
+ uint32_t constraint_type;
+ uint32_t constraint_value;
+};
+
struct Idmap_data_header {
uint32_t target_entry_count;
uint32_t target_inline_entry_count;
@@ -254,13 +261,18 @@ std::optional<std::string_view> ReadString(const uint8_t** in_out_data_ptr, size
#endif
LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* header,
- const Idmap_data_header* data_header, Idmap_target_entries target_entries,
+ const Idmap_constraint* constraints,
+ uint32_t constraints_count,
+ const Idmap_data_header* data_header,
+ Idmap_target_entries target_entries,
Idmap_target_inline_entries target_inline_entries,
const Idmap_target_entry_inline_value* inline_entry_values,
const ConfigDescription* configs, Idmap_overlay_entries overlay_entries,
std::unique_ptr<ResStringPool>&& string_pool,
std::string_view overlay_apk_path, std::string_view target_apk_path)
: header_(header),
+ constraints_(constraints),
+ constraints_count_(constraints_count),
data_header_(data_header),
target_entries_(target_entries),
target_inline_entries_(target_inline_entries),
@@ -298,9 +310,9 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie
return {};
}
std::optional<std::string_view> target_path = ReadString(&data_ptr, &data_size, "target path");
- if (!target_path) {
- return {};
- }
+ if (!target_path) {
+ return {};
+ }
std::optional<std::string_view> overlay_path = ReadString(&data_ptr, &data_size, "overlay path");
if (!overlay_path) {
return {};
@@ -310,6 +322,17 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie
return {};
}
+ auto constraints_count = ReadType<uint32_t>(&data_ptr, &data_size, "constraints count");
+ if (!constraints_count) {
+ return {};
+ }
+ auto constraints = *constraints_count > 0 ?
+ ReadType<Idmap_constraint>(&data_ptr, &data_size, "constraints", *constraints_count)
+ : nullptr;
+ if (*constraints_count > 0 && !constraints) {
+ return {};
+ }
+
// Parse the idmap data blocks. Currently idmap2 can only generate one data block.
auto data_header = ReadType<Idmap_data_header>(&data_ptr, &data_size, "data header");
if (data_header == nullptr) {
@@ -376,9 +399,10 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie
// Can't use make_unique because LoadedIdmap constructor is private.
return std::unique_ptr<LoadedIdmap>(
- new LoadedIdmap(std::string(idmap_path), header, data_header, target_entries,
- target_inline_entries, target_inline_entry_values, configurations,
- overlay_entries, std::move(idmap_string_pool), *overlay_path, *target_path));
+ new LoadedIdmap(std::string(idmap_path), header, constraints, *constraints_count,
+ data_header, target_entries, target_inline_entries,
+ target_inline_entry_values,configurations, overlay_entries,
+ std::move(idmap_string_pool),*overlay_path, *target_path));
}
bool LoadedIdmap::IsUpToDate() const {
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index de9991a8be5e..978bc768cd3d 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -290,11 +290,11 @@ static bool assertIdmapHeader(const void* idmap, size_t size) {
}
const uint32_t version = htodl(*(reinterpret_cast<const uint32_t*>(idmap) + 1));
- if (version != ResTable::IDMAP_CURRENT_VERSION) {
+ if (version != kIdmapCurrentVersion) {
// We are strict about versions because files with this format are
// auto-generated and don't need backwards compatibility.
ALOGW("idmap: version mismatch in header (is 0x%08x, expected 0x%08x)",
- version, ResTable::IDMAP_CURRENT_VERSION);
+ version, kIdmapCurrentVersion);
return false;
}
return true;
@@ -400,14 +400,18 @@ status_t parseIdmap(const void* idmap, size_t size, uint8_t* outPackageId, Keyed
return UNKNOWN_ERROR;
}
- size -= ResTable::IDMAP_HEADER_SIZE_BYTES;
+ size_t sizeOfHeaderAndConstraints = ResTable::IDMAP_HEADER_SIZE_BYTES +
+ // This accounts for zero constraints, and hence takes only 4 bytes for
+ // the constraints count.
+ ResTable::IDMAP_CONSTRAINTS_COUNT_SIZE_BYTES;
+ size -= sizeOfHeaderAndConstraints;
if (size < sizeof(uint16_t) * 2) {
ALOGE("idmap: too small to contain any mapping");
return UNKNOWN_ERROR;
}
const uint16_t* data = reinterpret_cast<const uint16_t*>(
- reinterpret_cast<const uint8_t*>(idmap) + ResTable::IDMAP_HEADER_SIZE_BYTES);
+ reinterpret_cast<const uint8_t*>(idmap) + sizeOfHeaderAndConstraints);
uint16_t targetPackageId = dtohs(*(data++));
if (targetPackageId == 0 || targetPackageId > 255) {
@@ -7492,7 +7496,7 @@ status_t ResTable::createIdmap(const ResTable& targetResTable,
// write idmap header
uint32_t* data = reinterpret_cast<uint32_t*>(*outData);
*data++ = htodl(IDMAP_MAGIC); // write: magic
- *data++ = htodl(ResTable::IDMAP_CURRENT_VERSION); // write: version
+ *data++ = htodl(kIdmapCurrentVersion); // write: version
*data++ = htodl(targetCrc); // write: target crc
*data++ = htodl(overlayCrc); // write: overlay crc
@@ -7507,6 +7511,9 @@ status_t ResTable::createIdmap(const ResTable& targetResTable,
}
data += (2 * 256) / sizeof(uint32_t);
+ // write zero constraints count (no constraints)
+ *data++ = htodl(0);
+
// write idmap data header
uint16_t* typeData = reinterpret_cast<uint16_t*>(data);
*typeData++ = htods(targetPackageStruct->id); // write: target package id
diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h
index ac75eb3bb98c..d1db13f53069 100644
--- a/libs/androidfw/include/androidfw/Idmap.h
+++ b/libs/androidfw/include/androidfw/Idmap.h
@@ -35,6 +35,7 @@ namespace android {
class LoadedIdmap;
class IdmapResMap;
struct Idmap_header;
+struct Idmap_constraint;
struct Idmap_data_header;
struct Idmap_target_entry;
struct Idmap_target_entry_inline;
@@ -203,6 +204,8 @@ class LoadedIdmap {
LoadedIdmap() = default;
const Idmap_header* header_;
+ const Idmap_constraint* constraints_;
+ uint32_t constraints_count_;
const Idmap_data_header* data_header_;
Idmap_target_entries target_entries_;
Idmap_target_inline_entries target_inline_entries_;
@@ -220,7 +223,10 @@ class LoadedIdmap {
DISALLOW_COPY_AND_ASSIGN(LoadedIdmap);
explicit LoadedIdmap(const std::string& idmap_path, const Idmap_header* header,
- const Idmap_data_header* data_header, Idmap_target_entries target_entries,
+ const Idmap_constraint* constraints,
+ uint32_t constraints_count,
+ const Idmap_data_header* data_header,
+ Idmap_target_entries target_entries,
Idmap_target_inline_entries target_inline_entries,
const Idmap_target_entry_inline_value* inline_entry_values_,
const ConfigDescription* configs, Idmap_overlay_entries overlay_entries,
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index e330410ed1a0..8b2871c21a1e 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -48,7 +48,7 @@
namespace android {
constexpr const uint32_t kIdmapMagic = 0x504D4449u;
-constexpr const uint32_t kIdmapCurrentVersion = 0x0000000Au;
+constexpr const uint32_t kIdmapCurrentVersion = 0x0000000Bu;
// This must never change.
constexpr const uint32_t kFabricatedOverlayMagic = 0x4f525246; // FRRO (big endian)
@@ -2267,7 +2267,7 @@ public:
void** outData, size_t* outSize) const;
static const size_t IDMAP_HEADER_SIZE_BYTES = 4 * sizeof(uint32_t) + 2 * 256;
- static const uint32_t IDMAP_CURRENT_VERSION = 0x00000001;
+ static const size_t IDMAP_CONSTRAINTS_COUNT_SIZE_BYTES = sizeof(uint32_t);
// Retrieve idmap meta-data.
//
diff --git a/libs/androidfw/tests/data/overlay/overlay.idmap b/libs/androidfw/tests/data/overlay/overlay.idmap
index 7e4b261cf109..6bd57c8d517c 100644
--- a/libs/androidfw/tests/data/overlay/overlay.idmap
+++ b/libs/androidfw/tests/data/overlay/overlay.idmap
Binary files differ
diff --git a/libs/hwui/OWNERS b/libs/hwui/OWNERS
index bc174599a4d3..70d13ab8b3e5 100644
--- a/libs/hwui/OWNERS
+++ b/libs/hwui/OWNERS
@@ -4,7 +4,6 @@ alecmouri@google.com
djsollen@google.com
jreck@google.com
njawad@google.com
-scroggo@google.com
sumir@google.com
# For text, e.g. Typeface, Font, Minikin, etc.
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index 2a6919c5e03d..0deed3982d9b 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -145,6 +145,16 @@ flag {
}
flag {
+ name: "enable_output_switcher_device_grouping"
+ namespace: "media_better_together"
+ description: "Enables selected items in Output Switcher to be grouped together."
+ bug: "388347018"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_prevention_of_keep_alive_route_providers"
namespace: "media_solutions"
description: "Enables mechanisms to prevent route providers from keeping malicious apps alive."
diff --git a/media/jni/android_media_MediaDrm.cpp b/media/jni/android_media_MediaDrm.cpp
index 48cd53dc44d7..d50acd837bf9 100644
--- a/media/jni/android_media_MediaDrm.cpp
+++ b/media/jni/android_media_MediaDrm.cpp
@@ -27,7 +27,6 @@
#include "jni.h"
#include <nativehelper/JNIHelp.h>
-#include <android_companion_virtualdevice_flags.h>
#include <android/companion/virtualnative/IVirtualDeviceManagerNative.h>
#include <android/hardware/drm/1.3/IDrmFactory.h>
#include <binder/Parcel.h>
@@ -46,7 +45,6 @@
using ::android::companion::virtualnative::IVirtualDeviceManagerNative;
using ::android::os::PersistableBundle;
namespace drm = ::android::hardware::drm;
-namespace virtualdevice_flags = android::companion::virtualdevice::flags;
namespace android {
@@ -1050,11 +1048,6 @@ DrmPlugin::SecurityLevel jintToSecurityLevel(jint jlevel) {
}
std::vector<int> getVirtualDeviceIds() {
- if (!virtualdevice_flags::device_aware_drm()) {
- ALOGW("Device-aware DRM flag disabled.");
- return std::vector<int>();
- }
-
sp<IBinder> binder =
defaultServiceManager()->checkService(String16("virtualdevice_native"));
if (binder != nullptr) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 69e41a36f48f..aa84571c73d0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -779,7 +779,7 @@ public abstract class InfoMediaManager {
static List<RouteListingPreference.Item> composePreferenceRouteListing(
RouteListingPreference routeListingPreference) {
boolean preferRouteListingOrdering =
- com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping()
+ com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping()
&& preferRouteListingOrdering(routeListingPreference);
List<RouteListingPreference.Item> finalizedItemList = new ArrayList<>();
List<RouteListingPreference.Item> itemList = routeListingPreference.getItems();
@@ -814,7 +814,7 @@ public abstract class InfoMediaManager {
* Returns an ordered list of available devices based on the provided {@code
* routeListingPreferenceItems}.
*
- * <p>The resulting order if enableOutputSwitcherSessionGrouping is disabled is:
+ * <p>The resulting order if enableOutputSwitcherDeviceGrouping is disabled is:
*
* <ol>
* <li>Selected routes.
@@ -822,7 +822,7 @@ public abstract class InfoMediaManager {
* <li>Not-selected, non-system, available routes sorted by route listing preference.
* </ol>
*
- * <p>The resulting order if enableOutputSwitcherSessionGrouping is enabled is:
+ * <p>The resulting order if enableOutputSwitcherDeviceGrouping is enabled is:
*
* <ol>
* <li>Selected routes sorted by route listing preference.
@@ -848,7 +848,7 @@ public abstract class InfoMediaManager {
Set<String> sortedRouteIds = new LinkedHashSet<>();
boolean addSelectedRlpItemsFirst =
- com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping()
+ com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping()
&& preferRouteListingOrdering(routeListingPreference);
Set<String> selectedRouteIds = new HashSet<>();
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
index 93ebc84374b2..7b6604b3f1c6 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java
@@ -942,7 +942,7 @@ public class InfoMediaManagerTest {
assertThat(mInfoMediaManager.getCurrentConnectedDevice()).isEqualTo(device);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void composePreferenceRouteListing_useSystemOrderingIsFalse() {
RouteListingPreference routeListingPreference =
@@ -955,7 +955,7 @@ public class InfoMediaManagerTest {
assertThat(routeOrder.get(1).getRouteId()).isEqualTo(TEST_ID_4);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void composePreferenceRouteListing_useSystemOrderingIsTrue() {
RouteListingPreference routeListingPreference =
@@ -968,7 +968,7 @@ public class InfoMediaManagerTest {
assertThat(routeOrder.get(1).getRouteId()).isEqualTo(TEST_ID_3);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void arrangeRouteListByPreference_useSystemOrderingIsFalse() {
RouteListingPreference routeListingPreference =
@@ -986,7 +986,7 @@ public class InfoMediaManagerTest {
assertThat(routeOrder.get(3).getId()).isEqualTo(TEST_ID_1);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void arrangeRouteListByPreference_useSystemOrderingIsTrue() {
RouteListingPreference routeListingPreference =
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 744388f47d0e..1a6365433be5 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -538,6 +538,7 @@ android_library {
kotlincflags: [
"-Xjvm-default=all",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ "-P plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true",
],
plugins: [
diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp
index c63c2b48638c..9c6bb2c8f778 100644
--- a/packages/SystemUI/compose/core/Android.bp
+++ b/packages/SystemUI/compose/core/Android.bp
@@ -42,6 +42,9 @@ android_library {
"//frameworks/libs/systemui:tracinglib-platform",
],
- kotlincflags: ["-Xjvm-default=all"],
+ kotlincflags: [
+ "-Xjvm-default=all",
+ "-P plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true",
+ ],
use_resource_processor: true,
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 4a4607b6e8fc..2ca70558f18b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -207,12 +207,7 @@ fun CommunalContainer(
Box(modifier = Modifier.fillMaxSize())
}
- scene(
- CommunalScenes.Communal,
- userActions =
- if (viewModel.v2FlagEnabled()) emptyMap()
- else mapOf(Swipe.End to CommunalScenes.Blank),
- ) {
+ scene(CommunalScenes.Communal, userActions = mapOf(Swipe.End to CommunalScenes.Blank)) {
CommunalScene(
backgroundType = backgroundType,
colors = colors,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
index ba25719f1d60..0abed39dce6b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/BurnInState.kt
@@ -26,18 +26,16 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalDensity
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.plugins.clocks.ClockController
import kotlin.math.min
import kotlin.math.roundToInt
/** Produces a [BurnInState] that can be used to query the `LockscreenBurnInViewModel` flows. */
@Composable
-fun rememberBurnIn(
- clockInteractor: KeyguardClockInteractor,
-): BurnInState {
- val clock by clockInteractor.currentClock.collectAsStateWithLifecycle()
+fun rememberBurnIn(clockViewModel: KeyguardClockViewModel): BurnInState {
+ val clock by clockViewModel.currentClock.collectAsStateWithLifecycle()
val (smartspaceTop, onSmartspaceTopChanged) = remember { mutableStateOf<Float?>(null) }
val (smallClockTop, onSmallClockTopChanged) = remember { mutableStateOf<Float?>(null) }
@@ -62,18 +60,12 @@ fun rememberBurnIn(
}
@Composable
-private fun rememberBurnInParameters(
- clock: ClockController?,
- topmostTop: Int,
-): BurnInParameters {
+private fun rememberBurnInParameters(clock: ClockController?, topmostTop: Int): BurnInParameters {
val density = LocalDensity.current
val topInset = WindowInsets.systemBars.union(WindowInsets.displayCutout).getTop(density)
return remember(clock, topInset, topmostTop) {
- BurnInParameters(
- topInset = topInset,
- minViewY = topmostTop,
- )
+ BurnInParameters(topInset = topInset, minViewY = topmostTop)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index abf7fdc05f2e..f51049a10569 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -38,11 +38,11 @@ import com.android.compose.animation.scene.ContentScope
import com.android.compose.modifiers.thenIf
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.notifications.ui.composable.ConstrainedNotificationStack
@@ -89,7 +89,7 @@ constructor(
private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore,
private val aodPromotedNotificationViewModelFactory: AODPromotedNotificationViewModel.Factory,
private val systemBarUtilsState: SystemBarUtilsState,
- private val clockInteractor: KeyguardClockInteractor,
+ private val keyguardClockViewModel: KeyguardClockViewModel,
) {
init {
@@ -118,7 +118,7 @@ constructor(
val isVisible by
keyguardRootViewModel.isAodPromotedNotifVisible.collectAsStateWithLifecycle()
- val burnIn = rememberBurnIn(clockInteractor)
+ val burnIn = rememberBurnIn(keyguardClockViewModel)
AnimatedVisibility(
visible = isVisible,
@@ -141,7 +141,7 @@ constructor(
isVisible.stopAnimating()
}
}
- val burnIn = rememberBurnIn(clockInteractor)
+ val burnIn = rememberBurnIn(keyguardClockViewModel)
AnimatedVisibility(
visibleState = transitionState,
enter = fadeIn(),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
index 410499a3c23f..6293fc26f96a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
@@ -37,7 +37,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState
import com.android.compose.modifiers.thenIf
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene
import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.smallClockScene
import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.splitShadeLargeClockScene
@@ -56,7 +55,7 @@ constructor(
private val mediaCarouselSection: MediaCarouselSection,
private val clockSection: DefaultClockSection,
private val weatherClockSection: WeatherClockSection,
- private val clockInteractor: KeyguardClockInteractor,
+ private val keyguardClockViewModel: KeyguardClockViewModel,
) {
@Composable
fun ContentScope.DefaultClockLayout(
@@ -138,7 +137,7 @@ constructor(
smartSpacePaddingTop: (Resources) -> Int,
modifier: Modifier = Modifier,
) {
- val burnIn = rememberBurnIn(clockInteractor)
+ val burnIn = rememberBurnIn(keyguardClockViewModel)
Column(modifier = modifier) {
with(clockSection) {
@@ -163,7 +162,7 @@ constructor(
smartSpacePaddingTop: (Resources) -> Int,
shouldOffSetClockToOneHalf: Boolean = false,
) {
- val burnIn = rememberBurnIn(clockInteractor)
+ val burnIn = rememberBurnIn(keyguardClockViewModel)
val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsStateWithLifecycle()
LaunchedEffect(isLargeClockVisible) {
@@ -204,7 +203,7 @@ constructor(
smartSpacePaddingTop: (Resources) -> Int,
modifier: Modifier = Modifier,
) {
- val burnIn = rememberBurnIn(clockInteractor)
+ val burnIn = rememberBurnIn(keyguardClockViewModel)
val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsStateWithLifecycle()
val currentClockState = clockViewModel.currentClock.collectAsStateWithLifecycle()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
index f5de7dca6d9d..89f82a90c3b8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/media/controls/ui/composable/MediaCarousel.kt
@@ -119,6 +119,8 @@ fun ContentScope.MediaCarousel(
},
update = {
MediaCarouselStateLoader.loadCarouselState(carouselController, carouselState())
+ carouselController.mediaCarouselScrollHandler.showsSettingsButton =
+ !mediaHost.showsOnlyActiveMedia
it.setView(carouselController.mediaFrame)
},
onRelease = { it.removeAllViews() },
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
index 7c50d6f8af12..db1358a5a28a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
@@ -30,9 +30,9 @@ import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
import com.android.systemui.keyguard.ui.composable.section.DefaultClockSection
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayActionsViewModel
import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeOverlayContentViewModel
@@ -57,7 +57,7 @@ constructor(
private val shadeSession: SaveableSession,
private val stackScrollView: Lazy<NotificationScrollView>,
private val clockSection: DefaultClockSection,
- private val clockInteractor: KeyguardClockInteractor,
+ private val keyguardClockViewModel: KeyguardClockViewModel,
) : Overlay {
override val key = Overlays.NotificationsShade
@@ -105,7 +105,7 @@ constructor(
Box {
Column {
if (viewModel.showClock) {
- val burnIn = rememberBurnIn(clockInteractor)
+ val burnIn = rememberBurnIn(keyguardClockViewModel)
with(clockSection) {
SmallClock(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index 62a8cc5a7fe3..061fdd99eb1b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -84,7 +84,9 @@ import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.media.controls.ui.composable.MediaCarousel
import com.android.systemui.media.controls.ui.composable.isLandscape
import com.android.systemui.media.controls.ui.controller.MediaCarouselController
+import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
import com.android.systemui.media.controls.ui.view.MediaHost
+import com.android.systemui.media.controls.ui.view.MediaHostState.Companion.EXPANDED
import com.android.systemui.media.dagger.MediaModule
import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
import com.android.systemui.notifications.ui.composable.NotificationScrollingStack
@@ -165,6 +167,12 @@ constructor(
shadeSession = shadeSession,
)
}
+
+ init {
+ mediaHost.expansion = EXPANDED
+ mediaHost.showsOnlyActiveMedia = false
+ mediaHost.init(MediaHierarchyManager.LOCATION_QS)
+ }
}
@Composable
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index da4e5824eb3e..619b4280d954 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -46,6 +46,8 @@ import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.observableTransitionState
import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState
import com.android.compose.gesture.effect.rememberOffsetOverscrollEffectFactory
+import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
+import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
import com.android.systemui.lifecycle.rememberActivated
import com.android.systemui.qs.ui.adapter.QSSceneAdapter
import com.android.systemui.qs.ui.composable.QuickSettingsTheme
@@ -239,7 +241,12 @@ fun SceneContainer(
BottomRightCornerRibbon(
content = { Text(text = "flexi\uD83E\uDD43", color = Color.White) },
colorSaturation = { viewModel.ribbonColorSaturation },
- modifier = Modifier.align(Alignment.BottomEnd),
+ modifier =
+ Modifier.align(Alignment.BottomEnd)
+ .burnInAware(
+ viewModel = viewModel.burnIn,
+ params = rememberBurnIn(viewModel.clock).parameters,
+ ),
)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
index 1423d4acca21..6d906bd4aa66 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
@@ -59,10 +59,7 @@ class SceneTransitionLayoutDataSource(
initialValue = emptySet(),
)
- override fun changeScene(
- toScene: SceneKey,
- transitionKey: TransitionKey?,
- ) {
+ override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
state.setTargetScene(
targetScene = toScene,
transitionKey = transitionKey,
@@ -71,9 +68,7 @@ class SceneTransitionLayoutDataSource(
}
override fun snapToScene(toScene: SceneKey) {
- state.snapToScene(
- scene = toScene,
- )
+ state.snapToScene(scene = toScene)
}
override fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) {
@@ -100,4 +95,18 @@ class SceneTransitionLayoutDataSource(
transitionKey = transitionKey,
)
}
+
+ override fun instantlyShowOverlay(overlay: OverlayKey) {
+ state.snapToScene(
+ scene = state.transitionState.currentScene,
+ currentOverlays = state.currentOverlays + overlay,
+ )
+ }
+
+ override fun instantlyHideOverlay(overlay: OverlayKey) {
+ state.snapToScene(
+ scene = state.transitionState.currentScene,
+ currentOverlays = state.currentOverlays - overlay,
+ )
+ }
}
diff --git a/packages/SystemUI/compose/scene/Android.bp b/packages/SystemUI/compose/scene/Android.bp
index 090e9ccedda0..42dd85a3d0a7 100644
--- a/packages/SystemUI/compose/scene/Android.bp
+++ b/packages/SystemUI/compose/scene/Android.bp
@@ -45,6 +45,9 @@ android_library {
"mechanics",
],
- kotlincflags: ["-Xjvm-default=all"],
+ kotlincflags: [
+ "-Xjvm-default=all",
+ "-P plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true",
+ ],
use_resource_processor: true,
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt
index cade9bff5abb..ad2ddfe2b2a4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/testing/ElementStateAccess.kt
@@ -16,9 +16,12 @@
package com.android.compose.animation.scene.testing
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.unit.IntSize
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.Element.Companion.AlphaUnspecified
+import com.android.compose.animation.scene.Element.Companion.SizeUnspecified
import com.android.compose.animation.scene.ElementModifier
import com.android.compose.animation.scene.Scale
@@ -28,6 +31,12 @@ val SemanticsNode.lastAlphaForTesting: Float?
val SemanticsNode.lastScaleForTesting: Scale?
get() = elementState.lastScale.takeIf { it != Scale.Unspecified }
+val SemanticsNode.lastOffsetForTesting: Offset?
+ get() = elementState.lastOffset.takeIf { it != Offset.Unspecified }
+
+val SemanticsNode.lastSizeForTesting: IntSize?
+ get() = elementState.lastSize.takeIf { it != SizeUnspecified }
+
private val SemanticsNode.elementState: Element.State
get() {
val elementModifier =
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/DataPointTypes.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/DataPointTypes.kt
new file mode 100644
index 000000000000..7be7fa17eeea
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/DataPointTypes.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.animation.scene
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isFinite
+import androidx.compose.ui.geometry.isUnspecified
+import org.json.JSONObject
+import platform.test.motion.golden.DataPointType
+import platform.test.motion.golden.UnknownTypeException
+
+fun Scale.asDataPoint() = DataPointTypes.scale.makeDataPoint(this)
+
+object DataPointTypes {
+ val scale: DataPointType<Scale> =
+ DataPointType(
+ "scale",
+ jsonToValue = {
+ when (it) {
+ "unspecified" -> Scale.Unspecified
+ "default" -> Scale.Default
+ "zero" -> Scale.Zero
+ is JSONObject -> {
+ val pivot = it.get("pivot")
+ Scale(
+ scaleX = it.getDouble("x").toFloat(),
+ scaleY = it.getDouble("y").toFloat(),
+ pivot =
+ when (pivot) {
+ "unspecified" -> Offset.Unspecified
+ "infinite" -> Offset.Infinite
+ is JSONObject ->
+ Offset(
+ pivot.getDouble("x").toFloat(),
+ pivot.getDouble("y").toFloat(),
+ )
+ else -> throw UnknownTypeException()
+ },
+ )
+ }
+ else -> throw UnknownTypeException()
+ }
+ },
+ valueToJson = {
+ when (it) {
+ Scale.Unspecified -> "unspecified"
+ Scale.Default -> "default"
+ Scale.Zero -> "zero"
+ else -> {
+ JSONObject().apply {
+ put("x", it.scaleX)
+ put("y", it.scaleY)
+ put(
+ "pivot",
+ when {
+ it.pivot.isUnspecified -> "unspecified"
+ !it.pivot.isFinite -> "infinite"
+ else ->
+ JSONObject().apply {
+ put("x", it.pivot.x)
+ put("y", it.pivot.y)
+ }
+ },
+ )
+ }
+ }
+ }
+ },
+ )
+}
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/FeatureCaptures.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/FeatureCaptures.kt
new file mode 100644
index 000000000000..8658bbf1da56
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/FeatureCaptures.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.compose.animation.scene
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.DataPointTypes.scale
+import com.android.compose.animation.scene.testing.lastAlphaForTesting
+import com.android.compose.animation.scene.testing.lastOffsetForTesting
+import com.android.compose.animation.scene.testing.lastScaleForTesting
+import com.android.compose.animation.scene.testing.lastSizeForTesting
+import platform.test.motion.compose.DataPointTypes.intSize
+import platform.test.motion.compose.DataPointTypes.offset
+import platform.test.motion.golden.DataPoint
+import platform.test.motion.golden.DataPointTypes
+import platform.test.motion.golden.FeatureCapture
+
+/**
+ * [FeatureCapture] implementations to record animated state of [SceneTransitionLayout] [Element].
+ */
+object FeatureCaptures {
+
+ val elementAlpha =
+ FeatureCapture<SemanticsNode, Float>("alpha") {
+ DataPoint.of(it.lastAlphaForTesting, DataPointTypes.float)
+ }
+
+ val elementScale =
+ FeatureCapture<SemanticsNode, Scale>("scale") {
+ DataPoint.of(it.lastScaleForTesting, scale)
+ }
+
+ val elementOffset =
+ FeatureCapture<SemanticsNode, Offset>("offset") {
+ DataPoint.of(it.lastOffsetForTesting, offset)
+ }
+
+ val elementSize =
+ FeatureCapture<SemanticsNode, IntSize>("size") {
+ DataPoint.of(it.lastSizeForTesting, intSize)
+ }
+}
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 3fd796a9481a..2e5b5b56c982 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
@@ -16,6 +16,7 @@ package com.android.systemui.shared.clocks
import android.content.Context
import android.content.res.Resources
import android.graphics.Typeface
+import android.os.Vibrator
import android.view.LayoutInflater
import com.android.systemui.customization.R
import com.android.systemui.log.core.MessageBuffer
@@ -40,6 +41,7 @@ data class ClockContext(
val typefaceCache: TypefaceCache,
val messageBuffers: ClockMessageBuffers,
val messageBuffer: MessageBuffer,
+ val vibrator: Vibrator?,
)
/** Provides the default system clock */
@@ -48,6 +50,7 @@ class DefaultClockProvider(
val layoutInflater: LayoutInflater,
val resources: Resources,
private val isClockReactiveVariantsEnabled: Boolean = false,
+ private val vibrator: Vibrator?,
) : ClockProvider {
private var messageBuffers: ClockMessageBuffers? = null
@@ -82,6 +85,7 @@ class DefaultClockProvider(
typefaceCache,
buffers,
buffers.infraMessageBuffer,
+ vibrator,
)
)
} else {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index c6d31a58bc7d..b9a5f1f18210 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -24,6 +24,7 @@ import android.graphics.Point
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
+import android.os.VibrationEffect
import android.text.Layout
import android.text.TextPaint
import android.util.AttributeSet
@@ -67,7 +68,7 @@ enum class HorizontalAlignment {
@SuppressLint("AppCompatCustomView")
open class SimpleDigitalClockTextView(
- clockCtx: ClockContext,
+ val clockCtx: ClockContext,
isLargeClock: Boolean,
attrs: AttributeSet? = null,
) : TextView(clockCtx.context, attrs) {
@@ -92,6 +93,9 @@ open class SimpleDigitalClockTextView(
(fixedAodAxes + listOf(roundAxis, SLANT_AXIS)).toFVar()
}
+ // TODO(b/374306512): Fidget endpoint to spec
+ private var fidgetFontVariation = aodFontVariation
+
private val parser = DimensionParser(clockCtx.context)
var maxSingleDigitHeight = -1
var maxSingleDigitWidth = -1
@@ -289,24 +293,45 @@ open class SimpleDigitalClockTextView(
return
}
logger.d("animateCharge()")
- val startAnimPhase2 = Runnable {
- textAnimator.setTextStyle(
- fvar = if (dozeFraction == 0F) lsFontVariation else aodFontVariation,
- animate = isAnimationEnabled,
- )
- updateTextBoundsForTextAnimator()
- }
textAnimator.setTextStyle(
fvar = if (dozeFraction == 0F) aodFontVariation else lsFontVariation,
animate = isAnimationEnabled,
- onAnimationEnd = startAnimPhase2,
+ onAnimationEnd =
+ Runnable {
+ textAnimator.setTextStyle(
+ fvar = if (dozeFraction == 0F) lsFontVariation else aodFontVariation,
+ animate = isAnimationEnabled,
+ )
+ updateTextBoundsForTextAnimator()
+ },
)
updateTextBoundsForTextAnimator()
}
fun animateFidget(x: Float, y: Float) {
- // TODO(b/374306512): Implement Fidget Animation
+ if (!this::textAnimator.isInitialized || textAnimator.isRunning()) {
+ // Skip fidget animation if other animation is already playing.
+ return
+ }
+
logger.animateFidget(x, y)
+ clockCtx.vibrator?.vibrate(FIDGET_HAPTICS)
+
+ // TODO(b/374306512): Duplicated charge animation as placeholder. Implement final version
+ // when we have a complete spec. May require additional code to animate individual digits.
+ textAnimator.setTextStyle(
+ fvar = fidgetFontVariation,
+ animate = isAnimationEnabled,
+ onAnimationEnd =
+ Runnable {
+ textAnimator.setTextStyle(
+ fvar = if (dozeFraction == 0F) lsFontVariation else aodFontVariation,
+ animate = isAnimationEnabled,
+ )
+ updateTextBoundsForTextAnimator()
+ },
+ )
+ updateTextBoundsForTextAnimator()
}
fun refreshText() {
@@ -533,6 +558,12 @@ open class SimpleDigitalClockTextView(
private val PORTER_DUFF_XFER_MODE_PAINT =
Paint().also { it.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) }
+ val FIDGET_HAPTICS =
+ VibrationEffect.startComposition()
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, 0)
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 43)
+ .compose()
+
val AOD_COLOR = Color.WHITE
val LS_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 400f)
val AOD_WEIGHT_AXIS = ClockFontAxisSetting(GSFAxes.WEIGHT, 200f)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
index bd33e52689c2..f53f964cd3d9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
@@ -64,12 +64,12 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
-import java.util.List;
-import java.util.Optional;
-
import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
+import java.util.List;
+import java.util.Optional;
+
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
@@ -171,6 +171,7 @@ public class BouncerFullscreenSwipeTouchHandlerTest extends SysuiTestCase {
mActivityStarter,
mKeyguardInteractor,
mSceneInteractor,
+ mKosmos.getShadeRepository(),
Optional.of(() -> mWindowRootView));
when(mScrimManager.getCurrentController()).thenReturn(mScrimController);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
index 494e0b4deef4..dd43d817cccc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
@@ -74,12 +74,12 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
-import java.util.List;
-import java.util.Optional;
-
import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
+import java.util.List;
+import java.util.Optional;
+
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
@@ -187,6 +187,7 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
mActivityStarter,
mKeyguardInteractor,
mSceneInteractor,
+ mKosmos.getShadeRepository(),
Optional.of(() -> mWindowRootView)
);
@@ -627,6 +628,22 @@ public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
onRemovedCallbackCaptor.getValue().onRemoved();
}
+ @Test
+ public void testTouchSessionStart_notifiesShadeOfUserInteraction() {
+ mTouchHandler.onSessionStart(mTouchSession);
+
+ mKosmos.getTestScope().getTestScheduler().runCurrent();
+ assertThat(mKosmos.getShadeRepository().getLegacyShadeTracking().getValue()).isTrue();
+
+ ArgumentCaptor<TouchHandler.TouchSession.Callback> onRemovedCallbackCaptor =
+ ArgumentCaptor.forClass(TouchHandler.TouchSession.Callback.class);
+ verify(mTouchSession).registerCallback(onRemovedCallbackCaptor.capture());
+ onRemovedCallbackCaptor.getValue().onRemoved();
+
+ mKosmos.getTestScope().getTestScheduler().runCurrent();
+ assertThat(mKosmos.getShadeRepository().getLegacyShadeTracking().getValue()).isFalse();
+ }
+
private void swipeToPosition(float percent, float velocityY) {
Mockito.clearInvocations(mTouchSession);
mTouchHandler.onSessionStart(mTouchSession);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
index d8a9719d2058..dda460a6198f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -30,22 +30,17 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.isFinite
-import androidx.compose.ui.geometry.isUnspecified
-import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.android.compose.animation.scene.ContentScope
+import com.android.compose.animation.scene.FeatureCaptures.elementAlpha
+import com.android.compose.animation.scene.FeatureCaptures.elementScale
import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.compose.animation.scene.Scale
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.isElement
-import com.android.compose.animation.scene.testing.lastAlphaForTesting
-import com.android.compose.animation.scene.testing.lastScaleForTesting
import com.android.compose.theme.PlatformTheme
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
@@ -71,12 +66,12 @@ import com.android.systemui.scene.ui.composable.Scene
import com.android.systemui.scene.ui.composable.SceneContainer
import com.android.systemui.scene.ui.view.sceneJankMonitorFactory
import com.android.systemui.testKosmos
+import kotlin.test.Ignore
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
-import org.json.JSONObject
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -89,14 +84,8 @@ import platform.test.motion.compose.MotionControl
import platform.test.motion.compose.feature
import platform.test.motion.compose.recordMotion
import platform.test.motion.compose.runTest
-import platform.test.motion.golden.DataPoint
-import platform.test.motion.golden.DataPointType
-import platform.test.motion.golden.DataPointTypes
-import platform.test.motion.golden.FeatureCapture
-import platform.test.motion.golden.UnknownTypeException
import platform.test.screenshot.DeviceEmulationSpec
import platform.test.screenshot.Displays.Phone
-import kotlin.test.Ignore
/** MotionTest for the Bouncer Predictive Back animation */
@LargeTest
@@ -280,72 +269,4 @@ class BouncerPredictiveBackTest : SysuiTestCase() {
override suspend fun onActivated() = awaitCancellation()
}
-
- companion object {
- private val elementAlpha =
- FeatureCapture<SemanticsNode, Float>("alpha") {
- DataPoint.of(it.lastAlphaForTesting, DataPointTypes.float)
- }
-
- private val elementScale =
- FeatureCapture<SemanticsNode, Scale>("scale") {
- DataPoint.of(it.lastScaleForTesting, scale)
- }
-
- private val scale: DataPointType<Scale> =
- DataPointType(
- "scale",
- jsonToValue = {
- when (it) {
- "unspecified" -> Scale.Unspecified
- "default" -> Scale.Default
- "zero" -> Scale.Zero
- is JSONObject -> {
- val pivot = it.get("pivot")
- Scale(
- scaleX = it.getDouble("x").toFloat(),
- scaleY = it.getDouble("y").toFloat(),
- pivot =
- when (pivot) {
- "unspecified" -> Offset.Unspecified
- "infinite" -> Offset.Infinite
- is JSONObject ->
- Offset(
- pivot.getDouble("x").toFloat(),
- pivot.getDouble("y").toFloat(),
- )
- else -> throw UnknownTypeException()
- },
- )
- }
- else -> throw UnknownTypeException()
- }
- },
- valueToJson = {
- when (it) {
- Scale.Unspecified -> "unspecified"
- Scale.Default -> "default"
- Scale.Zero -> "zero"
- else -> {
- JSONObject().apply {
- put("x", it.scaleX)
- put("y", it.scaleY)
- put(
- "pivot",
- when {
- it.pivot.isUnspecified -> "unspecified"
- !it.pivot.isFinite -> "infinite"
- else ->
- JSONObject().apply {
- put("x", it.pivot.x)
- put("y", it.pivot.y)
- }
- },
- )
- }
- }
- }
- },
- )
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
index 3eb08004ae61..f063655b9f86 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/DefaultWidgetPopulationTest.kt
@@ -18,7 +18,7 @@ package com.android.systemui.communal.data.db
import android.content.ComponentName
import android.os.UserHandle
-import android.os.UserManager
+import android.os.userManager
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -27,10 +27,13 @@ import com.android.systemui.communal.data.db.DefaultWidgetPopulation.SkipReason.
import com.android.systemui.communal.shared.model.SpanValue
import com.android.systemui.communal.widgets.CommunalWidgetHost
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.log.logcatLogBuffer
import com.android.systemui.testKosmos
-import kotlinx.coroutines.test.runCurrent
+import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.domain.interactor.userLockedInteractor
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -38,6 +41,7 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
@@ -46,8 +50,7 @@ import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
class DefaultWidgetPopulationTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val communalWidgetHost =
mock<CommunalWidgetHost> {
@@ -57,11 +60,6 @@ class DefaultWidgetPopulationTest : SysuiTestCase() {
private val communalWidgetDao = mock<CommunalWidgetDao>()
private val database = mock<SupportSQLiteDatabase>()
private val mainUser = UserHandle(0)
- private val userManager =
- mock<UserManager> {
- on { mainUser }.thenReturn(mainUser)
- on { getUserSerialNumber(0) }.thenReturn(0)
- }
private val defaultWidgets =
arrayOf(
@@ -74,6 +72,7 @@ class DefaultWidgetPopulationTest : SysuiTestCase() {
@Before
fun setUp() {
+ kosmos.fakeUserRepository.setUserUnlocked(MAIN_USER_ID, true)
underTest =
DefaultWidgetPopulation(
bgScope = kosmos.applicationCoroutineScope,
@@ -81,32 +80,45 @@ class DefaultWidgetPopulationTest : SysuiTestCase() {
communalWidgetDaoProvider = { communalWidgetDao },
defaultWidgets = defaultWidgets,
logBuffer = logcatLogBuffer("DefaultWidgetPopulationTest"),
- userManager = userManager,
+ userManager = kosmos.userManager,
+ userLockedInteractor = kosmos.userLockedInteractor,
)
}
@Test
+ fun testNoInteractionUntilMainUserUnlocked() =
+ kosmos.runTest {
+ kosmos.fakeUserRepository.setUserUnlocked(MAIN_USER_ID, false)
+ // Database created
+ underTest.onCreate(database)
+ verify(communalWidgetHost, never())
+ .allocateIdAndBindWidget(provider = any(), user = any())
+ kosmos.fakeUserRepository.setUserUnlocked(MAIN_USER_ID, true)
+ verify(communalWidgetHost, atLeastOnce())
+ .allocateIdAndBindWidget(provider = any(), user = any())
+ }
+
+ @Test
fun testPopulateDefaultWidgetsWhenDatabaseCreated() =
- testScope.runTest {
+ kosmos.runTest {
// Database created
underTest.onCreate(database)
- runCurrent()
// Verify default widgets bound
verify(communalWidgetHost)
.allocateIdAndBindWidget(
provider = eq(ComponentName.unflattenFromString(defaultWidgets[0])!!),
- user = eq(mainUser),
+ user = eq(UserHandle(MAIN_USER_ID)),
)
verify(communalWidgetHost)
.allocateIdAndBindWidget(
provider = eq(ComponentName.unflattenFromString(defaultWidgets[1])!!),
- user = eq(mainUser),
+ user = eq(UserHandle(MAIN_USER_ID)),
)
verify(communalWidgetHost)
.allocateIdAndBindWidget(
provider = eq(ComponentName.unflattenFromString(defaultWidgets[2])!!),
- user = eq(mainUser),
+ user = eq(UserHandle(MAIN_USER_ID)),
)
// Verify default widgets added in database
@@ -138,13 +150,12 @@ class DefaultWidgetPopulationTest : SysuiTestCase() {
@Test
fun testSkipDefaultWidgetsPopulation() =
- testScope.runTest {
+ kosmos.runTest {
// Skip default widgets population
underTest.skipDefaultWidgetsPopulation(RESTORED_FROM_BACKUP)
// Database created
underTest.onCreate(database)
- runCurrent()
// Verify no widget bounded or added to the database
verify(communalWidgetHost, never()).allocateIdAndBindWidget(any(), any())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index c3cc3e66f81f..8424746f3db5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -75,6 +75,7 @@ import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.statusbar.phone.fakeManagedProfileController
import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
@@ -163,12 +164,12 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- fun isCommunalAvailable_storageUnlockedAndMainUser_true() =
+ fun isCommunalAvailable_mainUserUnlockedAndMainUser_true() =
kosmos.runTest {
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
@@ -176,12 +177,12 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- fun isCommunalAvailable_storageLockedAndMainUser_false() =
+ fun isCommunalAvailable_mainUserLockedAndMainUser_false() =
kosmos.runTest {
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
- fakeKeyguardRepository.setIsEncryptedOrLockdown(true)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
@@ -189,12 +190,12 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- fun isCommunalAvailable_storageUnlockedAndSecondaryUser_false() =
+ fun isCommunalAvailable_mainUserUnlockedAndSecondaryUser_false() =
kosmos.runTest {
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(secondaryUser)
fakeKeyguardRepository.setKeyguardShowing(true)
@@ -207,7 +208,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
@@ -222,7 +223,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, false)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
@@ -1282,7 +1283,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
@Test
fun showCommunalWhileCharging() =
kosmos.runTest {
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
fakeSettings.putIntForUser(
@@ -1302,7 +1303,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
@Test
fun showCommunalWhilePosturedAndCharging() =
kosmos.runTest {
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
fakeSettings.putIntForUser(
@@ -1323,7 +1324,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
@Test
fun showCommunalWhileDocked() =
kosmos.runTest {
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(mainUser)
fakeKeyguardRepository.setKeyguardShowing(true)
fakeSettings.putIntForUser(Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK, 1, mainUser.id)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt
index 9fab652e2375..2f3073e8eb23 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt
@@ -37,6 +37,7 @@ import com.android.systemui.lifecycle.activateIn
import com.android.systemui.plugins.activityStarter
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.statusbar.policy.batteryController
+import com.android.systemui.statusbar.policy.fake
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.settings.fakeSettings
@@ -47,7 +48,6 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.verify
import org.mockito.kotlin.any
-import org.mockito.kotlin.whenever
@SmallTest
@EnableFlags(FLAG_GLANCEABLE_HUB_V2)
@@ -69,7 +69,7 @@ class CommunalToDreamButtonViewModelTest : SysuiTestCase() {
fun shouldShowDreamButtonOnHub_trueWhenPluggedIn() =
with(kosmos) {
runTest {
- whenever(batteryController.isPluggedIn()).thenReturn(true)
+ batteryController.fake._isPluggedIn = true
runCurrent()
assertThat(underTest.shouldShowDreamButtonOnHub).isTrue()
@@ -80,8 +80,7 @@ class CommunalToDreamButtonViewModelTest : SysuiTestCase() {
fun shouldShowDreamButtonOnHub_falseWhenNotPluggedIn() =
with(kosmos) {
runTest {
- whenever(batteryController.isPluggedIn()).thenReturn(false)
- runCurrent()
+ batteryController.fake._isPluggedIn = false
assertThat(underTest.shouldShowDreamButtonOnHub).isFalse()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt
index c158baf5a80c..619995478ecc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModelTest.kt
@@ -35,7 +35,6 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
-import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
import com.android.systemui.shade.domain.interactor.enableDualShade
@@ -74,7 +73,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() {
setUpState(isShadeTouchable = true, isDeviceUnlocked = false)
assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen))
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))
@@ -83,7 +82,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() {
setUpState(isShadeTouchable = true, isDeviceUnlocked = true)
assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen))
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))
}
@@ -96,7 +95,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() {
setUpState(isShadeTouchable = true, isDeviceUnlocked = false)
assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen))
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
assertThat(actions?.get(Swipe.Down))
.isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))
@@ -106,7 +105,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() {
setUpState(isShadeTouchable = true, isDeviceUnlocked = true)
assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen))
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
assertThat(actions?.get(Swipe.Down))
.isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))
@@ -120,7 +119,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() {
setUpState(isShadeTouchable = true, isDeviceUnlocked = false)
assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen))
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
assertThat(actions?.get(Swipe.Down))
.isEqualTo(UserActionResult.ShowOverlay(Overlays.NotificationsShade))
@@ -130,7 +129,7 @@ class CommunalUserActionsViewModelTest : SysuiTestCase() {
setUpState(isShadeTouchable = true, isDeviceUnlocked = true)
assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(SceneFamilies.Home))
+ assertThat(actions?.get(Swipe.End)).isEqualTo(UserActionResult(Scenes.Lockscreen))
assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
assertThat(actions?.get(Swipe.Down))
.isEqualTo(UserActionResult.ShowOverlay(Overlays.NotificationsShade))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index dbdd7fb2773a..85155157eda2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -203,7 +203,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
// Keyguard showing, storage unlocked, main user, and tutorial not started.
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setKeyguardOccluded(false)
- keyguardRepository.setIsEncryptedOrLockdown(false)
+ userRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, true)
setIsMainUser(true)
tutorialRepository.setTutorialSettingState(
Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index 95681941a1c1..c15f797aad5d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -37,7 +37,9 @@ import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID
import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.domain.interactor.userLockedInteractor
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
@@ -91,6 +93,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
kosmos.testDispatcher,
{ widgetManager },
helper,
+ kosmos.userLockedInteractor,
)
}
@@ -269,6 +272,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
// Binding to the service does not require keyguard showing
setCommunalAvailable(true, setKeyguardShowing = false)
+ fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
runCurrent()
verify(widgetManager).register()
@@ -283,7 +287,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
setKeyguardShowing: Boolean = true,
) =
with(kosmos) {
- fakeKeyguardRepository.setIsEncryptedOrLockdown(false)
+ fakeUserRepository.setUserUnlocked(MAIN_USER_ID, true)
fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
if (setKeyguardShowing) {
fakeKeyguardRepository.setKeyguardShowing(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
index 1c93b3c66e32..102ce0b51c94 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelTest.kt
@@ -23,7 +23,6 @@ import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.EnableSceneContainer
@@ -62,15 +61,14 @@ class DreamUserActionsViewModelTest : SysuiTestCase() {
@Before
fun setUp() {
- underTest = kosmos.dreamUserActionsViewModel
+ underTest = kosmos.dreamUserActionsViewModelFactory.create()
underTest.activateIn(testScope)
}
@Test
- fun actions_communalNotAvailable_singleShade() =
+ fun actions_singleShade() =
testScope.runTest {
kosmos.enableSingleShade()
- kosmos.setCommunalAvailable(false)
val actions by collectLastValue(underTest.actions)
@@ -93,10 +91,9 @@ class DreamUserActionsViewModelTest : SysuiTestCase() {
}
@Test
- fun actions_communalNotAvailable_splitShade() =
+ fun actions_splitShade() =
testScope.runTest {
kosmos.enableSplitShade()
- kosmos.setCommunalAvailable(false)
val actions by collectLastValue(underTest.actions)
@@ -121,10 +118,9 @@ class DreamUserActionsViewModelTest : SysuiTestCase() {
}
@Test
- fun actions_communalNotAvailable_dualShade() =
+ fun actions_dualShade() =
testScope.runTest {
kosmos.enableDualShade()
- kosmos.setCommunalAvailable(false)
val actions by collectLastValue(underTest.actions)
@@ -148,88 +144,6 @@ class DreamUserActionsViewModelTest : SysuiTestCase() {
assertThat(actions?.get(Swipe.End)).isNull()
}
- @Test
- fun actions_communalAvailable_singleShade() =
- testScope.runTest {
- kosmos.enableSingleShade()
- kosmos.setCommunalAvailable(true)
-
- val actions by collectLastValue(underTest.actions)
-
- setUpState(isShadeTouchable = true, isDeviceUnlocked = false)
- assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
- assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))
- assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
- assertThat(actions?.get(Swipe.End)).isNull()
-
- setUpState(isShadeTouchable = false, isDeviceUnlocked = false)
- assertThat(actions).isEmpty()
-
- setUpState(isShadeTouchable = true, isDeviceUnlocked = true)
- assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
- assertThat(actions?.get(Swipe.Down)).isEqualTo(UserActionResult(Scenes.Shade))
- assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
- assertThat(actions?.get(Swipe.End)).isNull()
- }
-
- @Test
- fun actions_communalAvailable_splitShade() =
- testScope.runTest {
- kosmos.enableSplitShade()
- kosmos.setCommunalAvailable(true)
-
- val actions by collectLastValue(underTest.actions)
-
- setUpState(isShadeTouchable = true, isDeviceUnlocked = false)
- assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
- assertThat(actions?.get(Swipe.Down))
- .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))
- assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
- assertThat(actions?.get(Swipe.End)).isNull()
-
- setUpState(isShadeTouchable = false, isDeviceUnlocked = false)
- assertThat(actions).isEmpty()
-
- setUpState(isShadeTouchable = true, isDeviceUnlocked = true)
- assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
- assertThat(actions?.get(Swipe.Down))
- .isEqualTo(UserActionResult(Scenes.Shade, ToSplitShade))
- assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
- assertThat(actions?.get(Swipe.End)).isNull()
- }
-
- @Test
- fun actions_communalAvailable_dualShade() =
- testScope.runTest {
- kosmos.enableDualShade()
- kosmos.setCommunalAvailable(true)
-
- val actions by collectLastValue(underTest.actions)
-
- setUpState(isShadeTouchable = true, isDeviceUnlocked = false)
- assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Bouncer))
- assertThat(actions?.get(Swipe.Down))
- .isEqualTo(UserActionResult.ShowOverlay(Overlays.NotificationsShade))
- assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
- assertThat(actions?.get(Swipe.End)).isNull()
-
- setUpState(isShadeTouchable = false, isDeviceUnlocked = false)
- assertThat(actions).isEmpty()
-
- setUpState(isShadeTouchable = true, isDeviceUnlocked = true)
- assertThat(actions).isNotEmpty()
- assertThat(actions?.get(Swipe.Up)).isEqualTo(UserActionResult(Scenes.Gone))
- assertThat(actions?.get(Swipe.Down))
- .isEqualTo(UserActionResult.ShowOverlay(Overlays.NotificationsShade))
- assertThat(actions?.get(Swipe.Start)).isEqualTo(UserActionResult(Scenes.Communal))
- assertThat(actions?.get(Swipe.End)).isNull()
- }
-
private fun TestScope.setUpState(isShadeTouchable: Boolean, isDeviceUnlocked: Boolean) {
if (isShadeTouchable) {
kosmos.powerInteractor.setAwakeForTest()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
index cf8123764928..b66e2fe13e8a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelTest.kt
@@ -29,7 +29,6 @@ import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.flags.EnableSceneContainer
@@ -65,13 +64,12 @@ import platform.test.runner.parameterized.Parameters
class LockscreenUserActionsViewModelTest : SysuiTestCase() {
companion object {
- private const val parameterCount = 7
+ private const val parameterCount = 6
@Parameters(
name =
"canSwipeToEnter={0}, downWithTwoPointers={1}, downFromEdge={2}," +
- " isSingleShade={3}, isCommunalAvailable={4}, isShadeTouchable={5}," +
- " isOccluded={6}"
+ " isSingleShade={3}, isShadeTouchable={4}, isOccluded={6}"
)
@JvmStatic
fun combinations() = buildList {
@@ -82,9 +80,8 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() {
/* downWithTwoPointers= */ combination and 2 != 0,
/* downFromEdge= */ combination and 4 != 0,
/* isSingleShade= */ combination and 8 != 0,
- /* isCommunalAvailable= */ combination and 16 != 0,
- /* isShadeTouchable= */ combination and 32 != 0,
- /* isOccluded= */ combination and 64 != 0,
+ /* isShadeTouchable= */ combination and 16 != 0,
+ /* isOccluded= */ combination and 32 != 0,
)
.also { check(it.size == parameterCount) }
)
@@ -145,17 +142,6 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() {
else -> Scenes.Bouncer
}
}
-
- private fun expectedStartDestination(
- isCommunalAvailable: Boolean,
- isShadeTouchable: Boolean,
- ): SceneKey? {
- return when {
- !isShadeTouchable -> null
- isCommunalAvailable -> Scenes.Communal
- else -> null
- }
- }
}
private val kosmos = testKosmos()
@@ -166,9 +152,8 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() {
@JvmField @Parameter(1) var downWithTwoPointers: Boolean = false
@JvmField @Parameter(2) var downFromEdge: Boolean = false
@JvmField @Parameter(3) var isNarrowScreen: Boolean = true
- @JvmField @Parameter(4) var isCommunalAvailable: Boolean = false
- @JvmField @Parameter(5) var isShadeTouchable: Boolean = false
- @JvmField @Parameter(6) var isOccluded: Boolean = false
+ @JvmField @Parameter(4) var isShadeTouchable: Boolean = false
+ @JvmField @Parameter(5) var isOccluded: Boolean = false
private val underTest by lazy { kosmos.lockscreenUserActionsViewModel }
@@ -188,7 +173,6 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() {
)
sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
kosmos.shadeRepository.setShadeLayoutWide(!isNarrowScreen)
- kosmos.setCommunalAvailable(isCommunalAvailable)
kosmos.fakePowerRepository.updateWakefulness(
rawState =
if (isShadeTouchable) {
@@ -246,22 +230,6 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() {
isShadeTouchable = isShadeTouchable,
)
)
-
- val startScene by
- collectLastValue(
- (userActions?.get(Swipe.Start) as? UserActionResult.ChangeScene)
- ?.toScene
- ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) }
- ?: flowOf(null)
- )
-
- assertThat(startScene)
- .isEqualTo(
- expectedStartDestination(
- isCommunalAvailable = isCommunalAvailable,
- isShadeTouchable = isShadeTouchable,
- )
- )
}
@Test
@@ -279,7 +247,6 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() {
)
sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
kosmos.enableDualShade(wideLayout = !isNarrowScreen)
- kosmos.setCommunalAvailable(isCommunalAvailable)
kosmos.fakePowerRepository.updateWakefulness(
rawState =
if (isShadeTouchable) {
@@ -340,21 +307,5 @@ class LockscreenUserActionsViewModelTest : SysuiTestCase() {
isShadeTouchable = isShadeTouchable,
)
)
-
- val startScene by
- collectLastValue(
- (userActions?.get(Swipe.Start) as? UserActionResult.ChangeScene)
- ?.toScene
- ?.let { scene -> kosmos.sceneInteractor.resolveSceneFamily(scene) }
- ?: flowOf(null)
- )
-
- assertThat(startScene)
- .isEqualTo(
- expectedStartDestination(
- isCommunalAvailable = isCommunalAvailable,
- isShadeTouchable = isShadeTouchable,
- )
- )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java
index 69485e848a6a..b177e07d09b6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,8 +30,9 @@ import static org.mockito.Mockito.when;
import android.content.ComponentName;
import android.content.pm.PackageManager;
-import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.dream.lowlight.LowLightDreamManager;
@@ -39,6 +40,8 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.shared.condition.Condition;
import com.android.systemui.shared.condition.Monitor;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
import dagger.Lazy;
@@ -53,7 +56,8 @@ import org.mockito.MockitoAnnotations;
import java.util.Set;
@SmallTest
-@RunWith(AndroidTestingRunner.class)
+@RunWith(AndroidJUnit4.class)
+@TestableLooper.RunWithLooper()
public class LowLightMonitorTest extends SysuiTestCase {
@Mock
@@ -78,6 +82,8 @@ public class LowLightMonitorTest extends SysuiTestCase {
@Mock
private ComponentName mDreamComponent;
+ FakeExecutor mBackgroundExecutor = new FakeExecutor(new FakeSystemClock());
+
Condition mCondition = mock(Condition.class);
Set<Condition> mConditionSet = Set.of(mCondition);
@@ -91,12 +97,13 @@ public class LowLightMonitorTest extends SysuiTestCase {
when(mLazyConditions.get()).thenReturn(mConditionSet);
mLowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy,
mMonitor, mLazyConditions, mScreenLifecycle, mLogger, mDreamComponent,
- mPackageManager);
+ mPackageManager, mBackgroundExecutor);
}
@Test
public void testSetAmbientLowLightWhenInLowLight() {
mLowLightMonitor.onConditionsChanged(true);
+ mBackgroundExecutor.runAllReady();
// Verify setting low light when condition is true
verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
}
@@ -105,6 +112,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
public void testExitAmbientLowLightWhenNotInLowLight() {
mLowLightMonitor.onConditionsChanged(true);
mLowLightMonitor.onConditionsChanged(false);
+ mBackgroundExecutor.runAllReady();
// Verify ambient light toggles back to light mode regular
verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR);
}
@@ -112,6 +120,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
@Test
public void testStartMonitorLowLightConditionsWhenScreenTurnsOn() {
mLowLightMonitor.onScreenTurnedOn();
+ mBackgroundExecutor.runAllReady();
// Verify subscribing to low light conditions monitor when screen turns on.
verify(mMonitor).addSubscription(any());
@@ -125,6 +134,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
// Verify removing subscription when screen turns off.
mLowLightMonitor.onScreenTurnedOff();
+ mBackgroundExecutor.runAllReady();
verify(mMonitor).removeSubscription(token);
}
@@ -135,6 +145,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
mLowLightMonitor.onScreenTurnedOn();
mLowLightMonitor.onScreenTurnedOn();
+ mBackgroundExecutor.runAllReady();
// Verify subscription is only added once.
verify(mMonitor, times(1)).addSubscription(any());
}
@@ -146,6 +157,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
mLowLightMonitor.onScreenTurnedOn();
mLowLightMonitor.onScreenTurnedOn();
+ mBackgroundExecutor.runAllReady();
Set<Condition> conditions = captureConditions();
// Verify Monitor is subscribed to the expected conditions
assertThat(conditions).isEqualTo(mConditionSet);
@@ -154,7 +166,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
@Test
public void testNotUnsubscribeIfNotSubscribedWhenScreenTurnsOff() {
mLowLightMonitor.onScreenTurnedOff();
-
+ mBackgroundExecutor.runAllReady();
// Verify doesn't remove subscription since there is none.
verify(mMonitor, never()).removeSubscription(any());
}
@@ -163,6 +175,7 @@ public class LowLightMonitorTest extends SysuiTestCase {
public void testSubscribeIfScreenIsOnWhenStarting() {
when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON);
mLowLightMonitor.start();
+ mBackgroundExecutor.runAllReady();
// Verify to add subscription on start if the screen state is on
verify(mMonitor, times(1)).addSubscription(any());
}
@@ -170,9 +183,11 @@ public class LowLightMonitorTest extends SysuiTestCase {
@Test
public void testNoSubscribeIfDreamNotPresent() {
LowLightMonitor lowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy,
- mMonitor, mLazyConditions, mScreenLifecycle, mLogger, null, mPackageManager);
+ mMonitor, mLazyConditions, mScreenLifecycle, mLogger, null, mPackageManager,
+ mBackgroundExecutor);
when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON);
lowLightMonitor.start();
+ mBackgroundExecutor.runAllReady();
verify(mScreenLifecycle, never()).addObserver(any());
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 8e8867b80dc2..847044aa405e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -166,7 +166,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
mContext.getText(R.string.media_output_dialog_pairing_new).toString());
}
- @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void onBindViewHolder_bindGroup_withSessionName_verifyView() {
when(mMediaSwitchingController.getSelectedMediaDevice())
@@ -188,7 +188,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
}
- @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void onBindViewHolder_bindGroup_noSessionName_verifyView() {
when(mMediaSwitchingController.getSelectedMediaDevice())
@@ -237,7 +237,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
}
- @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void onBindViewHolder_bindConnectedRemoteDevice_verifyView() {
when(mMediaSwitchingController.getSelectableMediaDevice())
@@ -818,7 +818,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice1);
}
- @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void onBindViewHolder_hasVolumeAdjustmentRestriction_verifySeekbarDisabled() {
when(mMediaSwitchingController.getSelectedMediaDevice()).thenReturn(
@@ -935,7 +935,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
.isEqualTo(R.drawable.media_output_icon_volume);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void multipleSelectedDevices_verifySessionView() {
initializeSession();
@@ -956,7 +956,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mSeekBar.getVolume()).isEqualTo(TEST_CURRENT_VOLUME);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void multipleSelectedDevices_verifyCollapsedView() {
initializeSession();
@@ -970,7 +970,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mEndTouchArea.getVisibility()).isEqualTo(View.GONE);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void multipleSelectedDevices_expandIconClicked_verifyInitialView() {
initializeSession();
@@ -993,7 +993,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void multipleSelectedDevices_expandIconClicked_verifyCollapsedView() {
initializeSession();
@@ -1016,7 +1016,7 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void deviceCanNotBeDeselected_verifyView() {
List<MediaDevice> selectedDevices = new ArrayList<>();
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 26f5d9ea0996..0bba8bba2419 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
@@ -25,6 +25,7 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationResul
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
@@ -65,6 +66,7 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
fun setUp() {
kosmos.sceneContainerStartable.start()
kosmos.enableDualShade()
+ kosmos.runCurrent()
underTest.activateIn(testScope)
}
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 ec0596515efd..bf5f9f4872f0 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
@@ -25,6 +25,7 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationResul
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
@@ -68,6 +69,7 @@ class QuickSettingsShadeOverlayContentViewModelTest : SysuiTestCase() {
fun setUp() {
kosmos.sceneContainerStartable.start()
kosmos.enableDualShade()
+ kosmos.runCurrent()
underTest.activateIn(testScope)
}
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 fd485edec117..a0d86f27b9b8 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
@@ -30,6 +30,7 @@ import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintA
import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.data.repository.Idle
@@ -44,6 +45,8 @@ import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.shade.domain.interactor.disableDualShade
+import com.android.systemui.shade.domain.interactor.enableDualShade
import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFlagsRepository
import com.android.systemui.statusbar.disableflags.shared.model.DisableFlagsModel
import com.android.systemui.testKosmos
@@ -298,7 +301,9 @@ class SceneInteractorTest : SysuiTestCase() {
@Test
fun transitioningTo_overlayChange() =
- testScope.runTest {
+ kosmos.runTest {
+ enableDualShade()
+ runCurrent()
val transitionState =
MutableStateFlow<ObservableTransitionState>(
ObservableTransitionState.Idle(underTest.currentScene.value)
@@ -529,6 +534,8 @@ class SceneInteractorTest : SysuiTestCase() {
@Test
fun showOverlay_overlayDisabled_doesNothing() =
kosmos.runTest {
+ enableDualShade()
+ runCurrent()
val currentOverlays by collectLastValue(underTest.currentOverlays)
val disabledOverlay = Overlays.QuickSettingsShade
fakeDisableFlagsRepository.disableFlags.value =
@@ -544,6 +551,8 @@ class SceneInteractorTest : SysuiTestCase() {
@Test
fun replaceOverlay_withDisabledOverlay_doesNothing() =
kosmos.runTest {
+ enableDualShade()
+ runCurrent()
val currentOverlays by collectLastValue(underTest.currentOverlays)
val showingOverlay = Overlays.NotificationsShade
underTest.showOverlay(showingOverlay, "reason")
@@ -618,4 +627,90 @@ class SceneInteractorTest : SysuiTestCase() {
// No more active animations, not forced visible.
assertThat(isVisible).isFalse()
}
+
+ @Test(expected = IllegalStateException::class)
+ fun changeScene_toIncorrectShade_crashes() =
+ kosmos.runTest {
+ enableDualShade()
+ runCurrent()
+ underTest.changeScene(Scenes.Shade, "reason")
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun changeScene_toIncorrectQuickSettings_crashes() =
+ kosmos.runTest {
+ enableDualShade()
+ runCurrent()
+ underTest.changeScene(Scenes.QuickSettings, "reason")
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun snapToScene_toIncorrectShade_crashes() =
+ kosmos.runTest {
+ enableDualShade()
+ runCurrent()
+ underTest.snapToScene(Scenes.Shade, "reason")
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun snapToScene_toIncorrectQuickSettings_crashes() =
+ kosmos.runTest {
+ enableDualShade()
+ runCurrent()
+ underTest.changeScene(Scenes.QuickSettings, "reason")
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun showOverlay_incorrectShadeOverlay_crashes() =
+ kosmos.runTest {
+ disableDualShade()
+ runCurrent()
+ underTest.showOverlay(Overlays.NotificationsShade, "reason")
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun showOverlay_incorrectQuickSettingsOverlay_crashes() =
+ kosmos.runTest {
+ disableDualShade()
+ runCurrent()
+ underTest.showOverlay(Overlays.QuickSettingsShade, "reason")
+ }
+
+ @Test
+ fun instantlyShowOverlay() =
+ kosmos.runTest {
+ enableDualShade()
+ runCurrent()
+ val currentScene by collectLastValue(underTest.currentScene)
+ val currentOverlays by collectLastValue(underTest.currentOverlays)
+ val originalScene = currentScene
+ assertThat(currentOverlays).isEmpty()
+
+ val overlay = Overlays.NotificationsShade
+ underTest.instantlyShowOverlay(overlay, "reason")
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(originalScene)
+ assertThat(currentOverlays).contains(overlay)
+ }
+
+ @Test
+ fun instantlyHideOverlay() =
+ kosmos.runTest {
+ enableDualShade()
+ runCurrent()
+ val currentScene by collectLastValue(underTest.currentScene)
+ val currentOverlays by collectLastValue(underTest.currentOverlays)
+ val overlay = Overlays.QuickSettingsShade
+ underTest.showOverlay(overlay, "reason")
+ runCurrent()
+ val originalScene = currentScene
+ assertThat(currentOverlays).contains(overlay)
+
+ underTest.instantlyHideOverlay(overlay, "reason")
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(originalScene)
+ assertThat(currentOverlays).isEmpty()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 33733103053e..ae77ac4cc327 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -2049,9 +2049,9 @@ class SceneContainerStartableTest : SysuiTestCase() {
)
clearInvocations(centralSurfaces)
- emulateSceneTransition(
+ emulateOverlayTransition(
transitionStateFlow = transitionStateFlow,
- toScene = Scenes.Shade,
+ toOverlay = Overlays.NotificationsShade,
verifyBeforeTransition = {
verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
},
@@ -2079,9 +2079,9 @@ class SceneContainerStartableTest : SysuiTestCase() {
)
clearInvocations(centralSurfaces)
- emulateSceneTransition(
+ emulateOverlayTransition(
transitionStateFlow = transitionStateFlow,
- toScene = Scenes.QuickSettings,
+ toOverlay = Overlays.QuickSettingsShade,
verifyBeforeTransition = {
verify(centralSurfaces, never()).setInteracting(anyInt(), anyBoolean())
},
@@ -2654,22 +2654,36 @@ class SceneContainerStartableTest : SysuiTestCase() {
}
@Test
- fun handleDisableFlags() =
+ fun handleDisableFlags_singleShade() =
kosmos.runTest {
underTest.start()
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ disableDualShade()
runCurrent()
sceneInteractor.changeScene(Scenes.Shade, "reason")
- sceneInteractor.showOverlay(Overlays.NotificationsShade, "reason")
assertThat(currentScene).isEqualTo(Scenes.Shade)
- assertThat(currentOverlays).contains(Overlays.NotificationsShade)
fakeDisableFlagsRepository.disableFlags.value =
DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_NOTIFICATION_SHADE)
runCurrent()
assertThat(currentScene).isNotEqualTo(Scenes.Shade)
+ }
+
+ @Test
+ fun handleDisableFlags_dualShade() =
+ kosmos.runTest {
+ underTest.start()
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ enableDualShade()
+ runCurrent()
+ sceneInteractor.showOverlay(Overlays.NotificationsShade, "reason")
+ assertThat(currentOverlays).contains(Overlays.NotificationsShade)
+
+ fakeDisableFlagsRepository.disableFlags.value =
+ DisableFlagsModel(disable2 = StatusBarManager.DISABLE2_NOTIFICATION_SHADE)
+ runCurrent()
+
assertThat(currentOverlays).isEmpty()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 79fc999e1b50..904f5e869637 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -87,6 +87,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver;
+import com.android.systemui.keyguard.ui.transitions.BlurConfig;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel;
import com.android.systemui.kosmos.KosmosJavaAdapter;
@@ -587,7 +588,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
mPowerInteractor,
mKeyguardClockPositionAlgorithm,
mMSDLPlayer,
- mBrightnessMirrorShowingInteractor);
+ mBrightnessMirrorShowingInteractor,
+ new BlurConfig(0f, 0f));
mNotificationPanelViewController.initDependencies(
mCentralSurfaces,
null,
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 508836e3b48b..4a011c0844e5 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
@@ -781,28 +781,6 @@ class ShadeInteractorSceneContainerImplTest : SysuiTestCase() {
}
@Test
- fun collapseQuickSettingsShadeNotBypassingShade_splitShade_switchesToLockscreen() =
- testScope.runTest {
- kosmos.enableSplitShade()
- val shadeMode by collectLastValue(kosmos.shadeMode)
- val currentScene by collectLastValue(sceneInteractor.currentScene)
- val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
- assertThat(shadeMode).isEqualTo(ShadeMode.Split)
-
- sceneInteractor.changeScene(Scenes.QuickSettings, "reason")
- assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
- assertThat(currentOverlays).isEmpty()
-
- underTest.collapseQuickSettingsShade(
- loggingReason = "reason",
- bypassNotificationsShade = false,
- )
-
- assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
- assertThat(currentOverlays).isEmpty()
- }
-
- @Test
fun collapseQuickSettingsShadeBypassingShade_singleShade_switchesToLockscreen() =
testScope.runTest {
kosmos.enableSingleShade()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
index 4423426945eb..9eba410ffdb5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt
@@ -81,7 +81,7 @@ class DefaultClockProviderTest : SysuiTestCase() {
whenever(mockSmallClockView.getLayoutParams()).thenReturn(FrameLayout.LayoutParams(10, 10))
whenever(mockLargeClockView.getLayoutParams()).thenReturn(FrameLayout.LayoutParams(10, 10))
- provider = DefaultClockProvider(context, layoutInflater, resources)
+ provider = DefaultClockProvider(context, layoutInflater, resources, vibrator = null)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt
index e7b6e4d34fe8..402b53c12bda 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar
+import android.content.res.Resources
import android.view.CrossWindowBlurListeners
import android.view.SurfaceControl
import android.view.ViewRootImpl
@@ -46,6 +47,7 @@ class BlurUtilsTest : SysuiTestCase() {
@Mock lateinit var dumpManager: DumpManager
@Mock lateinit var transaction: SurfaceControl.Transaction
@Mock lateinit var crossWindowBlurListeners: CrossWindowBlurListeners
+ @Mock lateinit var resources: Resources
lateinit var blurUtils: TestableBlurUtils
@Before
@@ -109,7 +111,7 @@ class BlurUtilsTest : SysuiTestCase() {
verify(transaction).setEarlyWakeupEnd()
}
- inner class TestableBlurUtils : BlurUtils(blurConfig, crossWindowBlurListeners, dumpManager) {
+ inner class TestableBlurUtils : BlurUtils(resources, blurConfig, crossWindowBlurListeners, dumpManager) {
var blursEnabled = true
override fun supportsBlursOnWindows(): Boolean {
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 3d3178793a09..c6801f1ad9d5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/CommandQueueTest.java
@@ -460,14 +460,14 @@ public class CommandQueueTest extends SysuiTestCase {
}
@Test
- public void testonDisplayAddSystemDecorations() {
+ public void testOnDisplayAddSystemDecorations() {
mCommandQueue.onDisplayAddSystemDecorations(DEFAULT_DISPLAY);
waitForIdleSync();
verify(mCallbacks).onDisplayAddSystemDecorations(eq(DEFAULT_DISPLAY));
}
@Test
- public void testonDisplayAddSystemDecorationsForSecondaryDisplay() {
+ public void testOnDisplayAddSystemDecorationsForSecondaryDisplay() {
mCommandQueue.onDisplayAddSystemDecorations(SECONDARY_DISPLAY);
waitForIdleSync();
verify(mCallbacks).onDisplayAddSystemDecorations(eq(SECONDARY_DISPLAY));
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
deleted file mode 100644
index a64339e20f7c..000000000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.java
+++ /dev/null
@@ -1,1189 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar.notification.row;
-
-import static android.app.Notification.EXTRA_BUILDER_APPLICATION_INFO;
-import static android.app.NotificationChannel.SOCIAL_MEDIA_ID;
-import static android.app.NotificationChannel.USER_LOCKED_IMPORTANCE;
-import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
-import static android.app.NotificationManager.IMPORTANCE_LOW;
-import static android.app.NotificationManager.IMPORTANCE_MIN;
-import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED;
-import static android.print.PrintManager.PRINT_SPOOLER_PACKAGE_NAME;
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyBoolean;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.Flags;
-import android.app.INotificationManager;
-import android.app.Notification;
-import android.app.NotificationChannel;
-import android.app.NotificationChannelGroup;
-import android.app.PendingIntent;
-import android.app.Person;
-import android.content.ComponentName;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.graphics.drawable.Drawable;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
-import android.platform.test.flag.junit.SetFlagsRule;
-import android.service.notification.StatusBarNotification;
-import android.telecom.TelecomManager;
-import android.testing.TestableLooper;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.testing.UiEventLoggerFake;
-import com.android.systemui.Dependency;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.res.R;
-import com.android.systemui.statusbar.RankingBuilder;
-import com.android.systemui.statusbar.notification.AssistantFeedbackController;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.CountDownLatch;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-@TestableLooper.RunWithLooper
-public class NotificationInfoTest extends SysuiTestCase {
- private static final String TEST_PACKAGE_NAME = "test_package";
- private static final String TEST_SYSTEM_PACKAGE_NAME = PRINT_SPOOLER_PACKAGE_NAME;
- private static final int TEST_UID = 1;
- private static final String TEST_CHANNEL = "test_channel";
- private static final String TEST_CHANNEL_NAME = "TEST CHANNEL NAME";
-
- private TestableLooper mTestableLooper;
- private NotificationInfo mNotificationInfo;
- private NotificationChannel mNotificationChannel;
- private NotificationChannel mDefaultNotificationChannel;
- private NotificationChannel mClassifiedNotificationChannel;
- private StatusBarNotification mSbn;
- private NotificationEntry mEntry;
- private UiEventLoggerFake mUiEventLogger = new UiEventLoggerFake();
-
- @Rule
- public MockitoRule mockito = MockitoJUnit.rule();
- @Mock
- private MetricsLogger mMetricsLogger;
- @Mock
- private INotificationManager mMockINotificationManager;
- @Mock
- private PackageManager mMockPackageManager;
- @Mock
- private OnUserInteractionCallback mOnUserInteractionCallback;
- @Mock
- private ChannelEditorDialogController mChannelEditorDialogController;
- @Mock
- private AssistantFeedbackController mAssistantFeedbackController;
- @Mock
- private TelecomManager mTelecomManager;
-
- @Rule
- public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
-
- @Before
- public void setUp() throws Exception {
- mTestableLooper = TestableLooper.get(this);
-
- mContext.addMockSystemService(TelecomManager.class, mTelecomManager);
-
- mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper());
- // Inflate the layout
- final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
- mNotificationInfo = (NotificationInfo) layoutInflater.inflate(R.layout.notification_info,
- null);
- mNotificationInfo.setGutsParent(mock(NotificationGuts.class));
- // Our view is never attached to a window so the View#post methods in NotificationInfo never
- // get called. Setting this will skip the post and do the action immediately.
- mNotificationInfo.mSkipPost = true;
-
- // PackageManager must return a packageInfo and applicationInfo.
- final PackageInfo packageInfo = new PackageInfo();
- packageInfo.packageName = TEST_PACKAGE_NAME;
- when(mMockPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt()))
- .thenReturn(packageInfo);
- final ApplicationInfo applicationInfo = new ApplicationInfo();
- applicationInfo.uid = TEST_UID; // non-zero
- final PackageInfo systemPackageInfo = new PackageInfo();
- systemPackageInfo.packageName = TEST_SYSTEM_PACKAGE_NAME;
- when(mMockPackageManager.getPackageInfo(eq(TEST_SYSTEM_PACKAGE_NAME), anyInt()))
- .thenReturn(systemPackageInfo);
- when(mMockPackageManager.getPackageInfo(eq("android"), anyInt()))
- .thenReturn(packageInfo);
-
- ComponentName assistant = new ComponentName("package", "service");
- when(mMockINotificationManager.getAllowedNotificationAssistant()).thenReturn(assistant);
- ResolveInfo ri = new ResolveInfo();
- ri.activityInfo = new ActivityInfo();
- ri.activityInfo.packageName = assistant.getPackageName();
- ri.activityInfo.name = "activity";
- when(mMockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(List.of(ri));
-
- // Package has one channel by default.
- when(mMockINotificationManager.getNumNotificationChannelsForPackage(
- eq(TEST_PACKAGE_NAME), eq(TEST_UID), anyBoolean())).thenReturn(1);
-
- // Some test channels.
- mNotificationChannel = new NotificationChannel(
- TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW);
- mDefaultNotificationChannel = new NotificationChannel(
- NotificationChannel.DEFAULT_CHANNEL_ID, TEST_CHANNEL_NAME,
- IMPORTANCE_LOW);
- mClassifiedNotificationChannel =
- new NotificationChannel(SOCIAL_MEDIA_ID, "social", IMPORTANCE_LOW);
-
- Notification notification = new Notification();
- notification.extras.putParcelable(EXTRA_BUILDER_APPLICATION_INFO, applicationInfo);
- mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0,
- notification, UserHandle.getUserHandleForUid(TEST_UID), null, 0);
- mEntry = new NotificationEntryBuilder().setSbn(mSbn).build();
- when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(false);
- when(mAssistantFeedbackController.getInlineDescriptionResource(any()))
- .thenReturn(R.string.notification_channel_summary_automatic);
- }
-
- private void doStandardBind() throws Exception {
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger, null);
- }
-
- @Test
- public void testBindNotification_SetsTextApplicationName() throws Exception {
- when(mMockPackageManager.getApplicationLabel(any())).thenReturn("App Name");
- doStandardBind();
- final TextView textView = mNotificationInfo.findViewById(R.id.pkg_name);
- assertTrue(textView.getText().toString().contains("App Name"));
- assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.header).getVisibility());
- }
-
- @Test
- public void testBindNotification_SetsPackageIcon() throws Exception {
- final Drawable iconDrawable = mock(Drawable.class);
- when(mMockPackageManager.getApplicationIcon(any(ApplicationInfo.class)))
- .thenReturn(iconDrawable);
- doStandardBind();
- final ImageView iconView = mNotificationInfo.findViewById(R.id.pkg_icon);
- assertEquals(iconDrawable, iconView.getDrawable());
- }
-
- @Test
- public void testBindNotification_noDelegate() throws Exception {
- doStandardBind();
- final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
- assertEquals(GONE, nameView.getVisibility());
- }
-
- @Test
- public void testBindNotification_delegate() throws Exception {
- mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, "other", 0, null, TEST_UID, 0,
- new Notification(), UserHandle.CURRENT, null, 0);
- final ApplicationInfo applicationInfo = new ApplicationInfo();
- applicationInfo.uid = 7; // non-zero
- when(mMockPackageManager.getApplicationInfo(eq("other"), anyInt())).thenReturn(
- applicationInfo);
- when(mMockPackageManager.getApplicationLabel(any())).thenReturn("Other");
-
- NotificationEntry entry = new NotificationEntryBuilder().setSbn(mSbn).build();
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- entry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger, null);
- final TextView nameView = mNotificationInfo.findViewById(R.id.delegate_name);
- assertEquals(VISIBLE, nameView.getVisibility());
- assertTrue(nameView.getText().toString().contains("Proxied"));
- }
-
- @Test
- public void testBindNotification_GroupNameHiddenIfNoGroup() throws Exception {
- doStandardBind();
- final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name);
- assertEquals(GONE, groupNameView.getVisibility());
- }
-
- @Test
- public void testBindNotification_SetsGroupNameIfNonNull() throws Exception {
- mNotificationChannel.setGroup("test_group_id");
- final NotificationChannelGroup notificationChannelGroup =
- new NotificationChannelGroup("test_group_id", "Test Group Name");
- when(mMockINotificationManager.getNotificationChannelGroupForPackage(
- eq("test_group_id"), eq(TEST_PACKAGE_NAME), eq(TEST_UID)))
- .thenReturn(notificationChannelGroup);
- doStandardBind();
- final TextView groupNameView = mNotificationInfo.findViewById(R.id.group_name);
- assertEquals(View.VISIBLE, groupNameView.getVisibility());
- assertEquals("Test Group Name", groupNameView.getText());
- }
-
- @Test
- public void testBindNotification_SetsTextChannelName() throws Exception {
- doStandardBind();
- final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
- assertEquals(TEST_CHANNEL_NAME, textView.getText());
- }
-
- @Test
- public void testBindNotification_DefaultChannelDoesNotUseChannelName() throws Exception {
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mDefaultNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger, null);
- final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
- assertEquals(GONE, textView.getVisibility());
- }
-
- @Test
- public void testBindNotification_DefaultChannelUsesChannelNameIfMoreChannelsExist()
- throws Exception {
- // Package has more than one channel by default.
- when(mMockINotificationManager.getNumNotificationChannelsForPackage(
- eq(TEST_PACKAGE_NAME), eq(TEST_UID), anyBoolean())).thenReturn(10);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mDefaultNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
- final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
- assertEquals(VISIBLE, textView.getVisibility());
- }
-
- @Test
- public void testBindNotification_UnblockablePackageUsesChannelName() throws Exception {
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- true,
- true,
- mAssistantFeedbackController,
- mMetricsLogger, null);
- final TextView textView = mNotificationInfo.findViewById(R.id.channel_name);
- assertEquals(VISIBLE, textView.getVisibility());
- }
-
- @Test
- public void testBindNotification_SetsOnClickListenerForSettings() throws Exception {
- final CountDownLatch latch = new CountDownLatch(1);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- (View v, NotificationChannel c, int appUid) -> {
- assertEquals(mNotificationChannel, c);
- latch.countDown();
- },
- null,
- null,
- mUiEventLogger,
- true,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- final View settingsButton = mNotificationInfo.findViewById(R.id.info);
- settingsButton.performClick();
- // Verify that listener was triggered.
- assertEquals(0, latch.getCount());
- }
-
- @Test
- public void testBindNotification_SettingsButtonInvisibleWhenNoClickListener() throws Exception {
- doStandardBind();
- final View settingsButton = mNotificationInfo.findViewById(R.id.info);
- assertTrue(settingsButton.getVisibility() != View.VISIBLE);
- }
-
- @Test
- public void testBindNotification_SettingsButtonInvisibleWhenDeviceUnprovisioned()
- throws Exception {
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- (View v, NotificationChannel c, int appUid) -> {
- assertEquals(mNotificationChannel, c);
- },
- null,
- null,
- mUiEventLogger,
- false,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
- final View settingsButton = mNotificationInfo.findViewById(R.id.info);
- assertTrue(settingsButton.getVisibility() != View.VISIBLE);
- }
-
- @Test
- public void testBindNotification_SettingsButtonReappearsAfterSecondBind() throws Exception {
- doStandardBind();
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- (View v, NotificationChannel c, int appUid) -> { },
- null,
- null,
- mUiEventLogger,
- true,
- false,
- true,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
- final View settingsButton = mNotificationInfo.findViewById(R.id.info);
- assertEquals(View.VISIBLE, settingsButton.getVisibility());
- }
-
- @Test
- public void testBindNotification_whenAppUnblockable() throws Exception {
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- true,
- true,
- mAssistantFeedbackController,
- mMetricsLogger, null);
- final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_text);
- assertEquals(View.VISIBLE, view.getVisibility());
- assertEquals(mContext.getString(R.string.notification_unblockable_desc),
- view.getText());
- assertEquals(GONE,
- mNotificationInfo.findViewById(R.id.interruptiveness_settings).getVisibility());
- }
-
- @Test
- public void testBindNotification_whenCurrentlyInCall() throws Exception {
- when(mMockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true);
-
- Person person = new Person.Builder()
- .setName("caller")
- .build();
- Notification.Builder nb = new Notification.Builder(
- mContext, mNotificationChannel.getId())
- .setContentTitle("foo")
- .setSmallIcon(android.R.drawable.sym_def_app_icon)
- .setStyle(Notification.CallStyle.forOngoingCall(
- person, mock(PendingIntent.class)))
- .setFullScreenIntent(mock(PendingIntent.class), true)
- .addAction(new Notification.Action.Builder(null, "test", null).build());
-
- mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0,
- nb.build(), UserHandle.getUserHandleForUid(TEST_UID), null, 0);
- mEntry.setSbn(mSbn);
- doStandardBind();
- final TextView view = mNotificationInfo.findViewById(R.id.non_configurable_call_text);
- assertEquals(View.VISIBLE, view.getVisibility());
- assertEquals(mContext.getString(R.string.notification_unblockable_call_desc),
- view.getText());
- assertEquals(GONE,
- mNotificationInfo.findViewById(R.id.interruptiveness_settings).getVisibility());
- assertEquals(GONE,
- mNotificationInfo.findViewById(R.id.non_configurable_text).getVisibility());
- }
-
- @Test
- public void testBindNotification_whenCurrentlyInCall_notCall() throws Exception {
- when(mMockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true);
-
- Notification.Builder nb = new Notification.Builder(
- mContext, mNotificationChannel.getId())
- .setContentTitle("foo")
- .setSmallIcon(android.R.drawable.sym_def_app_icon)
- .setFullScreenIntent(mock(PendingIntent.class), true)
- .addAction(new Notification.Action.Builder(null, "test", null).build());
-
- mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0,
- nb.build(), UserHandle.getUserHandleForUid(TEST_UID), null, 0);
- mEntry.setSbn(mSbn);
- doStandardBind();
- assertEquals(GONE,
- mNotificationInfo.findViewById(R.id.non_configurable_call_text).getVisibility());
- assertEquals(VISIBLE,
- mNotificationInfo.findViewById(R.id.interruptiveness_settings).getVisibility());
- assertEquals(GONE,
- mNotificationInfo.findViewById(R.id.non_configurable_text).getVisibility());
- }
-
- @Test
- public void testBindNotification_automaticIsVisible() throws Exception {
- when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(true);
- doStandardBind();
- assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic).getVisibility());
- assertEquals(VISIBLE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility());
- }
-
- @Test
- public void testBindNotification_automaticIsGone() throws Exception {
- doStandardBind();
- assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic).getVisibility());
- assertEquals(GONE, mNotificationInfo.findViewById(R.id.automatic_summary).getVisibility());
- }
-
- @Test
- public void testBindNotification_automaticIsSelected() throws Exception {
- when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(true);
- mNotificationChannel.unlockFields(USER_LOCKED_IMPORTANCE);
- doStandardBind();
- assertTrue(mNotificationInfo.findViewById(R.id.automatic).isSelected());
- }
-
- @Test
- public void testBindNotification_alertIsSelected() throws Exception {
- doStandardBind();
- assertTrue(mNotificationInfo.findViewById(R.id.alert).isSelected());
- }
-
- @Test
- public void testBindNotification_silenceIsSelected() throws Exception {
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
- assertTrue(mNotificationInfo.findViewById(R.id.silence).isSelected());
- }
-
- @Test
- public void testBindNotification_DoesNotUpdateNotificationChannel() throws Exception {
- doStandardBind();
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), any());
- }
-
- @Test
- public void testBindNotification_LogsOpen() throws Exception {
- doStandardBind();
- assertEquals(1, mUiEventLogger.numLogs());
- assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
- mUiEventLogger.eventId(0));
- }
-
- @Test
- public void testDoesNotUpdateNotificationChannelAfterImportanceChanged() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.alert).performClick();
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), any());
- }
-
- @Test
- public void testDoesNotUpdateNotificationChannelAfterImportanceChangedSilenced()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
- doStandardBind();
-
- mNotificationInfo.findViewById(R.id.silence).performClick();
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), any());
- }
-
- @Test
- public void testDoesNotUpdateNotificationChannelAfterImportanceChangedAutomatic()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
- doStandardBind();
-
- mNotificationInfo.findViewById(R.id.automatic).performClick();
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), any());
- }
-
- @Test
- public void testHandleCloseControls_persistAutomatic()
- throws Exception {
- when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(true);
- mNotificationChannel.unlockFields(USER_LOCKED_IMPORTANCE);
- doStandardBind();
-
- mNotificationInfo.handleCloseControls(true, false);
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, times(1)).unlockNotificationChannel(
- anyString(), eq(TEST_UID), any());
- }
-
- @Test
- public void testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnchanged()
- throws Exception {
- int originalImportance = mNotificationChannel.getImportance();
- doStandardBind();
-
- mNotificationInfo.handleCloseControls(true, false);
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), any());
- assertEquals(originalImportance, mNotificationChannel.getImportance());
-
- assertEquals(2, mUiEventLogger.numLogs());
- assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
- mUiEventLogger.eventId(0));
- // The SAVE_IMPORTANCE event is logged whenever importance is saved, even if unchanged.
- assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.getId(),
- mUiEventLogger.eventId(1));
- }
-
- @Test
- public void testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnspecified()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_UNSPECIFIED);
- doStandardBind();
-
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), any());
- assertEquals(IMPORTANCE_UNSPECIFIED, mNotificationChannel.getImportance());
- }
-
- @Test
- public void testSilenceCallsUpdateNotificationChannel() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
- doStandardBind();
-
- mNotificationInfo.findViewById(R.id.silence).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- ArgumentCaptor<NotificationChannel> updated =
- ArgumentCaptor.forClass(NotificationChannel.class);
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), updated.capture());
- assertTrue((updated.getValue().getUserLockedFields()
- & USER_LOCKED_IMPORTANCE) != 0);
- assertEquals(IMPORTANCE_LOW, updated.getValue().getImportance());
-
- assertEquals(2, mUiEventLogger.numLogs());
- assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
- mUiEventLogger.eventId(0));
- assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.getId(),
- mUiEventLogger.eventId(1));
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testUnSilenceCallsUpdateNotificationChannel() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.alert).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- ArgumentCaptor<NotificationChannel> updated =
- ArgumentCaptor.forClass(NotificationChannel.class);
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), updated.capture());
- assertTrue((updated.getValue().getUserLockedFields()
- & USER_LOCKED_IMPORTANCE) != 0);
- assertEquals(IMPORTANCE_DEFAULT, updated.getValue().getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testAutomaticUnlocksUserImportance() throws Exception {
- when(mAssistantFeedbackController.isFeedbackEnabled()).thenReturn(true);
- mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
- mNotificationChannel.lockFields(USER_LOCKED_IMPORTANCE);
- doStandardBind();
-
- mNotificationInfo.findViewById(R.id.automatic).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, times(1)).unlockNotificationChannel(
- anyString(), eq(TEST_UID), any());
- assertEquals(IMPORTANCE_DEFAULT, mNotificationChannel.getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_UNSPECIFIED);
- doStandardBind();
-
- mNotificationInfo.findViewById(R.id.silence).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- ArgumentCaptor<NotificationChannel> updated =
- ArgumentCaptor.forClass(NotificationChannel.class);
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), updated.capture());
- assertTrue((updated.getValue().getUserLockedFields()
- & USER_LOCKED_IMPORTANCE) != 0);
- assertEquals(IMPORTANCE_LOW, updated.getValue().getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testSilenceCallsUpdateNotificationChannel_channelImportanceMin()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_MIN);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- assertEquals(mContext.getString(R.string.inline_done_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- mNotificationInfo.findViewById(R.id.silence).performClick();
- assertEquals(mContext.getString(R.string.inline_done_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- ArgumentCaptor<NotificationChannel> updated =
- ArgumentCaptor.forClass(NotificationChannel.class);
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), updated.capture());
- assertTrue((updated.getValue().getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0);
- assertEquals(IMPORTANCE_MIN, updated.getValue().getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testSilence_closeGutsThenTryToSave() throws RemoteException {
- mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.silence).performClick();
- mNotificationInfo.handleCloseControls(false, false);
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
-
- assertEquals(IMPORTANCE_DEFAULT, mNotificationChannel.getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testAlertCallsUpdateNotificationChannel_channelImportanceMin()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_MIN);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- assertEquals(mContext.getString(R.string.inline_done_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- mNotificationInfo.findViewById(R.id.alert).performClick();
- assertEquals(mContext.getString(R.string.inline_ok_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- ArgumentCaptor<NotificationChannel> updated =
- ArgumentCaptor.forClass(NotificationChannel.class);
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), updated.capture());
- assertTrue((updated.getValue().getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0);
- assertEquals(IMPORTANCE_DEFAULT, updated.getValue().getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testAdjustImportanceTemporarilyAllowsReordering() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_DEFAULT);
- doStandardBind();
-
- mNotificationInfo.findViewById(R.id.silence).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- verify(mOnUserInteractionCallback).onImportanceChanged(mEntry);
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testDoneText()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- assertEquals(mContext.getString(R.string.inline_done_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- mNotificationInfo.findViewById(R.id.alert).performClick();
- assertEquals(mContext.getString(R.string.inline_ok_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- mNotificationInfo.findViewById(R.id.silence).performClick();
- assertEquals(mContext.getString(R.string.inline_done_button),
- ((TextView) mNotificationInfo.findViewById(R.id.done)).getText());
- }
-
- @Test
- public void testUnSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified()
- throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.alert).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- ArgumentCaptor<NotificationChannel> updated =
- ArgumentCaptor.forClass(NotificationChannel.class);
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- anyString(), eq(TEST_UID), updated.capture());
- assertTrue((updated.getValue().getUserLockedFields()
- & USER_LOCKED_IMPORTANCE) != 0);
- assertEquals(IMPORTANCE_DEFAULT, updated.getValue().getImportance());
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testCloseControlsDoesNotUpdateIfSaveIsFalse() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.alert).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mNotificationInfo.handleCloseControls(false, false);
-
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
- eq(TEST_PACKAGE_NAME), eq(TEST_UID), eq(mNotificationChannel));
-
- assertEquals(1, mUiEventLogger.numLogs());
- assertEquals(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.getId(),
- mUiEventLogger.eventId(0));
- }
-
- @Test
- public void testCloseControlsUpdatesWhenCheckSaveListenerUsesCallback() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.alert).performClick();
- mNotificationInfo.findViewById(R.id.done).performClick();
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, never()).updateNotificationChannelForPackage(
- eq(TEST_PACKAGE_NAME), eq(TEST_UID), eq(mNotificationChannel));
-
- mNotificationInfo.handleCloseControls(true, false);
-
- mTestableLooper.processAllMessages();
- verify(mMockINotificationManager, times(1)).updateNotificationChannelForPackage(
- eq(TEST_PACKAGE_NAME), eq(TEST_UID), eq(mNotificationChannel));
- }
-
- @Test
- public void testCloseControls_withoutHittingApply() throws Exception {
- mNotificationChannel.setImportance(IMPORTANCE_LOW);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- mNotificationInfo.findViewById(R.id.alert).performClick();
-
- assertFalse(mNotificationInfo.shouldBeSavedOnClose());
- }
-
- @Test
- public void testWillBeRemovedReturnsFalse() throws Exception {
- assertFalse(mNotificationInfo.willBeRemoved());
-
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mNotificationChannel,
- mEntry,
- null,
- null,
- null,
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- assertFalse(mNotificationInfo.willBeRemoved());
- }
-
-
- @Test
- @DisableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
- public void testBindNotification_HidesFeedbackLink_flagOff() throws Exception {
- doStandardBind();
- assertEquals(GONE, mNotificationInfo.findViewById(R.id.feedback).getVisibility());
- }
-
- @Test
- @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
- public void testBindNotification_SetsFeedbackLink_isReservedChannel() throws RemoteException {
- mEntry.setRanking(new RankingBuilder(mEntry.getRanking())
- .setSummarization("something").build());
- final CountDownLatch latch = new CountDownLatch(1);
- mNotificationInfo.bindNotification(
- mMockPackageManager,
- mMockINotificationManager,
- mOnUserInteractionCallback,
- mChannelEditorDialogController,
- TEST_PACKAGE_NAME,
- mClassifiedNotificationChannel,
- mEntry,
- null,
- null,
- (View v, Intent intent) -> {
- latch.countDown();
- },
- mUiEventLogger,
- true,
- false,
- false,
- mAssistantFeedbackController,
- mMetricsLogger,
- null);
-
- final View feedback = mNotificationInfo.findViewById(R.id.feedback);
- assertEquals(VISIBLE, feedback.getVisibility());
- feedback.performClick();
- // Verify that listener was triggered.
- assertEquals(0, latch.getCount());
- }
-
- @Test
- @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
- public void testBindNotification_hidesFeedbackLink_notReservedChannel() throws Exception {
- doStandardBind();
-
- assertEquals(GONE, mNotificationInfo.findViewById(R.id.feedback).getVisibility());
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt
new file mode 100644
index 000000000000..2945fa98caad
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationInfoTest.kt
@@ -0,0 +1,910 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Flags
+import android.app.INotificationManager
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationChannel.SOCIAL_MEDIA_ID
+import android.app.NotificationChannelGroup
+import android.app.NotificationManager
+import android.app.NotificationManager.IMPORTANCE_LOW
+import android.app.PendingIntent
+import android.app.Person
+import android.content.ComponentName
+import android.content.Intent
+import android.content.mockPackageManager
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.graphics.drawable.Drawable
+import android.os.RemoteException
+import android.os.UserHandle
+import android.os.testableLooper
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.print.PrintManager
+import android.service.notification.StatusBarNotification
+import android.telecom.TelecomManager
+import android.testing.TestableLooper.RunWithLooper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.metricsLogger
+import com.android.internal.logging.uiEventLoggerFake
+import com.android.systemui.Dependency
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.RankingBuilder
+import com.android.systemui.statusbar.notification.AssistantFeedbackController
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.telecom.telecomManager
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+class NotificationInfoTest : SysuiTestCase() {
+ private val kosmos = Kosmos().also { it.testCase = this }
+
+ private lateinit var underTest: NotificationInfo
+ private lateinit var notificationChannel: NotificationChannel
+ private lateinit var defaultNotificationChannel: NotificationChannel
+ private lateinit var classifiedNotificationChannel: NotificationChannel
+ private lateinit var sbn: StatusBarNotification
+ private lateinit var entry: NotificationEntry
+
+ private val mockPackageManager = kosmos.mockPackageManager
+ private val uiEventLogger = kosmos.uiEventLoggerFake
+ private val testableLooper by lazy { kosmos.testableLooper }
+
+ private val onUserInteractionCallback = mock<OnUserInteractionCallback>()
+ private val mockINotificationManager = mock<INotificationManager>()
+ private val channelEditorDialogController = mock<ChannelEditorDialogController>()
+ private val assistantFeedbackController = mock<AssistantFeedbackController>()
+
+ @Before
+ fun setUp() {
+ mContext.addMockSystemService(TelecomManager::class.java, kosmos.telecomManager)
+
+ mDependency.injectTestDependency(Dependency.BG_LOOPER, testableLooper.looper)
+
+ // Inflate the layout
+ val inflater = LayoutInflater.from(mContext)
+ underTest = inflater.inflate(R.layout.notification_info, null) as NotificationInfo
+
+ underTest.setGutsParent(mock<NotificationGuts>())
+
+ // Our view is never attached to a window so the View#post methods in NotificationInfo never
+ // get called. Setting this will skip the post and do the action immediately.
+ underTest.mSkipPost = true
+
+ // PackageManager must return a packageInfo and applicationInfo.
+ val packageInfo = PackageInfo()
+ packageInfo.packageName = TEST_PACKAGE_NAME
+ whenever(mockPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt()))
+ .thenReturn(packageInfo)
+ val applicationInfo = ApplicationInfo()
+ applicationInfo.uid = TEST_UID // non-zero
+ val systemPackageInfo = PackageInfo()
+ systemPackageInfo.packageName = TEST_SYSTEM_PACKAGE_NAME
+ whenever(mockPackageManager.getPackageInfo(eq(TEST_SYSTEM_PACKAGE_NAME), anyInt()))
+ .thenReturn(systemPackageInfo)
+ whenever(mockPackageManager.getPackageInfo(eq("android"), anyInt())).thenReturn(packageInfo)
+
+ val assistant = ComponentName("package", "service")
+ whenever(mockINotificationManager.allowedNotificationAssistant).thenReturn(assistant)
+ val ri = ResolveInfo()
+ ri.activityInfo = ActivityInfo()
+ ri.activityInfo.packageName = assistant.packageName
+ ri.activityInfo.name = "activity"
+ whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(listOf(ri))
+
+ // Package has one channel by default.
+ whenever(
+ mockINotificationManager.getNumNotificationChannelsForPackage(
+ eq(TEST_PACKAGE_NAME),
+ eq(TEST_UID),
+ anyBoolean(),
+ )
+ )
+ .thenReturn(1)
+
+ // Some test channels.
+ notificationChannel = NotificationChannel(TEST_CHANNEL, TEST_CHANNEL_NAME, IMPORTANCE_LOW)
+ defaultNotificationChannel =
+ NotificationChannel(
+ NotificationChannel.DEFAULT_CHANNEL_ID,
+ TEST_CHANNEL_NAME,
+ IMPORTANCE_LOW,
+ )
+ classifiedNotificationChannel =
+ NotificationChannel(SOCIAL_MEDIA_ID, "social", IMPORTANCE_LOW)
+
+ val notification = Notification()
+ notification.extras.putParcelable(
+ Notification.EXTRA_BUILDER_APPLICATION_INFO,
+ applicationInfo,
+ )
+ sbn =
+ StatusBarNotification(
+ TEST_PACKAGE_NAME,
+ TEST_PACKAGE_NAME,
+ 0,
+ null,
+ TEST_UID,
+ 0,
+ notification,
+ UserHandle.getUserHandleForUid(TEST_UID),
+ null,
+ 0,
+ )
+ entry = NotificationEntryBuilder().setSbn(sbn).build()
+ whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(false)
+ whenever(assistantFeedbackController.getInlineDescriptionResource(any()))
+ .thenReturn(R.string.notification_channel_summary_automatic)
+ }
+
+ @Test
+ fun testBindNotification_SetsTextApplicationName() {
+ whenever(mockPackageManager.getApplicationLabel(any())).thenReturn("App Name")
+ bindNotification()
+ val textView = underTest.findViewById<TextView>(R.id.pkg_name)
+ assertThat(textView.text.toString()).contains("App Name")
+ assertThat(underTest.findViewById<View>(R.id.header).visibility).isEqualTo(VISIBLE)
+ }
+
+ @Test
+ fun testBindNotification_SetsPackageIcon() {
+ val iconDrawable = mock<Drawable>()
+ whenever(mockPackageManager.getApplicationIcon(any<ApplicationInfo>()))
+ .thenReturn(iconDrawable)
+ bindNotification()
+ val iconView = underTest.findViewById<ImageView>(R.id.pkg_icon)
+ assertThat(iconView.drawable).isEqualTo(iconDrawable)
+ }
+
+ @Test
+ fun testBindNotification_noDelegate() {
+ bindNotification()
+ val nameView = underTest.findViewById<TextView>(R.id.delegate_name)
+ assertThat(nameView.visibility).isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_delegate() {
+ sbn =
+ StatusBarNotification(
+ TEST_PACKAGE_NAME,
+ "other",
+ 0,
+ null,
+ TEST_UID,
+ 0,
+ Notification(),
+ UserHandle.CURRENT,
+ null,
+ 0,
+ )
+ val applicationInfo = ApplicationInfo()
+ applicationInfo.uid = 7 // non-zero
+ whenever(mockPackageManager.getApplicationInfo(eq("other"), anyInt()))
+ .thenReturn(applicationInfo)
+ whenever(mockPackageManager.getApplicationLabel(any())).thenReturn("Other")
+
+ val entry = NotificationEntryBuilder().setSbn(sbn).build()
+ bindNotification(entry = entry)
+ val nameView = underTest.findViewById<TextView>(R.id.delegate_name)
+ assertThat(nameView.visibility).isEqualTo(VISIBLE)
+ assertThat(nameView.text.toString()).contains("Proxied")
+ }
+
+ @Test
+ fun testBindNotification_GroupNameHiddenIfNoGroup() {
+ bindNotification()
+ val groupNameView = underTest.findViewById<TextView>(R.id.group_name)
+ assertThat(groupNameView.visibility).isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_SetsGroupNameIfNonNull() {
+ notificationChannel.group = "test_group_id"
+ val notificationChannelGroup = NotificationChannelGroup("test_group_id", "Test Group Name")
+ whenever(
+ mockINotificationManager.getNotificationChannelGroupForPackage(
+ eq("test_group_id"),
+ eq(TEST_PACKAGE_NAME),
+ eq(TEST_UID),
+ )
+ )
+ .thenReturn(notificationChannelGroup)
+ bindNotification()
+ val groupNameView = underTest.findViewById<TextView>(R.id.group_name)
+ assertThat(groupNameView.visibility).isEqualTo(VISIBLE)
+ assertThat(groupNameView.text).isEqualTo("Test Group Name")
+ }
+
+ @Test
+ fun testBindNotification_SetsTextChannelName() {
+ bindNotification()
+ val textView = underTest.findViewById<TextView>(R.id.channel_name)
+ assertThat(textView.text).isEqualTo(TEST_CHANNEL_NAME)
+ }
+
+ @Test
+ fun testBindNotification_DefaultChannelDoesNotUseChannelName() {
+ bindNotification(notificationChannel = defaultNotificationChannel)
+ val textView = underTest.findViewById<TextView>(R.id.channel_name)
+ assertThat(textView.visibility).isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_DefaultChannelUsesChannelNameIfMoreChannelsExist() {
+ // Package has more than one channel by default.
+ whenever(
+ mockINotificationManager.getNumNotificationChannelsForPackage(
+ eq(TEST_PACKAGE_NAME),
+ eq(TEST_UID),
+ anyBoolean(),
+ )
+ )
+ .thenReturn(10)
+ bindNotification(notificationChannel = defaultNotificationChannel)
+ val textView = underTest.findViewById<TextView>(R.id.channel_name)
+ assertThat(textView.visibility).isEqualTo(VISIBLE)
+ }
+
+ @Test
+ fun testBindNotification_UnblockablePackageUsesChannelName() {
+ bindNotification(isNonblockable = true)
+ val textView = underTest.findViewById<TextView>(R.id.channel_name)
+ assertThat(textView.visibility).isEqualTo(VISIBLE)
+ }
+
+ @Test
+ fun testBindNotification_SetsOnClickListenerForSettings() {
+ val latch = CountDownLatch(1)
+ bindNotification(
+ onSettingsClick = { _: View?, c: NotificationChannel?, _: Int ->
+ assertThat(c).isEqualTo(notificationChannel)
+ latch.countDown()
+ }
+ )
+
+ val settingsButton = underTest.findViewById<View>(R.id.info)
+ settingsButton.performClick()
+ // Verify that listener was triggered.
+ assertThat(latch.count).isEqualTo(0)
+ }
+
+ @Test
+ fun testBindNotification_SettingsButtonInvisibleWhenNoClickListener() {
+ bindNotification()
+ val settingsButton = underTest.findViewById<View>(R.id.info)
+ assertThat(settingsButton.visibility != VISIBLE).isTrue()
+ }
+
+ @Test
+ fun testBindNotification_SettingsButtonInvisibleWhenDeviceUnprovisioned() {
+ bindNotification(
+ onSettingsClick = { _: View?, c: NotificationChannel?, _: Int ->
+ assertThat(c).isEqualTo(notificationChannel)
+ },
+ isDeviceProvisioned = false,
+ )
+ val settingsButton = underTest.findViewById<View>(R.id.info)
+ assertThat(settingsButton.visibility != VISIBLE).isTrue()
+ }
+
+ @Test
+ fun testBindNotification_SettingsButtonReappearsAfterSecondBind() {
+ bindNotification()
+ bindNotification(onSettingsClick = { _: View?, _: NotificationChannel?, _: Int -> })
+ val settingsButton = underTest.findViewById<View>(R.id.info)
+ assertThat(settingsButton.visibility).isEqualTo(VISIBLE)
+ }
+
+ @Test
+ fun testBindNotification_whenAppUnblockable() {
+ bindNotification(isNonblockable = true)
+ val view = underTest.findViewById<TextView>(R.id.non_configurable_text)
+ assertThat(view.visibility).isEqualTo(VISIBLE)
+ assertThat(view.text).isEqualTo(mContext.getString(R.string.notification_unblockable_desc))
+ assertThat(underTest.findViewById<View>(R.id.interruptiveness_settings).visibility)
+ .isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_whenCurrentlyInCall() {
+ whenever(mockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true)
+
+ val person = Person.Builder().setName("caller").build()
+ val nb =
+ Notification.Builder(mContext, notificationChannel.id)
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setStyle(Notification.CallStyle.forOngoingCall(person, mock<PendingIntent>()))
+ .setFullScreenIntent(mock<PendingIntent>(), true)
+ .addAction(Notification.Action.Builder(null, "test", null).build())
+
+ sbn =
+ StatusBarNotification(
+ TEST_PACKAGE_NAME,
+ TEST_PACKAGE_NAME,
+ 0,
+ null,
+ TEST_UID,
+ 0,
+ nb.build(),
+ UserHandle.getUserHandleForUid(TEST_UID),
+ null,
+ 0,
+ )
+ entry.sbn = sbn
+ bindNotification()
+ val view = underTest.findViewById<TextView>(R.id.non_configurable_call_text)
+ assertThat(view.visibility).isEqualTo(VISIBLE)
+ assertThat(view.text)
+ .isEqualTo(mContext.getString(R.string.notification_unblockable_call_desc))
+ assertThat(underTest.findViewById<View>(R.id.interruptiveness_settings).visibility)
+ .isEqualTo(GONE)
+ assertThat(underTest.findViewById<View>(R.id.non_configurable_text).visibility)
+ .isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_whenCurrentlyInCall_notCall() {
+ whenever(mockINotificationManager.isInCall(anyString(), anyInt())).thenReturn(true)
+
+ val nb =
+ Notification.Builder(mContext, notificationChannel.id)
+ .setContentTitle("foo")
+ .setSmallIcon(android.R.drawable.sym_def_app_icon)
+ .setFullScreenIntent(mock<PendingIntent>(), true)
+ .addAction(Notification.Action.Builder(null, "test", null).build())
+
+ sbn =
+ StatusBarNotification(
+ TEST_PACKAGE_NAME,
+ TEST_PACKAGE_NAME,
+ 0,
+ null,
+ TEST_UID,
+ 0,
+ nb.build(),
+ UserHandle.getUserHandleForUid(TEST_UID),
+ null,
+ 0,
+ )
+ entry.sbn = sbn
+ bindNotification()
+ assertThat(underTest.findViewById<View>(R.id.non_configurable_call_text).visibility)
+ .isEqualTo(GONE)
+ assertThat(underTest.findViewById<View>(R.id.interruptiveness_settings).visibility)
+ .isEqualTo(VISIBLE)
+ assertThat(underTest.findViewById<View>(R.id.non_configurable_text).visibility)
+ .isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_automaticIsVisible() {
+ whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true)
+ bindNotification()
+ assertThat(underTest.findViewById<View>(R.id.automatic).visibility).isEqualTo(VISIBLE)
+ assertThat(underTest.findViewById<View>(R.id.automatic_summary).visibility)
+ .isEqualTo(VISIBLE)
+ }
+
+ @Test
+ fun testBindNotification_automaticIsGone() {
+ bindNotification()
+ assertThat(underTest.findViewById<View>(R.id.automatic).visibility).isEqualTo(GONE)
+ assertThat(underTest.findViewById<View>(R.id.automatic_summary).visibility).isEqualTo(GONE)
+ }
+
+ @Test
+ fun testBindNotification_automaticIsSelected() {
+ whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true)
+ notificationChannel.unlockFields(NotificationChannel.USER_LOCKED_IMPORTANCE)
+ bindNotification()
+ assertThat(underTest.findViewById<View>(R.id.automatic).isSelected).isTrue()
+ }
+
+ @Test
+ fun testBindNotification_alertIsSelected() {
+ bindNotification()
+ assertThat(underTest.findViewById<View>(R.id.alert).isSelected).isTrue()
+ }
+
+ @Test
+ fun testBindNotification_silenceIsSelected() {
+ bindNotification(wasShownHighPriority = false)
+ assertThat(underTest.findViewById<View>(R.id.silence).isSelected).isTrue()
+ }
+
+ @Test
+ fun testBindNotification_DoesNotUpdateNotificationChannel() {
+ bindNotification()
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager, never())
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
+ }
+
+ @Test
+ fun testBindNotification_LogsOpen() {
+ bindNotification()
+ assertThat(uiEventLogger.numLogs()).isEqualTo(1)
+ assertThat(uiEventLogger.eventId(0))
+ .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id)
+ }
+
+ @Test
+ fun testDoesNotUpdateNotificationChannelAfterImportanceChanged() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.alert).performClick()
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager, never())
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
+ }
+
+ @Test
+ fun testDoesNotUpdateNotificationChannelAfterImportanceChangedSilenced() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
+ bindNotification()
+
+ underTest.findViewById<View>(R.id.silence).performClick()
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager, never())
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
+ }
+
+ @Test
+ fun testDoesNotUpdateNotificationChannelAfterImportanceChangedAutomatic() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
+ bindNotification()
+
+ underTest.findViewById<View>(R.id.automatic).performClick()
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager, never())
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
+ }
+
+ @Test
+ fun testHandleCloseControls_persistAutomatic() {
+ whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true)
+ notificationChannel.unlockFields(NotificationChannel.USER_LOCKED_IMPORTANCE)
+ bindNotification()
+
+ underTest.handleCloseControls(true, false)
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager).unlockNotificationChannel(anyString(), eq(TEST_UID), any())
+ }
+
+ @Test
+ fun testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnchanged() {
+ val originalImportance = notificationChannel.importance
+ bindNotification()
+
+ underTest.handleCloseControls(true, false)
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
+ assertThat(notificationChannel.importance).isEqualTo(originalImportance)
+
+ assertThat(uiEventLogger.numLogs()).isEqualTo(2)
+ assertThat(uiEventLogger.eventId(0))
+ .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id)
+ // The SAVE_IMPORTANCE event is logged whenever importance is saved, even if unchanged.
+ assertThat(uiEventLogger.eventId(1))
+ .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.id)
+ }
+
+ @Test
+ fun testHandleCloseControls_DoesNotUpdateNotificationChannelIfUnspecified() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_UNSPECIFIED
+ bindNotification()
+
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), any())
+ assertThat(notificationChannel.importance)
+ .isEqualTo(NotificationManager.IMPORTANCE_UNSPECIFIED)
+ }
+
+ @Test
+ fun testSilenceCallsUpdateNotificationChannel() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
+ bindNotification()
+
+ underTest.findViewById<View>(R.id.silence).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ val updated = argumentCaptor<NotificationChannel>()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
+ assertThat(
+ updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
+ )
+ .isNotEqualTo(0)
+ assertThat(updated.firstValue.importance).isEqualTo(IMPORTANCE_LOW)
+
+ assertThat(uiEventLogger.numLogs()).isEqualTo(2)
+ assertThat(uiEventLogger.eventId(0))
+ .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id)
+ assertThat(uiEventLogger.eventId(1))
+ .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_SAVE_IMPORTANCE.id)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testUnSilenceCallsUpdateNotificationChannel() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.alert).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ val updated = argumentCaptor<NotificationChannel>()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
+ assertThat(
+ updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
+ )
+ .isNotEqualTo(0)
+ assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testAutomaticUnlocksUserImportance() {
+ whenever(assistantFeedbackController.isFeedbackEnabled).thenReturn(true)
+ notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
+ notificationChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE)
+ bindNotification()
+
+ underTest.findViewById<View>(R.id.automatic).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager).unlockNotificationChannel(anyString(), eq(TEST_UID), any())
+ assertThat(notificationChannel.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_UNSPECIFIED
+ bindNotification()
+
+ underTest.findViewById<View>(R.id.silence).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ val updated = argumentCaptor<NotificationChannel>()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
+ assertThat(
+ updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
+ )
+ .isNotEqualTo(0)
+ assertThat(updated.firstValue.importance).isEqualTo(IMPORTANCE_LOW)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testSilenceCallsUpdateNotificationChannel_channelImportanceMin() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_MIN
+ bindNotification(wasShownHighPriority = false)
+
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_done_button))
+ underTest.findViewById<View>(R.id.silence).performClick()
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_done_button))
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ val updated = argumentCaptor<NotificationChannel>()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
+ assertThat(
+ updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
+ )
+ .isNotEqualTo(0)
+ assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_MIN)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ @Throws(RemoteException::class)
+ fun testSilence_closeGutsThenTryToSave() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.silence).performClick()
+ underTest.handleCloseControls(false, false)
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+
+ assertThat(notificationChannel.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testAlertCallsUpdateNotificationChannel_channelImportanceMin() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_MIN
+ bindNotification(wasShownHighPriority = false)
+
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_done_button))
+ underTest.findViewById<View>(R.id.alert).performClick()
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_ok_button))
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ val updated = argumentCaptor<NotificationChannel>()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
+ assertThat(
+ updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
+ )
+ .isNotEqualTo(0)
+ assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testAdjustImportanceTemporarilyAllowsReordering() {
+ notificationChannel.importance = NotificationManager.IMPORTANCE_DEFAULT
+ bindNotification()
+
+ underTest.findViewById<View>(R.id.silence).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ verify(onUserInteractionCallback).onImportanceChanged(entry)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testDoneText() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_done_button))
+ underTest.findViewById<View>(R.id.alert).performClick()
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_ok_button))
+ underTest.findViewById<View>(R.id.silence).performClick()
+ assertThat((underTest.findViewById<View>(R.id.done) as TextView).text)
+ .isEqualTo(mContext.getString(R.string.inline_done_button))
+ }
+
+ @Test
+ fun testUnSilenceCallsUpdateNotificationChannel_channelImportanceUnspecified() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.alert).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ val updated = argumentCaptor<NotificationChannel>()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(anyString(), eq(TEST_UID), updated.capture())
+ assertThat(
+ updated.firstValue.userLockedFields and NotificationChannel.USER_LOCKED_IMPORTANCE
+ )
+ .isNotEqualTo(0)
+ assertThat(updated.firstValue.importance).isEqualTo(NotificationManager.IMPORTANCE_DEFAULT)
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testCloseControlsDoesNotUpdateIfSaveIsFalse() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.alert).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ underTest.handleCloseControls(false, false)
+
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager, never())
+ .updateNotificationChannelForPackage(
+ eq(TEST_PACKAGE_NAME),
+ eq(TEST_UID),
+ eq(notificationChannel),
+ )
+
+ assertThat(uiEventLogger.numLogs()).isEqualTo(1)
+ assertThat(uiEventLogger.eventId(0))
+ .isEqualTo(NotificationControlsEvent.NOTIFICATION_CONTROLS_OPEN.id)
+ }
+
+ @Test
+ fun testCloseControlsUpdatesWhenCheckSaveListenerUsesCallback() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.alert).performClick()
+ underTest.findViewById<View>(R.id.done).performClick()
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager, never())
+ .updateNotificationChannelForPackage(
+ eq(TEST_PACKAGE_NAME),
+ eq(TEST_UID),
+ eq(notificationChannel),
+ )
+
+ underTest.handleCloseControls(true, false)
+
+ testableLooper.processAllMessages()
+ verify(mockINotificationManager)
+ .updateNotificationChannelForPackage(
+ eq(TEST_PACKAGE_NAME),
+ eq(TEST_UID),
+ eq(notificationChannel),
+ )
+ }
+
+ @Test
+ fun testCloseControls_withoutHittingApply() {
+ notificationChannel.importance = IMPORTANCE_LOW
+ bindNotification(wasShownHighPriority = false)
+
+ underTest.findViewById<View>(R.id.alert).performClick()
+
+ assertThat(underTest.shouldBeSavedOnClose()).isFalse()
+ }
+
+ @Test
+ fun testWillBeRemovedReturnsFalse() {
+ assertThat(underTest.willBeRemoved()).isFalse()
+
+ bindNotification(wasShownHighPriority = false)
+
+ assertThat(underTest.willBeRemoved()).isFalse()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
+ @Throws(Exception::class)
+ fun testBindNotification_HidesFeedbackLink_flagOff() {
+ bindNotification()
+ assertThat(underTest.findViewById<View>(R.id.feedback).visibility).isEqualTo(GONE)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
+ @Throws(RemoteException::class)
+ fun testBindNotification_SetsFeedbackLink_isReservedChannel() {
+ entry.setRanking(RankingBuilder(entry.ranking).setSummarization("something").build())
+ val latch = CountDownLatch(1)
+ bindNotification(
+ notificationChannel = classifiedNotificationChannel,
+ onFeedbackClickListener = { _: View?, _: Intent? -> latch.countDown() },
+ wasShownHighPriority = false,
+ )
+
+ val feedback: View = underTest.findViewById(R.id.feedback)
+ assertThat(feedback.visibility).isEqualTo(VISIBLE)
+ feedback.performClick()
+ // Verify that listener was triggered.
+ assertThat(latch.count).isEqualTo(0)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
+ @Throws(Exception::class)
+ fun testBindNotification_hidesFeedbackLink_notReservedChannel() {
+ bindNotification()
+
+ assertThat(underTest.findViewById<View>(R.id.feedback).visibility).isEqualTo(GONE)
+ }
+
+ private fun bindNotification(
+ pm: PackageManager = this.mockPackageManager,
+ iNotificationManager: INotificationManager = this.mockINotificationManager,
+ onUserInteractionCallback: OnUserInteractionCallback = this.onUserInteractionCallback,
+ channelEditorDialogController: ChannelEditorDialogController =
+ this.channelEditorDialogController,
+ pkg: String = TEST_PACKAGE_NAME,
+ notificationChannel: NotificationChannel = this.notificationChannel,
+ entry: NotificationEntry = this.entry,
+ onSettingsClick: NotificationInfo.OnSettingsClickListener? = null,
+ onAppSettingsClick: NotificationInfo.OnAppSettingsClickListener? = null,
+ onFeedbackClickListener: NotificationInfo.OnFeedbackClickListener? = null,
+ uiEventLogger: UiEventLogger = this.uiEventLogger,
+ isDeviceProvisioned: Boolean = true,
+ isNonblockable: Boolean = false,
+ wasShownHighPriority: Boolean = true,
+ assistantFeedbackController: AssistantFeedbackController = this.assistantFeedbackController,
+ metricsLogger: MetricsLogger = kosmos.metricsLogger,
+ onCloseClick: View.OnClickListener? = null,
+ ) {
+ underTest.bindNotification(
+ pm,
+ iNotificationManager,
+ onUserInteractionCallback,
+ channelEditorDialogController,
+ pkg,
+ notificationChannel,
+ entry,
+ onSettingsClick,
+ onAppSettingsClick,
+ onFeedbackClickListener,
+ uiEventLogger,
+ isDeviceProvisioned,
+ isNonblockable,
+ wasShownHighPriority,
+ assistantFeedbackController,
+ metricsLogger,
+ onCloseClick,
+ )
+ }
+
+ companion object {
+ private const val TEST_PACKAGE_NAME = "test_package"
+ private const val TEST_SYSTEM_PACKAGE_NAME = PrintManager.PRINT_SPOOLER_PACKAGE_NAME
+ private const val TEST_UID = 1
+ private const val TEST_CHANNEL = "test_channel"
+ private const val TEST_CHANNEL_NAME = "TEST CHANNEL NAME"
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
index d14ff35f824a..e5cb0fbc9e4b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImplTest.kt
@@ -49,6 +49,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
private val sectionsManager = mock<NotificationSectionsManager>()
private val msdlPlayer = kosmos.fakeMSDLPlayer
private var canRowBeDismissed = true
+ private var magneticAnimationsCancelled = false
private val underTest = kosmos.magneticNotificationRowManagerImpl
@@ -64,6 +65,7 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
children = notificationTestHelper.createGroup(childrenNumber).childrenContainer
swipedRow = children.attachedChildren[childrenNumber / 2]
configureMagneticRowListener(swipedRow)
+ magneticAnimationsCancelled = false
}
@Test
@@ -247,6 +249,35 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
assertThat(underTest.currentState).isEqualTo(State.IDLE)
}
+ @Test
+ fun onMagneticInteractionEnd_whenDetached_cancelsMagneticAnimations() =
+ kosmos.testScope.runTest {
+ // GIVEN the swiped row is detached
+ setDetachedState()
+
+ // WHEN the interaction ends on the row
+ underTest.onMagneticInteractionEnd(swipedRow, velocity = null)
+
+ // THEN magnetic animations are cancelled
+ assertThat(magneticAnimationsCancelled).isTrue()
+ }
+
+ @Test
+ fun onMagneticInteractionEnd_forMagneticNeighbor_cancelsMagneticAnimations() =
+ kosmos.testScope.runTest {
+ val neighborRow = children.attachedChildren[childrenNumber / 2 - 1]
+ configureMagneticRowListener(neighborRow)
+
+ // GIVEN that targets are set
+ setTargets()
+
+ // WHEN the interactionEnd is called on a target different from the swiped row
+ underTest.onMagneticInteractionEnd(neighborRow, null)
+
+ // THEN magnetic animations are cancelled
+ assertThat(magneticAnimationsCancelled).isTrue()
+ }
+
private fun setDetachedState() {
val threshold = 100f
underTest.setSwipeThresholdPx(threshold)
@@ -284,7 +315,11 @@ class MagneticNotificationRowManagerImplTest : SysuiTestCase() {
startVelocity: Float,
) {}
- override fun cancelMagneticAnimations() {}
+ override fun cancelMagneticAnimations() {
+ magneticAnimationsCancelled = true
+ }
+
+ override fun cancelTranslationAnimations() {}
override fun canRowBeDismissed(): Boolean = canRowBeDismissed
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
index 766ae73cb49d..789701f5e4b0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelperTest.java
@@ -405,7 +405,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase {
doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 0, 0);
mSwipeHelper.snapChild(mNotificationRow, 0, 0);
- verify(mCallback, times(1)).onDragCancelledWithVelocity(mNotificationRow, 0);
+ verify(mCallback, times(1)).onDragCancelled(mNotificationRow);
verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 0, 0);
verify(mSwipeHelper, times(1)).handleMenuCoveredOrDismissed();
}
@@ -416,7 +416,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase {
doNothing().when(mSwipeHelper).superSnapChild(mNotificationRow, 10, 0);
mSwipeHelper.snapChild(mNotificationRow, 10, 0);
- verify(mCallback, times(1)).onDragCancelledWithVelocity(mNotificationRow, 0);
+ verify(mCallback, times(1)).onDragCancelled(mNotificationRow);
verify(mSwipeHelper, times(1)).superSnapChild(mNotificationRow, 10, 0);
verify(mSwipeHelper, times(0)).handleMenuCoveredOrDismissed();
}
@@ -426,7 +426,7 @@ public class NotificationSwipeHelperTest extends SysuiTestCase {
doNothing().when(mSwipeHelper).superSnapChild(mView, 10, 0);
mSwipeHelper.snapChild(mView, 10, 0);
- verify(mCallback).onDragCancelledWithVelocity(mView, 0);
+ verify(mCallback).onDragCancelled(mView);
verify(mSwipeHelper, never()).superSnapChild(mView, 10, 0);
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 2e12336f6e93..6f785a3731e1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.phone;
import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_HIDDEN;
import static com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE;
+import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher;
import static org.junit.Assert.assertFalse;
@@ -76,6 +77,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInte
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.bouncer.ui.BouncerView;
import com.android.systemui.bouncer.ui.BouncerViewDelegate;
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor;
import com.android.systemui.dock.DockManager;
import com.android.systemui.dreams.DreamOverlayStateController;
@@ -171,6 +173,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
@Mock private SceneInteractor mSceneInteractor;
@Mock private DismissCallbackRegistry mDismissCallbackRegistry;
@Mock private BouncerInteractor mBouncerInteractor;
+ @Mock private CommunalSceneInteractor mCommunalSceneInteractor;
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private PrimaryBouncerCallbackInteractor.PrimaryBouncerExpansionCallback
@@ -209,6 +212,7 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
.thenReturn(mNotificationShadeWindowView);
when(mNotificationShadeWindowView.getWindowInsetsController())
.thenReturn(mWindowInsetsController);
+ when(mCommunalSceneInteractor.isIdleOnCommunal()).thenReturn(MutableStateFlow(false));
mStatusBarKeyguardViewManager =
new StatusBarKeyguardViewManager(
@@ -245,7 +249,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
mExecutor,
() -> mDeviceEntryInteractor,
mDismissCallbackRegistry,
- () -> mBouncerInteractor) {
+ () -> mBouncerInteractor,
+ mCommunalSceneInteractor) {
@Override
public ViewRootImpl getViewRootImpl() {
return mViewRootImpl;
@@ -749,7 +754,8 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase {
mExecutor,
() -> mDeviceEntryInteractor,
mDismissCallbackRegistry,
- () -> mBouncerInteractor) {
+ () -> mBouncerInteractor,
+ mCommunalSceneInteractor) {
@Override
public ViewRootImpl getViewRootImpl() {
return mViewRootImpl;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
index 73c191b32393..f0823e2f645e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.phone.ongoingcall.domain.interactor
import android.app.PendingIntent
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -30,12 +31,12 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository
import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler
-import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
-import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
-import com.android.systemui.statusbar.notification.shared.CallType
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.setNoCallState
+import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.setOngoingCallState
import com.android.systemui.statusbar.window.fakeStatusBarWindowControllerStore
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -50,6 +51,7 @@ import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableFlags(StatusBarChipsModernization.FLAG_NAME)
class OngoingCallInteractorTest : SysuiTestCase() {
private val kosmos = Kosmos().useUnconfinedTestDispatcher()
private val repository = kosmos.activeNotificationListRepository
@@ -76,21 +78,14 @@ class OngoingCallInteractorTest : SysuiTestCase() {
val testIntent: PendingIntent = mock()
val testPromotedContent =
PromotedNotificationContentModel.Builder("promotedCall").build()
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "promotedCall",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- statusBarChipIcon = testIconView,
- contentIntent = testIntent,
- promotedContent = testPromotedContent,
- )
- )
- }
- .build()
+ setOngoingCallState(
+ kosmos = this,
+ key = "promotedCall",
+ startTimeMs = 1000L,
+ statusBarChipIconView = testIconView,
+ contentIntent = testIntent,
+ promotedContent = testPromotedContent,
+ )
// Verify model is InCall and has the correct icon, intent, and promoted content.
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
@@ -101,45 +96,13 @@ class OngoingCallInteractorTest : SysuiTestCase() {
}
@Test
- fun ongoingCallNotification_emitsInCall() =
- kosmos.runTest {
- val latest by collectLastValue(underTest.ongoingCallState)
-
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "notif1",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- )
- )
- }
- .build()
-
- assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
- }
-
- @Test
fun notificationRemoved_emitsNoCall() =
kosmos.runTest {
val latest by collectLastValue(underTest.ongoingCallState)
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "notif1",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- )
- )
- }
- .build()
-
- repository.activeNotifications.value = ActiveNotificationsStore()
+ setOngoingCallState(kosmos = this)
+ setNoCallState(kosmos = this)
+
assertThat(latest).isInstanceOf(OngoingCallModel.NoCall::class.java)
}
@@ -149,19 +112,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true
val latest by collectLastValue(underTest.ongoingCallState)
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "notif1",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- uid = UID,
- )
- )
- }
- .build()
+ setOngoingCallState(kosmos = this, uid = UID)
assertThat(latest).isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java)
}
@@ -172,19 +123,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
val latest by collectLastValue(underTest.ongoingCallState)
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "notif1",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- uid = UID,
- )
- )
- }
- .build()
+ setOngoingCallState(kosmos = this, uid = UID)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
}
@@ -196,19 +135,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
// Start with notification and app not visible
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "notif1",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- uid = UID,
- )
- )
- }
- .build()
+ setOngoingCallState(kosmos = this, uid = UID)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
// App becomes visible
@@ -234,7 +161,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.fakeStatusBarWindowControllerStore.defaultDisplay
.ongoingProcessRequiresStatusBarVisible
)
- postOngoingCallNotification()
+ setOngoingCallState(kosmos = this)
assertThat(isStatusBarRequired).isTrue()
assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -256,9 +183,9 @@ class OngoingCallInteractorTest : SysuiTestCase() {
.ongoingProcessRequiresStatusBarVisible
)
- postOngoingCallNotification()
+ setOngoingCallState(kosmos = this)
- repository.activeNotifications.value = ActiveNotificationsStore()
+ setNoCallState(kosmos = this)
assertThat(isStatusBarRequired).isFalse()
assertThat(requiresStatusBarVisibleInRepository).isFalse()
@@ -283,7 +210,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
- postOngoingCallNotification()
+ setOngoingCallState(kosmos = this, uid = UID)
assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -305,7 +232,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
clearInvocations(kosmos.swipeStatusBarAwayGestureHandler)
// Set up notification but not in fullscreen
kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
- postOngoingCallNotification()
+ setOngoingCallState(kosmos = this)
assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
verify(kosmos.swipeStatusBarAwayGestureHandler, never())
@@ -319,7 +246,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
// Set up notification and fullscreen mode
kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
- postOngoingCallNotification()
+ setOngoingCallState(kosmos = this)
assertThat(isGestureListeningEnabled).isTrue()
verify(kosmos.swipeStatusBarAwayGestureHandler)
@@ -333,7 +260,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
// Set up notification and fullscreen mode
kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
- postOngoingCallNotification()
+ setOngoingCallState(kosmos = this)
clearInvocations(kosmos.swipeStatusBarAwayGestureHandler)
@@ -360,7 +287,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
)
// Start with an ongoing call (which should set status bar required)
- postOngoingCallNotification()
+ setOngoingCallState(kosmos = this)
assertThat(isStatusBarRequiredForOngoingCall).isTrue()
assertThat(requiresStatusBarVisibleInRepository).isTrue()
@@ -374,22 +301,6 @@ class OngoingCallInteractorTest : SysuiTestCase() {
assertThat(requiresStatusBarVisibleInWindowController).isFalse()
}
- private fun postOngoingCallNotification() {
- repository.activeNotifications.value =
- ActiveNotificationsStore.Builder()
- .apply {
- addIndividualNotif(
- activeNotificationModel(
- key = "notif1",
- whenTime = 1000L,
- callType = CallType.Ongoing,
- uid = UID,
- )
- )
- }
- .build()
- }
-
companion object {
private const val UID = 885
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
index ffdebb3517e7..b9e1c2ff232f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.policy.ui.dialog
import android.app.Dialog
import android.content.Intent
+import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -25,10 +26,10 @@ import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.mockActivityTransitionAnimatorController
import com.android.systemui.animation.mockDialogTransitionAnimator
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.backgroundCoroutineContext
import com.android.systemui.kosmos.mainCoroutineContext
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.activityStarter
-import com.android.systemui.runOnMainThreadAndWaitForIdleSync
import com.android.systemui.shade.data.repository.shadeDialogContextInteractor
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.systemUIDialogFactory
@@ -49,6 +50,7 @@ import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
class ModesDialogDelegateTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
@@ -76,20 +78,28 @@ class ModesDialogDelegateTest : SysuiTestCase() {
activityStarter,
{ kosmos.modesDialogViewModel },
mockDialogEventLogger,
+ kosmos.applicationCoroutineScope,
kosmos.mainCoroutineContext,
+ kosmos.backgroundCoroutineContext,
kosmos.shadeDialogContextInteractor,
)
}
@Test
- fun launchFromDialog_whenDialogNotOpen() {
- val intent: Intent = mock()
+ fun launchFromDialog_whenDialogNotOpen() =
+ testScope.runTest {
+ val intent: Intent = mock()
- runOnMainThreadAndWaitForIdleSync { underTest.launchFromDialog(intent) }
+ underTest.launchFromDialog(intent)
+ runCurrent()
- verify(activityStarter)
- .startActivity(eq(intent), eq(true), eq<ActivityTransitionAnimator.Controller?>(null))
- }
+ verify(activityStarter)
+ .startActivity(
+ eq(intent),
+ eq(true),
+ eq<ActivityTransitionAnimator.Controller?>(null),
+ )
+ }
@Test
fun launchFromDialog_whenDialogOpen() =
@@ -97,29 +107,26 @@ class ModesDialogDelegateTest : SysuiTestCase() {
val intent: Intent = mock()
lateinit var dialog: Dialog
- runOnMainThreadAndWaitForIdleSync {
- kosmos.applicationCoroutineScope.launch { dialog = underTest.showDialog() }
- runCurrent()
- underTest.launchFromDialog(intent)
- }
+ kosmos.applicationCoroutineScope.launch { dialog = underTest.showDialog() }
+ runCurrent()
+ underTest.launchFromDialog(intent)
+ runCurrent()
verify(mockDialogTransitionAnimator)
.createActivityTransitionController(any<Dialog>(), eq(null))
verify(activityStarter).startActivity(eq(intent), eq(true), eq(mockAnimationController))
- runOnMainThreadAndWaitForIdleSync { dialog.dismiss() }
+ dialog.dismiss()
}
@Test
fun dismiss_clearsDialogReference() {
- val dialog = runOnMainThreadAndWaitForIdleSync { underTest.createDialog() }
+ val dialog = underTest.createDialog()
assertThat(underTest.currentDialog).isEqualTo(dialog)
- runOnMainThreadAndWaitForIdleSync {
- dialog.show()
- dialog.dismiss()
- }
+ dialog.show()
+ dialog.dismiss()
assertThat(underTest.currentDialog).isNull()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index 381ac1519ce8..4ab1c0b1af6b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -39,11 +39,9 @@ import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior
import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
-import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.batteryController
+import com.android.systemui.statusbar.policy.fake
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.capture
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
@@ -52,7 +50,6 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mockito.verify
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
@@ -219,23 +216,12 @@ class KeyguardStatusBarViewModelTest(flags: FlagsParameterization) : SysuiTestCa
val latest by collectLastValue(underTest.isBatteryCharging)
runCurrent()
- val captor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
- verify(batteryController).addCallback(capture(captor))
- val callback = captor.value
-
- callback.onBatteryLevelChanged(
- /* level= */ 2,
- /* pluggedIn= */ false,
- /* charging= */ true,
- )
+ batteryController.fake._level = 2
+ batteryController.fake._isPluggedIn = true
assertThat(latest).isTrue()
- callback.onBatteryLevelChanged(
- /* level= */ 2,
- /* pluggedIn= */ true,
- /* charging= */ false,
- )
+ batteryController.fake._isPluggedIn = false
assertThat(latest).isFalse()
}
@@ -246,12 +232,9 @@ class KeyguardStatusBarViewModelTest(flags: FlagsParameterization) : SysuiTestCa
val job = underTest.isBatteryCharging.launchIn(this)
runCurrent()
- val captor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
- verify(batteryController).addCallback(capture(captor))
-
job.cancel()
runCurrent()
- verify(batteryController).removeCallback(captor.value)
+ assertThat(batteryController.fake.listeners).isEmpty()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 3ca1f5c0dd30..bafa8cf05a7f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -18,6 +18,7 @@
package com.android.systemui.user.data.repository
import android.app.admin.devicePolicyManager
+import android.content.Intent
import android.content.pm.UserInfo
import android.internal.statusbar.fakeStatusBarService
import android.os.UserHandle
@@ -27,6 +28,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
@@ -50,6 +52,8 @@ import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -172,6 +176,39 @@ class UserRepositoryImplTest : SysuiTestCase() {
}
@Test
+ fun userUnlockedFlow_tracksBroadcastedChanges() =
+ testScope.runTest {
+ val userHandle: UserHandle = mock()
+ underTest = create(testScope.backgroundScope)
+ val latest by collectLastValue(underTest.isUserUnlocked(userHandle))
+ whenever(manager.isUserUnlocked(eq(userHandle))).thenReturn(false)
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_USER_UNLOCKED),
+ )
+
+ assertThat(latest).isFalse()
+
+ whenever(manager.isUserUnlocked(eq(userHandle))).thenReturn(true)
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(Intent.ACTION_USER_UNLOCKED),
+ )
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun userUnlockedFlow_initialValueReported() =
+ testScope.runTest {
+ val userHandle: UserHandle = mock()
+ underTest = create(testScope.backgroundScope)
+ whenever(manager.isUserUnlocked(eq(userHandle))).thenReturn(true)
+ val latest by collectLastValue(underTest.isUserUnlocked(userHandle))
+ assertThat(latest).isTrue()
+ }
+
+ @Test
fun refreshUsers_sortsByCreationTime_guestUserLast() =
testScope.runTest {
underTest = create(testScope.backgroundScope)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowTest.kt
new file mode 100644
index 000000000000..2ca3d2fb916b
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/FlowTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.kotlin
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FlowTest : SysuiTestCase() {
+ val kosmos = testKosmos()
+ val testScope = kosmos.testScope
+
+ @Test
+ fun combine6() {
+ testScope.runTest {
+ val result by collectLastValue(combine(f0, f1, f2, f3, f4, f5, ::listOf6))
+ assertItemsEqualIndices(result)
+ }
+ }
+
+ @Test
+ fun combine7() {
+ testScope.runTest {
+ val result by collectLastValue(combine(f0, f1, f2, f3, f4, f5, f6, ::listOf7))
+ assertItemsEqualIndices(result)
+ }
+ }
+
+ @Test
+ fun combine8() {
+ testScope.runTest {
+ val result by collectLastValue(combine(f0, f1, f2, f3, f4, f5, f6, f7, ::listOf8))
+ assertItemsEqualIndices(result)
+ }
+ }
+
+ @Test
+ fun combine9() {
+ testScope.runTest {
+ val result by collectLastValue(combine(f0, f1, f2, f3, f4, f5, f6, f7, f8, ::listOf9))
+ assertItemsEqualIndices(result)
+ }
+ }
+
+ private fun assertItemsEqualIndices(list: List<Int>?) {
+ assertThat(list).isNotNull()
+ list ?: return
+
+ for (index in list.indices) {
+ assertThat(list[index]).isEqualTo(index)
+ }
+ }
+
+ private val f0: Flow<Int> = flowOf(0)
+ private val f1: Flow<Int> = flowOf(1)
+ private val f2: Flow<Int> = flowOf(2)
+ private val f3: Flow<Int> = flowOf(3)
+ private val f4: Flow<Int> = flowOf(4)
+ private val f5: Flow<Int> = flowOf(5)
+ private val f6: Flow<Int> = flowOf(6)
+ private val f7: Flow<Int> = flowOf(7)
+ private val f8: Flow<Int> = flowOf(8)
+}
+
+private fun <T> listOf6(a0: T, a1: T, a2: T, a3: T, a4: T, a5: T): List<T> =
+ listOf(a0, a1, a2, a3, a4, a5)
+
+private fun <T> listOf7(a0: T, a1: T, a2: T, a3: T, a4: T, a5: T, a6: T): List<T> =
+ listOf(a0, a1, a2, a3, a4, a5, a6)
+
+private fun <T> listOf8(a0: T, a1: T, a2: T, a3: T, a4: T, a5: T, a6: T, a7: T): List<T> =
+ listOf(a0, a1, a2, a3, a4, a5, a6, a7)
+
+private fun <T> listOf9(a0: T, a1: T, a2: T, a3: T, a4: T, a5: T, a6: T, a7: T, a8: T): List<T> =
+ listOf(a0, a1, a2, a3, a4, a5, a6, a7, a8)
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
index 9507b0483a06..6e2adc0a74ca 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -18,8 +18,11 @@ package com.android.keyguard.dagger;
import android.content.Context;
import android.content.res.Resources;
+import android.os.Vibrator;
import android.view.LayoutInflater;
+import androidx.annotation.Nullable;
+
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Application;
import com.android.systemui.dagger.qualifiers.Background;
@@ -55,7 +58,8 @@ public abstract class ClockRegistryModule {
FeatureFlags featureFlags,
@Main Resources resources,
LayoutInflater layoutInflater,
- ClockMessageBuffers clockBuffers) {
+ ClockMessageBuffers clockBuffers,
+ @Nullable Vibrator vibrator) {
ClockRegistry registry = new ClockRegistry(
context,
pluginManager,
@@ -69,7 +73,8 @@ public abstract class ClockRegistryModule {
context,
layoutInflater,
resources,
- com.android.systemui.shared.Flags.clockReactiveVariants()
+ com.android.systemui.shared.Flags.clockReactiveVariants(),
+ vibrator
),
context.getString(R.string.lockscreen_clock_id_fallback),
clockBuffers,
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index 19b29206440d..e2065f175c79 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -133,6 +133,8 @@ public class SwipeHelper implements Gefingerpoken, Dumpable {
private final ArrayMap<View, Animator> mDismissPendingMap = new ArrayMap<>();
+ private float mSnapBackDirection = 0;
+
public SwipeHelper(
Callback callback, Resources resources, ViewConfiguration viewConfiguration,
FalsingManager falsingManager, FeatureFlags featureFlags) {
@@ -350,6 +352,7 @@ public class SwipeHelper implements Gefingerpoken, Dumpable {
&& Math.abs(delta) > Math.abs(deltaPerpendicular)) {
if (mCallback.canChildBeDragged(mTouchedView)) {
mIsSwiping = true;
+ mCallback.setMagneticAndRoundableTargets(mTouchedView);
mCallback.onBeginDrag(mTouchedView);
mInitialTouchPos = getPos(ev);
mTranslation = getTranslation(mTouchedView);
@@ -442,6 +445,7 @@ public class SwipeHelper implements Gefingerpoken, Dumpable {
};
Animator anim = getViewTranslationAnimator(animView, newPos, updateListener);
+ mCallback.onMagneticInteractionEnd(animView, velocity);
if (anim == null) {
onDismissChildWithAnimationFinished();
return;
@@ -523,17 +527,24 @@ public class SwipeHelper implements Gefingerpoken, Dumpable {
*/
protected void snapChild(final View animView, final float targetLeft, float velocity) {
final boolean canBeDismissed = mCallback.canChildBeDismissed(animView);
+ mSnapBackDirection = getTranslation(animView) - targetLeft;
cancelTranslateAnimation(animView);
PhysicsAnimator<? extends View> anim =
createSnapBackAnimation(animView, targetLeft, velocity);
anim.addUpdateListener((target, values) -> {
- onTranslationUpdate(target, getTranslation(target), canBeDismissed);
+ float translation = getTranslation(target);
+ onTranslationUpdate(target, translation, canBeDismissed);
+ if ((mSnapBackDirection > 0 && translation < targetLeft)
+ || (mSnapBackDirection < 0 && translation > targetLeft)) {
+ mCallback.onChildSnapBackOvershoots();
+ mSnapBackDirection = 0;
+ }
});
anim.addEndListener((t, p, wasFling, cancelled, finalValue, finalVelocity, allEnded) -> {
mSnappingChild = false;
-
+ mSnapBackDirection = 0;
if (!cancelled) {
updateSwipeProgressFromOffset(animView, canBeDismissed);
resetViewIfSwiping(animView);
@@ -724,7 +735,8 @@ public class SwipeHelper implements Gefingerpoken, Dumpable {
dismissChild(mTouchedView, velocity,
!swipedFastEnough() /* useAccelerateInterpolator */);
} else {
- mCallback.onDragCancelledWithVelocity(mTouchedView, velocity);
+ mCallback.onMagneticInteractionEnd(mTouchedView, velocity);
+ mCallback.onDragCancelled(mTouchedView);
snapChild(mTouchedView, 0 /* leftTarget */, velocity);
}
mTouchedView = null;
@@ -926,18 +938,24 @@ public class SwipeHelper implements Gefingerpoken, Dumpable {
void onBeginDrag(View v);
+ /**
+ * Set magnetic and roundable targets for a view.
+ */
+ void setMagneticAndRoundableTargets(View v);
+
void onChildDismissed(View v);
void onDragCancelled(View v);
/**
- * A drag operation has been cancelled on a view with a final velocity.
- * @param v View that was dragged.
- * @param finalVelocity Final velocity of the drag.
+ * Notify that a magnetic interaction ended on a view with a velocity.
+ * <p>
+ * This method should be called when a view will snap back or be dismissed.
+ *
+ * @param view The {@link View} whose magnetic interaction ended.
+ * @param velocity The velocity when the interaction ended.
*/
- default void onDragCancelledWithVelocity(View v, float finalVelocity) {
- onDragCancelled(v);
- }
+ void onMagneticInteractionEnd(View view, float velocity);
/**
* Called when the child is long pressed and available to start drag and drop.
@@ -947,6 +965,11 @@ public class SwipeHelper implements Gefingerpoken, Dumpable {
void onLongPressSent(View v);
/**
+ * The snap back animation on a view overshoots for the first time.
+ */
+ void onChildSnapBackOvershoots();
+
+ /**
* Called when the child is snapped to a position.
*
* @param animView the view that was snapped.
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
index e365b770c203..d8e7a168ef3c 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
@@ -43,6 +43,7 @@ import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.ShadeExpansionChangeEvent
+import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.wm.shell.animation.FlingAnimationUtils
@@ -79,6 +80,7 @@ constructor(
private val activityStarter: ActivityStarter,
private val keyguardInteractor: KeyguardInteractor,
private val sceneInteractor: SceneInteractor,
+ private val shadeRepository: ShadeRepository,
private val windowRootViewProvider: Optional<Provider<WindowRootView>>,
) : TouchHandler {
/** An interface for creating ValueAnimators. */
@@ -260,6 +262,8 @@ constructor(
}
scrimManager.addCallback(scrimManagerCallback)
currentScrimController = scrimManager.currentController
+
+ shadeRepository.setLegacyShadeTracking(true)
session.registerCallback {
velocityTracker?.apply { recycle() }
velocityTracker = null
@@ -270,6 +274,7 @@ constructor(
if (!Flags.communalBouncerDoNotModifyPluginOpen()) {
notificationShadeWindowController.setForcePluginOpen(false, this)
}
+ shadeRepository.setLegacyShadeTracking(false)
}
session.registerGestureListener(onGestureListener)
session.registerInputListener { ev: InputEvent -> onMotionEvent(ev) }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt
index 8d5ea3c21e29..e4c45402bb52 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt
@@ -42,7 +42,7 @@ object CredentialPasswordViewBinder {
view.repeatWhenAttached {
// the header info never changes - do it early
val header = viewModel.header.first()
- passwordField.setTextOperationUser(UserHandle.of(header.user.deviceCredentialOwnerId))
+ passwordField.setTextOperationUser(UserHandle.of(header.user.userIdForPasswordEntry))
viewModel.inputFlags.firstOrNull()?.let { flags -> passwordField.inputType = flags }
if (requestFocusForInput) {
passwordField.requestFocus()
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
index 3907a37cd5d9..f304b97a4ffe 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
@@ -37,11 +37,13 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
+import com.android.systemui.user.domain.interactor.UserLockedInteractor
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
/**
* Callback that will be invoked when the Room database is created. Then the database will be
@@ -57,6 +59,7 @@ constructor(
@Named(DEFAULT_WIDGETS) private val defaultWidgets: Array<String>,
@CommunalLog logBuffer: LogBuffer,
private val userManager: UserManager,
+ private val userLockedInteractor: UserLockedInteractor,
) : RoomDatabase.Callback() {
companion object {
private const val TAG = "DefaultWidgetPopulation"
@@ -79,38 +82,36 @@ constructor(
}
bgScope.launch {
- // Default widgets should be associated with the main user.
- val user = userManager.mainUser
-
- if (user == null) {
- logger.w(
- "Skipped populating default widgets. Reason: device does not have a main user"
- )
- return@launch
- }
+ userLockedInteractor.isUserUnlocked(userManager.mainUser).first { it }
+ populateDefaultWidgets()
+ }
+ }
- val userSerialNumber = userManager.getUserSerialNumber(user.identifier)
-
- defaultWidgets.forEachIndexed { index, name ->
- val provider = ComponentName.unflattenFromString(name)
- provider?.let {
- val id = communalWidgetHost.allocateIdAndBindWidget(provider, user)
- id?.let {
- communalWidgetDaoProvider
- .get()
- .addWidget(
- widgetId = id,
- componentName = name,
- rank = index,
- userSerialNumber = userSerialNumber,
- spanY = SpanValue.Fixed(3),
- )
- }
+ private fun populateDefaultWidgets() {
+ // Default widgets should be associated with the main user.
+ val user = userManager.mainUser ?: return
+
+ val userSerialNumber = userManager.getUserSerialNumber(user.identifier)
+
+ defaultWidgets.forEachIndexed { index, name ->
+ val provider = ComponentName.unflattenFromString(name)
+ provider?.let {
+ val id = communalWidgetHost.allocateIdAndBindWidget(provider, user)
+ id?.let {
+ communalWidgetDaoProvider
+ .get()
+ .addWidget(
+ widgetId = id,
+ componentName = name,
+ rank = index,
+ userSerialNumber = userSerialNumber,
+ spanY = SpanValue.Fixed(3),
+ )
}
}
-
- logger.i("Populated default widgets in the database.")
}
+
+ logger.i("Populated default widgets in the database.")
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
index 8b6322720118..0a9bd4214a12 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
@@ -147,5 +147,9 @@ constructor(
to: OverlayKey,
transitionKey: TransitionKey?,
) = Unit
+
+ override fun instantlyShowOverlay(overlay: OverlayKey) = Unit
+
+ override fun instantlyHideOverlay(overlay: OverlayKey) = Unit
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index de55c92e84f9..6dab32a66c94 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -69,6 +69,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.phone.ManagedProfileController
+import com.android.systemui.user.domain.interactor.UserLockedInteractor
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.util.kotlin.emitOnStart
@@ -127,6 +128,7 @@ constructor(
private val batteryInteractor: BatteryInteractor,
private val dockManager: DockManager,
private val posturingInteractor: PosturingInteractor,
+ private val userLockedInteractor: UserLockedInteractor,
) {
private val logger = Logger(logBuffer, "CommunalInteractor")
@@ -162,7 +164,7 @@ constructor(
val isCommunalAvailable: Flow<Boolean> =
allOf(
communalSettingsInteractor.isCommunalEnabled,
- not(keyguardInteractor.isEncryptedOrLockdown),
+ userLockedInteractor.isUserUnlocked(userManager.mainUser),
keyguardInteractor.isKeyguardShowing,
)
.distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt
index 29d9cacdbc79..be1d005a0198 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalUserActionsViewModel.kt
@@ -20,7 +20,6 @@ import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
-import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -60,8 +59,7 @@ constructor(
if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer
add(Swipe.Up to bouncerOrGone)
- // "Home" is either Lockscreen, or Gone - if the device is entered.
- add(Swipe.End to SceneFamilies.Home)
+ add(Swipe.End to Scenes.Lockscreen)
addAll(
when (shadeMode) {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
index dec7ba3a3eb1..06a14eaa5c85 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
@@ -26,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.settings.UserTracker
+import com.android.systemui.user.domain.interactor.UserLockedInteractor
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf
import com.android.systemui.util.kotlin.BooleanFlowOperators.not
@@ -58,6 +59,7 @@ constructor(
@Main private val uiDispatcher: CoroutineDispatcher,
private val glanceableHubWidgetManagerLazy: Lazy<GlanceableHubWidgetManager>,
private val glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper,
+ private val userLockedInteractor: UserLockedInteractor,
) : CoreStartable {
private val appWidgetHost by lazy { appWidgetHostLazy.get() }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
index 9ce2ce0100a3..7437d3e215d5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModel.kt
@@ -19,7 +19,6 @@ package com.android.systemui.dreams.ui.viewmodel
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.viewmodel.UserActionsViewModel
@@ -40,7 +39,6 @@ import kotlinx.coroutines.flow.map
class DreamUserActionsViewModel
@AssistedInject
constructor(
- private val communalInteractor: CommunalInteractor,
private val deviceUnlockedInteractor: DeviceUnlockedInteractor,
private val shadeInteractor: ShadeInteractor,
private val shadeModeInteractor: ShadeModeInteractor,
@@ -54,14 +52,9 @@ constructor(
} else {
combine(
deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked },
- communalInteractor.isCommunalAvailable,
shadeModeInteractor.shadeMode,
- ) { isDeviceUnlocked, isCommunalAvailable, shadeMode ->
+ ) { isDeviceUnlocked, shadeMode ->
buildList {
- if (isCommunalAvailable) {
- add(Swipe.Start to Scenes.Communal)
- }
-
val bouncerOrGone =
if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer
add(Swipe.Up to bouncerOrGone)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 23be5c52ab5c..c61530c3dbcc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -163,10 +163,6 @@ constructor(
}
private fun bindJankViewModel() {
- if (SceneContainerFlag.isEnabled) {
- return
- }
-
jankHandle?.dispose()
jankHandle =
KeyguardJankBinder.bind(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index efa9c21f96b4..caf0fd4450fc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -16,7 +16,6 @@
package com.android.systemui.keyguard;
-import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK;
import static android.app.StatusBarManager.SESSION_KEYGUARD;
import static android.provider.Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT;
import static android.provider.Settings.System.LOCKSCREEN_SOUNDS_ENABLED;
@@ -76,7 +75,6 @@ import android.os.Bundle;
import android.os.DeadObjectException;
import android.os.Handler;
import android.os.IBinder;
-import android.os.IRemoteCallback;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
@@ -194,8 +192,6 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Iterator;
-import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -286,9 +282,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
private static final int SYSTEM_READY = 18;
private static final int CANCEL_KEYGUARD_EXIT_ANIM = 19;
private static final int BOOT_INTERACTOR = 20;
- private static final int BEFORE_USER_SWITCHING = 21;
- private static final int USER_SWITCHING = 22;
- private static final int USER_SWITCH_COMPLETE = 23;
/** Enum for reasons behind updating wakeAndUnlock state. */
@Retention(RetentionPolicy.SOURCE)
@@ -306,8 +299,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
int WAKE_AND_UNLOCK = 3;
}
- private final List<LockNowCallback> mLockNowCallbacks = new ArrayList<>();
-
/**
* The default amount of time we stay awake (used for all key input)
*/
@@ -366,18 +357,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController;
private final Lazy<ShadeController> mShadeController;
private final Lazy<CommunalSceneInteractor> mCommunalSceneInteractor;
- /*
- * Records the user id on request to go away, for validation when WM calls back to start the
- * exit animation.
- */
- private int mGoingAwayRequestedForUserId = -1;
-
private boolean mSystemReady;
private boolean mBootCompleted;
private boolean mBootSendUserPresent;
private boolean mShuttingDown;
private boolean mDozing;
private boolean mAnimatingScreenOff;
+ private boolean mIgnoreDismiss;
private final Context mContext;
private final FalsingCollector mFalsingCollector;
@@ -640,78 +626,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
};
- @VisibleForTesting
- protected UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() {
-
- @Override
- public void onBeforeUserSwitching(int newUser, @NonNull Runnable resultCallback) {
- mHandler.sendMessage(mHandler.obtainMessage(BEFORE_USER_SWITCHING,
- newUser, 0, resultCallback));
- }
-
- @Override
- public void onUserChanging(int newUser, @NonNull Context userContext,
- @NonNull Runnable resultCallback) {
- mHandler.sendMessage(mHandler.obtainMessage(USER_SWITCHING,
- newUser, 0, resultCallback));
- }
-
- @Override
- public void onUserChanged(int newUser, Context userContext) {
- mHandler.sendMessage(mHandler.obtainMessage(USER_SWITCH_COMPLETE,
- newUser, 0));
- }
- };
-
- /**
- * Handle {@link #BEFORE_USER_SWITCHING}
- */
- @VisibleForTesting
- void handleBeforeUserSwitching(int userId, Runnable resultCallback) {
- Log.d(TAG, String.format("onBeforeUserSwitching %d", userId));
- synchronized (KeyguardViewMediator.this) {
- mHandler.removeMessages(DISMISS);
- notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId));
- resetKeyguardDonePendingLocked();
- adjustStatusBarLocked();
- mKeyguardStateController.notifyKeyguardGoingAway(false);
- if (mLockPatternUtils.isSecure(userId) && !mShowing) {
- doKeyguardLocked(null);
- } else {
- resetStateLocked();
- }
- resultCallback.run();
- }
- }
-
- /**
- * Handle {@link #USER_SWITCHING}
- */
- @VisibleForTesting
- void handleUserSwitching(int userId, Runnable resultCallback) {
- Log.d(TAG, String.format("onUserSwitching %d", userId));
- synchronized (KeyguardViewMediator.this) {
- if (!mLockPatternUtils.isSecure(userId)) {
- dismiss(null, null);
- }
- resultCallback.run();
- }
- }
-
- /**
- * Handle {@link #USER_SWITCH_COMPLETE}
- */
- @VisibleForTesting
- void handleUserSwitchComplete(int userId) {
- Log.d(TAG, String.format("onUserSwitchComplete %d", userId));
- // Calling dismiss on a secure user will show the bouncer
- if (mLockPatternUtils.isSecure(userId)) {
- // We are calling dismiss with a delay as there are race conditions in some scenarios
- // caused by async layout listeners
- mHandler.postDelayed(() -> dismiss(null /* callback */, null /* message */), 500);
- }
- }
-
KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() {
@Override
@@ -728,6 +642,27 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
@Override
+ public void onUserSwitching(int userId) {
+ Log.d(TAG, String.format("onUserSwitching %d", userId));
+ synchronized (KeyguardViewMediator.this) {
+ mIgnoreDismiss = true;
+ notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId));
+ resetKeyguardDonePendingLocked();
+ resetStateLocked();
+ adjustStatusBarLocked();
+ }
+ }
+
+ @Override
+ public void onUserSwitchComplete(int userId) {
+ mIgnoreDismiss = false;
+ Log.d(TAG, String.format("onUserSwitchComplete %d", userId));
+ // We are calling dismiss with a delay as there are race conditions in some scenarios
+ // caused by async layout listeners
+ mHandler.postDelayed(() -> dismiss(null /* callback */, null /* message */), 500);
+ }
+
+ @Override
public void onDeviceProvisioned() {
sendUserPresentBroadcast();
}
@@ -1736,13 +1671,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
com.android.internal.R.anim.lock_screen_behind_enter);
mWorkLockController = new WorkLockActivityController(mContext, mUserTracker);
- mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
- // start() can be invoked in the middle of user switching, so check for this state and issue
- // the call manually as that important event was missed.
- if (mUserTracker.isUserSwitching()) {
- handleBeforeUserSwitching(mUserTracker.getUserId(), () -> {});
- handleUserSwitching(mUserTracker.getUserId(), () -> {});
- }
+
mJavaAdapter.alwaysCollectFlow(
mWallpaperRepository.getWallpaperSupportsAmbientMode(),
this::setWallpaperSupportsAmbientMode);
@@ -1791,7 +1720,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
// System ready can be invoked in the middle of user switching, so check for this state
// and issue the call manually as that important event was missed.
if (mUserTracker.isUserSwitching()) {
- mUserChangedCallback.onUserChanging(mUserTracker.getUserId(), mContext, () -> {});
+ mUpdateCallback.onUserSwitching(mUserTracker.getUserId());
}
}
// Most services aren't available until the system reaches the ready state, so we
@@ -2432,23 +2361,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mCommunalSceneInteractor.get().showHubFromPowerButton();
}
- int currentUserId = mSelectedUserInteractor.getSelectedUserId();
- if (options != null && options.getBinder(LOCK_ON_USER_SWITCH_CALLBACK) != null) {
- LockNowCallback callback = new LockNowCallback(currentUserId,
- IRemoteCallback.Stub.asInterface(
- options.getBinder(LOCK_ON_USER_SWITCH_CALLBACK)));
- synchronized (mLockNowCallbacks) {
- mLockNowCallbacks.add(callback);
- }
- Log.d(TAG, "LockNowCallback required for user: " + callback.mUserId);
- }
-
// if another app is disabling us, don't show
if (!mExternallyEnabled
&& !mLockPatternUtils.isUserInLockdown(
mSelectedUserInteractor.getSelectedUserId())) {
if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled");
- notifyLockNowCallback();
+
mNeedToReshowWhenReenabled = true;
return;
}
@@ -2466,7 +2384,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
// We're removing "reset" in the refactor - "resetting" the views will happen
// as a reaction to the root cause of the "reset" signal.
if (KeyguardWmStateRefactor.isEnabled()) {
- notifyLockNowCallback();
return;
}
@@ -2479,7 +2396,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
+ "previously hiding. It should be safe to short-circuit "
+ "here.");
resetStateLocked(/* hideBouncer= */ false);
- notifyLockNowCallback();
return;
}
} else {
@@ -2506,7 +2422,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
Log.d(TAG, "doKeyguard: not showing because device isn't provisioned and the sim is"
+ " not locked or missing");
}
- notifyLockNowCallback();
return;
}
@@ -2514,7 +2429,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
if (mLockPatternUtils.isLockScreenDisabled(mSelectedUserInteractor.getSelectedUserId())
&& !lockedOrMissing && !forceShow) {
if (DEBUG) Log.d(TAG, "doKeyguard: not showing because lockscreen is off");
- notifyLockNowCallback();
return;
}
@@ -2562,6 +2476,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
public void dismiss(IKeyguardDismissCallback callback, CharSequence message) {
+ if (mIgnoreDismiss) {
+ android.util.Log.i(TAG, "Ignoring request to dismiss (user switch in progress?)");
+ return;
+ }
+
if (mKeyguardStateController.isKeyguardGoingAway()) {
Log.i(TAG, "Ignoring dismiss because we're already going away.");
return;
@@ -2579,7 +2498,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
private void resetStateLocked(boolean hideBouncer) {
- if (DEBUG) Log.d(TAG, "resetStateLocked");
+ if (DEBUG) Log.e(TAG, "resetStateLocked");
Message msg = mHandler.obtainMessage(RESET, hideBouncer ? 1 : 0, 0);
mHandler.sendMessage(msg);
}
@@ -2827,18 +2746,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
message = "BOOT_INTERACTOR";
handleBootInteractor();
break;
- case BEFORE_USER_SWITCHING:
- message = "BEFORE_USER_SWITCHING";
- handleBeforeUserSwitching(msg.arg1, (Runnable) msg.obj);
- break;
- case USER_SWITCHING:
- message = "USER_SWITCHING";
- handleUserSwitching(msg.arg1, (Runnable) msg.obj);
- break;
- case USER_SWITCH_COMPLETE:
- message = "USER_SWITCH_COMPLETE";
- handleUserSwitchComplete(msg.arg1);
- break;
}
Log.d(TAG, "KeyguardViewMediator queue processing message: " + message);
}
@@ -2980,9 +2887,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mUiBgExecutor.execute(() -> {
Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ", "
+ reason + ")");
- if (showing) {
- notifyLockNowCallback();
- }
if (KeyguardWmStateRefactor.isEnabled()) {
// Handled in WmLockscreenVisibilityManager if flag is enabled.
@@ -3027,7 +2931,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
synchronized (KeyguardViewMediator.this) {
if (!mSystemReady) {
if (DEBUG) Log.d(TAG, "ignoring handleShow because system is not ready.");
- notifyLockNowCallback();
return;
}
if (DEBUG) Log.d(TAG, "handleShow");
@@ -3086,11 +2989,12 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
}
- final Runnable mKeyguardGoingAwayRunnable = new Runnable() {
+ private final Runnable mKeyguardGoingAwayRunnable = new Runnable() {
@SuppressLint("MissingPermission")
@Override
public void run() {
Trace.beginSection("KeyguardViewMediator.mKeyGuardGoingAwayRunnable");
+ Log.d(TAG, "keyguardGoingAwayRunnable");
mKeyguardViewControllerLazy.get().keyguardGoingAway();
int flags = 0;
@@ -3127,10 +3031,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
// Handled in WmLockscreenVisibilityManager if flag is enabled.
if (!KeyguardWmStateRefactor.isEnabled()) {
- mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId();
- Log.d(TAG, "keyguardGoingAway requested for userId: "
- + mGoingAwayRequestedForUserId);
-
// Don't actually hide the Keyguard at the moment, wait for window manager
// until it tells us it's safe to do so with startKeyguardExitAnimation.
// Posting to mUiOffloadThread to ensure that calls to ActivityTaskManager
@@ -3269,30 +3169,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback) {
Log.d(TAG, "handleStartKeyguardExitAnimation startTime=" + startTime
+ " fadeoutDuration=" + fadeoutDuration);
- int currentUserId = mSelectedUserInteractor.getSelectedUserId();
- if (!KeyguardWmStateRefactor.isEnabled() && mGoingAwayRequestedForUserId != currentUserId) {
- Log.e(TAG, "Not executing handleStartKeyguardExitAnimationInner() due to userId "
- + "mismatch. Requested: " + mGoingAwayRequestedForUserId + ", current: "
- + currentUserId);
- if (finishedCallback != null) {
- // There will not execute animation, send a finish callback to ensure the remote
- // animation won't hang there.
- try {
- finishedCallback.onAnimationFinished();
- } catch (RemoteException e) {
- Slog.w(TAG, "Failed to call onAnimationFinished", e);
- }
- }
- mHiding = false;
- if (mLockPatternUtils.isSecure(currentUserId)) {
- doKeyguardLocked(null);
- } else {
- resetStateLocked();
- dismiss(null, null);
- }
- return;
- }
-
synchronized (KeyguardViewMediator.this) {
mIsKeyguardExitAnimationCanceled = false;
// Tell ActivityManager that we canceled the keyguard animation if
@@ -3537,13 +3413,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
* app transition before finishing the current RemoteAnimation, or the keyguard being re-shown).
*/
private void handleCancelKeyguardExitAnimation() {
- if (!KeyguardWmStateRefactor.isEnabled()
- && mGoingAwayRequestedForUserId != mSelectedUserInteractor.getSelectedUserId()) {
- Log.e(TAG, "Setting pendingLock = true due to userId mismatch. Requested: "
- + mGoingAwayRequestedForUserId + ", current: "
- + mSelectedUserInteractor.getSelectedUserId());
- setPendingLock(true);
- }
if (mPendingLock) {
Log.d(TAG, "#handleCancelKeyguardExitAnimation: keyguard exit animation cancelled. "
+ "There's a pending lock, so we were cancelled because the device was locked "
@@ -3644,7 +3513,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mSurfaceBehindRemoteAnimationRequested = true;
if (ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS && !KeyguardWmStateRefactor.isEnabled()) {
- mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId();
startKeyguardTransition(false /* keyguardShowing */, false /* aodShowing */);
return;
}
@@ -3665,9 +3533,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
if (!KeyguardWmStateRefactor.isEnabled()) {
// Handled in WmLockscreenVisibilityManager.
- mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId();
- Log.d(TAG, "keyguardGoingAway requested for userId: "
- + mGoingAwayRequestedForUserId);
mActivityTaskManagerService.keyguardGoingAway(flags);
}
} catch (RemoteException e) {
@@ -4123,29 +3988,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
mUiBgExecutor.execute(mTrustManager::reportKeyguardShowingChanged);
}
- private void notifyLockNowCallback() {
- List<LockNowCallback> callbacks;
- synchronized (mLockNowCallbacks) {
- callbacks = new ArrayList<LockNowCallback>(mLockNowCallbacks);
- mLockNowCallbacks.clear();
- }
- Iterator<LockNowCallback> iter = callbacks.listIterator();
- while (iter.hasNext()) {
- LockNowCallback callback = iter.next();
- iter.remove();
- if (callback.mUserId != mSelectedUserInteractor.getSelectedUserId()) {
- Log.i(TAG, "Not notifying lockNowCallback due to user mismatch");
- continue;
- }
- Log.i(TAG, "Notifying lockNowCallback");
- try {
- callback.mRemoteCallback.sendResult(null);
- } catch (RemoteException e) {
- Log.e(TAG, "Could not issue LockNowCallback sendResult", e);
- }
- }
- }
-
private void notifyTrustedChangedLocked(boolean trusted) {
int size = mKeyguardStateCallbacks.size();
for (int i = size - 1; i >= 0; i--) {
@@ -4310,14 +4152,4 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
};
}
-
- private class LockNowCallback {
- final int mUserId;
- final IRemoteCallback mRemoteCallback;
-
- LockNowCallback(int userId, IRemoteCallback remoteCallback) {
- mUserId = userId;
- mRemoteCallback = remoteCallback;
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
index 5c03d65e570f..8f6815829ba2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt
@@ -69,7 +69,7 @@ constructor(
* Note that [onCancel] isn't used when the scene framework is enabled.
*/
fun sharedFlow(
- duration: Duration,
+ duration: Duration = transitionDuration,
onStep: (Float) -> Float,
startTime: Duration = 0.milliseconds,
onStart: (() -> Unit)? = null,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt
index 0cb684a1aabe..38263be33c82 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt
@@ -30,6 +30,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.viewmodel.KeyguardJankViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -79,15 +80,18 @@ object KeyguardJankBinder {
}
}
- launch {
- viewModel.lockscreenToAodTransition.collect {
- processStep(it, CUJ_LOCKSCREEN_TRANSITION_TO_AOD)
+ // The following is already done in KeyguardTransitionAnimationCallbackImpl.
+ if (!SceneContainerFlag.isEnabled) {
+ launch {
+ viewModel.lockscreenToAodTransition.collect {
+ processStep(it, CUJ_LOCKSCREEN_TRANSITION_TO_AOD)
+ }
}
- }
- launch {
- viewModel.aodToLockscreenTransition.collect {
- processStep(it, CUJ_LOCKSCREEN_TRANSITION_FROM_AOD)
+ launch {
+ viewModel.aodToLockscreenTransition.collect {
+ processStep(it, CUJ_LOCKSCREEN_TRANSITION_FROM_AOD)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt
index 19cd501fa787..50f8e086ac6e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/GlanceableHubBlurProvider.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.transitions
+import android.util.MathUtils
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -33,8 +34,18 @@ constructor(
blurConfig: BlurConfig,
) {
val exitBlurRadius: Flow<Float> =
- transitionAnimation.immediatelyTransitionTo(blurConfig.minBlurRadiusPx)
+ transitionAnimation.sharedFlow(
+ onStep = { MathUtils.lerp(blurConfig.maxBlurRadiusPx, blurConfig.minBlurRadiusPx, it) },
+ onStart = { blurConfig.maxBlurRadiusPx },
+ onFinish = { blurConfig.minBlurRadiusPx },
+ onCancel = { blurConfig.maxBlurRadiusPx },
+ )
val enterBlurRadius: Flow<Float> =
- transitionAnimation.immediatelyTransitionTo(blurConfig.maxBlurRadiusPx)
+ transitionAnimation.sharedFlow(
+ onStep = { MathUtils.lerp(blurConfig.minBlurRadiusPx, blurConfig.maxBlurRadiusPx, it) },
+ onStart = { blurConfig.minBlurRadiusPx },
+ onFinish = { blurConfig.maxBlurRadiusPx },
+ onCancel = { blurConfig.minBlurRadiusPx },
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt
index 3353983ab5a5..06c27d31cc3b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModel.kt
@@ -19,7 +19,6 @@ package com.android.systemui.keyguard.ui.viewmodel
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
import com.android.systemui.scene.shared.model.Scenes
@@ -43,7 +42,6 @@ class LockscreenUserActionsViewModel
@AssistedInject
constructor(
private val deviceEntryInteractor: DeviceEntryInteractor,
- private val communalInteractor: CommunalInteractor,
private val shadeInteractor: ShadeInteractor,
private val shadeModeInteractor: ShadeModeInteractor,
private val occlusionInteractor: SceneContainerOcclusionInteractor,
@@ -58,15 +56,10 @@ constructor(
combine(
deviceEntryInteractor.isUnlocked,
- communalInteractor.isCommunalAvailable,
shadeModeInteractor.shadeMode,
occlusionInteractor.isOccludingActivityShown,
- ) { isDeviceUnlocked, isCommunalAvailable, shadeMode, isOccluded ->
+ ) { isDeviceUnlocked, shadeMode, isOccluded ->
buildList {
- if (isCommunalAvailable) {
- add(Swipe.Start to Scenes.Communal)
- }
-
add(Swipe.Up to if (isDeviceUnlocked) Scenes.Gone else Scenes.Bouncer)
addAll(
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java
index 912ace7675d5..e5eec64ac615 100644
--- a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java
@@ -27,6 +27,7 @@ import android.content.pm.PackageManager;
import androidx.annotation.Nullable;
import com.android.dream.lowlight.LowLightDreamManager;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.SystemUser;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.shared.condition.Condition;
@@ -36,6 +37,7 @@ import com.android.systemui.util.condition.ConditionalCoreStartable;
import dagger.Lazy;
import java.util.Set;
+import java.util.concurrent.Executor;
import javax.inject.Inject;
import javax.inject.Named;
@@ -59,6 +61,8 @@ public class LowLightMonitor extends ConditionalCoreStartable implements Monitor
private final PackageManager mPackageManager;
+ private final Executor mExecutor;
+
@Inject
public LowLightMonitor(Lazy<LowLightDreamManager> lowLightDreamManager,
@SystemUser Monitor conditionsMonitor,
@@ -66,7 +70,8 @@ public class LowLightMonitor extends ConditionalCoreStartable implements Monitor
ScreenLifecycle screenLifecycle,
LowLightLogger lowLightLogger,
@Nullable @Named(LOW_LIGHT_DREAM_SERVICE) ComponentName lowLightDreamService,
- PackageManager packageManager) {
+ PackageManager packageManager,
+ @Background Executor backgroundExecutor) {
super(conditionsMonitor);
mLowLightDreamManager = lowLightDreamManager;
mConditionsMonitor = conditionsMonitor;
@@ -75,59 +80,69 @@ public class LowLightMonitor extends ConditionalCoreStartable implements Monitor
mLogger = lowLightLogger;
mLowLightDreamService = lowLightDreamService;
mPackageManager = packageManager;
+ mExecutor = backgroundExecutor;
}
@Override
public void onConditionsChanged(boolean allConditionsMet) {
- mLogger.d(TAG, "Low light enabled: " + allConditionsMet);
+ mExecutor.execute(() -> {
+ mLogger.d(TAG, "Low light enabled: " + allConditionsMet);
- mLowLightDreamManager.get().setAmbientLightMode(allConditionsMet
- ? AMBIENT_LIGHT_MODE_LOW_LIGHT : AMBIENT_LIGHT_MODE_REGULAR);
+ mLowLightDreamManager.get().setAmbientLightMode(allConditionsMet
+ ? AMBIENT_LIGHT_MODE_LOW_LIGHT : AMBIENT_LIGHT_MODE_REGULAR);
+ });
}
@Override
public void onScreenTurnedOn() {
- if (mSubscriptionToken == null) {
- mLogger.d(TAG, "Screen turned on. Subscribing to low light conditions.");
-
- mSubscriptionToken = mConditionsMonitor.addSubscription(
- new Monitor.Subscription.Builder(this)
- .addConditions(mLowLightConditions.get())
- .build());
- }
+ mExecutor.execute(() -> {
+ if (mSubscriptionToken == null) {
+ mLogger.d(TAG, "Screen turned on. Subscribing to low light conditions.");
+
+ mSubscriptionToken = mConditionsMonitor.addSubscription(
+ new Monitor.Subscription.Builder(this)
+ .addConditions(mLowLightConditions.get())
+ .build());
+ }
+ });
}
@Override
public void onScreenTurnedOff() {
- if (mSubscriptionToken != null) {
- mLogger.d(TAG, "Screen turned off. Removing subscription to low light conditions.");
-
- mConditionsMonitor.removeSubscription(mSubscriptionToken);
- mSubscriptionToken = null;
- }
+ mExecutor.execute(() -> {
+ if (mSubscriptionToken != null) {
+ mLogger.d(TAG, "Screen turned off. Removing subscription to low light conditions.");
+
+ mConditionsMonitor.removeSubscription(mSubscriptionToken);
+ mSubscriptionToken = null;
+ }
+ });
}
@Override
protected void onStart() {
- if (mLowLightDreamService != null) {
- // Note that the dream service is disabled by default. This prevents the dream from
- // appearing in settings on devices that don't have it explicitly excluded (done in
- // the settings overlay). Therefore, the component is enabled if it is to be used
- // here.
- mPackageManager.setComponentEnabledSetting(
- mLowLightDreamService,
- PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
- PackageManager.DONT_KILL_APP
- );
- } else {
- // If there is no low light dream service, do not observe conditions.
- return;
- }
-
- mScreenLifecycle.addObserver(this);
- if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
- onScreenTurnedOn();
- }
+ mExecutor.execute(() -> {
+ if (mLowLightDreamService != null) {
+ // Note that the dream service is disabled by default. This prevents the dream from
+ // appearing in settings on devices that don't have it explicitly excluded (done in
+ // the settings overlay). Therefore, the component is enabled if it is to be used
+ // here.
+ mPackageManager.setComponentEnabledSetting(
+ mLowLightDreamService,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP
+ );
+ } else {
+ // If there is no low light dream service, do not observe conditions.
+ return;
+ }
+
+ mScreenLifecycle.addObserver(this);
+ if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
+ onScreenTurnedOn();
+ }
+ });
+
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
index c9740811101b..be814aecc42b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -58,7 +58,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
private static final float DEVICE_DISABLED_ALPHA = 0.5f;
private static final float DEVICE_ACTIVE_ALPHA = 1f;
protected List<MediaItem> mMediaItemList = new CopyOnWriteArrayList<>();
- private boolean mShouldGroupSelectedMediaItems = Flags.enableOutputSwitcherSessionGrouping();
+ private boolean mShouldGroupSelectedMediaItems = Flags.enableOutputSwitcherDeviceGrouping();
public MediaOutputAdapter(MediaSwitchingController controller) {
super(controller);
@@ -333,7 +333,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
}
private boolean shouldShowGroupCheckbox(@NonNull GroupStatus groupStatus) {
- if (Flags.enableOutputSwitcherSessionGrouping()) {
+ if (Flags.enableOutputSwitcherDeviceGrouping()) {
return isGroupCheckboxEnabled(groupStatus);
}
return true;
@@ -391,7 +391,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
if (drawable instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) drawable).start();
}
- if (Flags.enableOutputSwitcherSessionGrouping()) {
+ if (Flags.enableOutputSwitcherDeviceGrouping()) {
mEndClickIcon.setContentDescription(mContext.getString(accessibilityStringId));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 51437b3bbdaf..9d375809786a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -733,7 +733,7 @@ public class MediaSwitchingController
selectedDevicesIds.add(connectedMediaDevice.getId());
}
boolean groupSelectedDevices =
- com.android.media.flags.Flags.enableOutputSwitcherSessionGrouping();
+ com.android.media.flags.Flags.enableOutputSwitcherDeviceGrouping();
int nextSelectedItemIndex = 0;
boolean suggestedDeviceAdded = false;
boolean displayGroupAdded = false;
diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
index 80c7c4a07c1e..caa7bbae0420 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt
@@ -130,6 +130,26 @@ constructor(
dataSource.replaceOverlay(from = from, to = to, transitionKey = transitionKey)
}
+ /**
+ * Instantly shows [overlay].
+ *
+ * The change is instantaneous and not animated; it will be observable in the next frame and
+ * there will be no transition animation.
+ */
+ fun instantlyShowOverlay(overlay: OverlayKey) {
+ dataSource.instantlyShowOverlay(overlay)
+ }
+
+ /**
+ * Instantly hides [overlay].
+ *
+ * The change is instantaneous and not animated; it will be observable in the next frame and
+ * there will be no transition animation.
+ */
+ fun instantlyHideOverlay(overlay: OverlayKey) {
+ dataSource.instantlyHideOverlay(overlay)
+ }
+
/** Sets whether the container is visible. */
fun setVisible(isVisible: Boolean) {
_isVisible.value = isVisible
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index 9c04323f2a0e..7a32491c0b67 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -33,8 +33,10 @@ import com.android.systemui.log.table.TableRowLogger
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.domain.resolver.SceneResolver
import com.android.systemui.scene.shared.logger.SceneLogger
+import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.util.kotlin.pairwise
import dagger.Lazy
import javax.inject.Inject
@@ -72,6 +74,7 @@ constructor(
private val deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>,
private val keyguardEnabledInteractor: Lazy<KeyguardEnabledInteractor>,
private val disabledContentInteractor: DisabledContentInteractor,
+ private val shadeModeInteractor: ShadeModeInteractor,
) {
interface OnSceneAboutToChangeListener {
@@ -336,6 +339,38 @@ constructor(
}
/**
+ * Instantly shows [overlay].
+ *
+ * The change is instantaneous and not animated; it will be observable in the next frame and
+ * there will be no transition animation.
+ */
+ fun instantlyShowOverlay(overlay: OverlayKey, loggingReason: String) {
+ if (!validateOverlayChange(to = overlay, loggingReason = loggingReason)) {
+ return
+ }
+
+ logger.logOverlayChangeRequested(to = overlay, reason = loggingReason)
+
+ repository.instantlyShowOverlay(overlay)
+ }
+
+ /**
+ * Instantly hides [overlay].
+ *
+ * The change is instantaneous and not animated; it will be observable in the next frame and
+ * there will be no transition animation.
+ */
+ fun instantlyHideOverlay(overlay: OverlayKey, loggingReason: String) {
+ if (!validateOverlayChange(from = overlay, loggingReason = loggingReason)) {
+ return
+ }
+
+ logger.logOverlayChangeRequested(from = overlay, reason = loggingReason)
+
+ repository.instantlyHideOverlay(overlay)
+ }
+
+ /**
* Replace [from] by [to] so that [from] ends up not being visible on screen and [to] ends up
* being visible.
*
@@ -459,6 +494,15 @@ constructor(
* @return `true` if the scene change is valid; `false` if it shouldn't happen
*/
private fun validateSceneChange(to: SceneKey, loggingReason: String): Boolean {
+ check(
+ !shadeModeInteractor.isDualShade || (to != Scenes.Shade && to != Scenes.QuickSettings)
+ ) {
+ "Can't change scene to ${to.debugName} when dual shade is on!"
+ }
+ check(!shadeModeInteractor.isSplitShade || (to != Scenes.QuickSettings)) {
+ "Can't change scene to ${to.debugName} in split shade mode!"
+ }
+
if (to !in repository.allContentKeys) {
return false
}
@@ -505,6 +549,13 @@ constructor(
" Logging reason for overlay change was: $loggingReason"
}
+ check(
+ shadeModeInteractor.isDualShade ||
+ (to != Overlays.NotificationsShade && to != Overlays.QuickSettingsShade)
+ ) {
+ "Can't show overlay ${to?.debugName} when dual shade is off!"
+ }
+
if (to != null && disabledContentInteractor.isDisabled(to)) {
return false
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
index 4538d1ca48f8..daf2d7f698b6 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
@@ -45,17 +45,12 @@ interface SceneDataSource {
* Asks for an asynchronous scene switch to [toScene], which will use the corresponding
* installed transition or the one specified by [transitionKey], if provided.
*/
- fun changeScene(
- toScene: SceneKey,
- transitionKey: TransitionKey? = null,
- )
+ fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null)
/**
* Asks for an instant scene switch to [toScene], without an animated transition of any kind.
*/
- fun snapToScene(
- toScene: SceneKey,
- )
+ fun snapToScene(toScene: SceneKey)
/**
* Request to show [overlay] so that it animates in from [currentScene] and ends up being
@@ -64,10 +59,7 @@ interface SceneDataSource {
* After this returns, this overlay will be included in [currentOverlays]. This does nothing if
* [overlay] is already shown.
*/
- fun showOverlay(
- overlay: OverlayKey,
- transitionKey: TransitionKey? = null,
- )
+ fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null)
/**
* Request to hide [overlay] so that it animates out to [currentScene] and ends up *not* being
@@ -76,10 +68,7 @@ interface SceneDataSource {
* After this returns, this overlay will not be included in [currentOverlays]. This does nothing
* if [overlay] is already hidden.
*/
- fun hideOverlay(
- overlay: OverlayKey,
- transitionKey: TransitionKey? = null,
- )
+ fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey? = null)
/**
* Replace [from] by [to] so that [from] ends up not being visible on screen and [to] ends up
@@ -87,9 +76,11 @@ interface SceneDataSource {
*
* This throws if [from] is not currently shown or if [to] is already shown.
*/
- fun replaceOverlay(
- from: OverlayKey,
- to: OverlayKey,
- transitionKey: TransitionKey? = null,
- )
+ fun replaceOverlay(from: OverlayKey, to: OverlayKey, transitionKey: TransitionKey? = null)
+
+ /** Asks for [overlay] to be instantly shown, without an animated transition of any kind. */
+ fun instantlyShowOverlay(overlay: OverlayKey)
+
+ /** Asks for [overlay] to be instantly hidden, without an animated transition of any kind. */
+ fun instantlyHideOverlay(overlay: OverlayKey)
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
index 5d0edc504782..dcb699539760 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
@@ -31,10 +31,8 @@ import kotlinx.coroutines.flow.stateIn
* Delegates calls to a runtime-provided [SceneDataSource] or to a no-op implementation if a
* delegate isn't set.
*/
-class SceneDataSourceDelegator(
- applicationScope: CoroutineScope,
- config: SceneContainerConfig,
-) : SceneDataSource {
+class SceneDataSourceDelegator(applicationScope: CoroutineScope, config: SceneContainerConfig) :
+ SceneDataSource {
private val noOpDelegate = NoOpSceneDataSource(config.initialSceneKey)
private val delegateMutable = MutableStateFlow<SceneDataSource>(noOpDelegate)
@@ -57,38 +55,31 @@ class SceneDataSourceDelegator(
)
override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
- delegateMutable.value.changeScene(
- toScene = toScene,
- transitionKey = transitionKey,
- )
+ delegateMutable.value.changeScene(toScene = toScene, transitionKey = transitionKey)
}
override fun snapToScene(toScene: SceneKey) {
- delegateMutable.value.snapToScene(
- toScene = toScene,
- )
+ delegateMutable.value.snapToScene(toScene = toScene)
}
override fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) {
- delegateMutable.value.showOverlay(
- overlay = overlay,
- transitionKey = transitionKey,
- )
+ delegateMutable.value.showOverlay(overlay = overlay, transitionKey = transitionKey)
}
override fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) {
- delegateMutable.value.hideOverlay(
- overlay = overlay,
- transitionKey = transitionKey,
- )
+ delegateMutable.value.hideOverlay(overlay = overlay, transitionKey = transitionKey)
}
override fun replaceOverlay(from: OverlayKey, to: OverlayKey, transitionKey: TransitionKey?) {
- delegateMutable.value.replaceOverlay(
- from = from,
- to = to,
- transitionKey = transitionKey,
- )
+ delegateMutable.value.replaceOverlay(from = from, to = to, transitionKey = transitionKey)
+ }
+
+ override fun instantlyShowOverlay(overlay: OverlayKey) {
+ delegateMutable.value.instantlyShowOverlay(overlay)
+ }
+
+ override fun instantlyHideOverlay(overlay: OverlayKey) {
+ delegateMutable.value.instantlyHideOverlay(overlay)
}
/**
@@ -105,9 +96,7 @@ class SceneDataSourceDelegator(
delegateMutable.value = delegate ?: noOpDelegate
}
- private class NoOpSceneDataSource(
- initialSceneKey: SceneKey,
- ) : SceneDataSource {
+ private class NoOpSceneDataSource(initialSceneKey: SceneKey) : SceneDataSource {
override val currentScene: StateFlow<SceneKey> =
MutableStateFlow(initialSceneKey).asStateFlow()
@@ -125,7 +114,11 @@ class SceneDataSourceDelegator(
override fun replaceOverlay(
from: OverlayKey,
to: OverlayKey,
- transitionKey: TransitionKey?
+ transitionKey: TransitionKey?,
) = Unit
+
+ override fun instantlyShowOverlay(overlay: OverlayKey) = Unit
+
+ override fun instantlyHideOverlay(overlay: OverlayKey) = Unit
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
index f0f476e65e2f..364da5f8e80d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/WindowRootView.kt
@@ -30,19 +30,14 @@ import com.android.systemui.compose.ComposeInitializer
import com.android.systemui.res.R
/** A view that can serve as the root of the main SysUI window. */
-open class WindowRootView(
- context: Context,
- attrs: AttributeSet?,
-) :
- FrameLayout(
- context,
- attrs,
- ) {
+open class WindowRootView(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
private lateinit var layoutInsetsController: LayoutInsetsController
private var leftInset = 0
private var rightInset = 0
+ private var previousInsets: WindowInsets? = null
+
override fun onAttachedToWindow() {
super.onAttachedToWindow()
@@ -66,11 +61,14 @@ open class WindowRootView(
override fun generateDefaultLayoutParams(): FrameLayout.LayoutParams? {
return LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.MATCH_PARENT
+ FrameLayout.LayoutParams.MATCH_PARENT,
)
}
override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets? {
+ if (windowInsets == previousInsets) {
+ return windowInsets
+ }
val insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
if (fitsSystemWindows) {
val paddingChanged = insets.top != paddingTop || insets.bottom != paddingBottom
@@ -95,7 +93,7 @@ open class WindowRootView(
leftInset = pairInsets.first
rightInset = pairInsets.second
applyMargins()
- return windowInsets
+ return windowInsets.also { previousInsets = WindowInsets(it) }
}
fun setLayoutInsetsController(layoutInsetsController: LayoutInsetsController) {
@@ -143,37 +141,22 @@ open class WindowRootView(
interface LayoutInsetsController {
/** Update the insets and calculate them accordingly. */
- fun getinsets(
- windowInsets: WindowInsets?,
- displayCutout: DisplayCutout?,
- ): Pair<Int, Int>
+ fun getinsets(windowInsets: WindowInsets?, displayCutout: DisplayCutout?): Pair<Int, Int>
}
private class LayoutParams : FrameLayout.LayoutParams {
var ignoreRightInset = false
- constructor(
- width: Int,
- height: Int,
- ) : super(
- width,
- height,
- )
+ constructor(width: Int, height: Int) : super(width, height)
@SuppressLint("CustomViewStyleable")
- constructor(
- context: Context,
- attrs: AttributeSet?,
- ) : super(
- context,
- attrs,
- ) {
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
val obtainedAttributes =
context.obtainStyledAttributes(attrs, R.styleable.StatusBarWindowView_Layout)
ignoreRightInset =
obtainedAttributes.getBoolean(
R.styleable.StatusBarWindowView_Layout_ignoreRightInset,
- false
+ false,
)
obtainedAttributes.recycle()
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index fbcd8ea9b9e4..233e15846450 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -31,6 +31,8 @@ import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.classifier.Classifier
import com.android.systemui.classifier.domain.interactor.FalsingInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
@@ -68,6 +70,8 @@ constructor(
val lightRevealScrim: LightRevealScrimViewModel,
val wallpaperViewModel: WallpaperViewModel,
keyguardInteractor: KeyguardInteractor,
+ val burnIn: AodBurnInViewModel,
+ val clock: KeyguardClockViewModel,
@Assisted view: View,
@Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
) : ExclusiveActivatable() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index c50c3dc07616..5746cef41d6b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -110,6 +110,7 @@ import com.android.systemui.keyguard.shared.model.Edge;
import com.android.systemui.keyguard.shared.model.TransitionState;
import com.android.systemui.keyguard.shared.model.TransitionStep;
import com.android.systemui.keyguard.ui.binder.KeyguardTouchViewBinder;
+import com.android.systemui.keyguard.ui.transitions.BlurConfig;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardTouchHandlingViewModel;
import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
@@ -309,6 +310,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
private final AlternateBouncerInteractor mAlternateBouncerInteractor;
private final QuickSettingsControllerImpl mQsController;
private final TouchHandler mTouchHandler = new TouchHandler();
+ private final BlurConfig mBlurConfig;
private long mDownTime;
private long mStatusBarLongPressDowntime = -1L;
@@ -606,7 +608,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
PowerInteractor powerInteractor,
KeyguardClockPositionAlgorithm keyguardClockPositionAlgorithm,
MSDLPlayer msdlPlayer,
- BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor) {
+ BrightnessMirrorShowingInteractor brightnessMirrorShowingInteractor,
+ BlurConfig blurConfig) {
+ mBlurConfig = blurConfig;
SceneContainerFlag.assertInLegacyMode();
keyguardStateController.addCallback(new KeyguardStateController.Callback() {
@Override
@@ -923,8 +927,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump
if (isBouncerShowing && isExpanded()) {
if (mBlurRenderEffect == null) {
mBlurRenderEffect = RenderEffect.createBlurEffect(
- mDepthController.getMaxBlurRadiusPx(),
- mDepthController.getMaxBlurRadiusPx(),
+ mBlurConfig.getMaxBlurRadiusPx(),
+ mBlurConfig.getMaxBlurRadiusPx(),
Shader.TileMode.CLAMP);
}
mView.setRenderEffect(mBlurRenderEffect);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt
index 04bdfbe00be3..f30043eece62 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar
import android.app.ActivityManager
+import android.content.res.Resources
import android.os.SystemProperties
import android.os.Trace
import android.os.Trace.TRACE_TAG_APP
@@ -28,20 +29,28 @@ import android.view.SurfaceControl
import android.view.ViewRootImpl
import androidx.annotation.VisibleForTesting
import com.android.systemui.Dumpable
+import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
import java.io.PrintWriter
import javax.inject.Inject
import com.android.systemui.keyguard.ui.transitions.BlurConfig
+import com.android.systemui.res.R
@SysUISingleton
open class BlurUtils @Inject constructor(
+ @Main resources: Resources,
blurConfig: BlurConfig,
private val crossWindowBlurListeners: CrossWindowBlurListeners,
dumpManager: DumpManager
) : Dumpable {
val minBlurRadius = blurConfig.minBlurRadiusPx
- val maxBlurRadius = blurConfig.maxBlurRadiusPx
+ val maxBlurRadius = if (Flags.notificationShadeBlur()) {
+ blurConfig.maxBlurRadiusPx
+ } else {
+ resources.getDimensionPixelSize(R.dimen.max_window_blur_radius).toFloat()
+ }
private var lastAppliedBlur = 0
private var earlyWakeupEnabled = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/NewStatusBarIcons.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/NewStatusBarIcons.kt
new file mode 100644
index 000000000000..e3be95373698
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/NewStatusBarIcons.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.core
+
+import com.android.settingslib.flags.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading and using the status bar simple fragment flag state */
+object NewStatusBarIcons {
+ /** Aconfig flag for new status bar icons */
+ const val FLAG_NAME = Flags.FLAG_NEW_STATUS_BAR_ICONS
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.newStatusBarIcons()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is not enabled to ensure that the refactor author catches issues in testing.
+ * Caution!! Using this check incorrectly will cause crashes in nextfood builds!
+ */
+ @JvmStatic
+ inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index 76ba7f9ea901..2bc48746f847 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -106,7 +106,7 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro
@Override
public void triggerMagneticForce(float endTranslation, @NonNull SpringForce springForce,
float startVelocity) {
- cancelMagneticAnimations();
+ cancelTranslationAnimations();
mMagneticAnimator.setSpring(springForce);
mMagneticAnimator.setStartVelocity(startVelocity);
mMagneticAnimator.animateToFinalPosition(endTranslation);
@@ -114,11 +114,15 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro
@Override
public void cancelMagneticAnimations() {
- cancelTranslationAnimations();
mMagneticAnimator.cancel();
}
@Override
+ public void cancelTranslationAnimations() {
+ ExpandableView.this.cancelTranslationAnimations();
+ }
+
+ @Override
public boolean canRowBeDismissed() {
return canExpandableViewBeDismissed();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
index bea14b2c003f..49b682d0a5d2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationInfo.java
@@ -83,20 +83,6 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
private static final String TAG = "InfoGuts";
private int mActualHeight;
- @IntDef(prefix = { "ACTION_" }, value = {
- ACTION_NONE,
- ACTION_TOGGLE_ALERT,
- ACTION_TOGGLE_SILENT,
- })
- public @interface NotificationInfoAction {
- }
-
- public static final int ACTION_NONE = 0;
- // standard controls
- static final int ACTION_TOGGLE_SILENT = 2;
- // standard controls
- private static final int ACTION_TOGGLE_ALERT = 5;
-
private TextView mPriorityDescriptionView;
private TextView mSilentDescriptionView;
private TextView mAutomaticDescriptionView;
@@ -123,7 +109,8 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
* The last importance level chosen by the user. Null if the user has not chosen an importance
* level; non-null once the user takes an action which indicates an explicit preference.
*/
- @Nullable private Integer mChosenImportance;
+ @Nullable
+ private Integer mChosenImportance;
private boolean mIsAutomaticChosen;
private boolean mIsSingleDefaultChannel;
private boolean mIsNonblockable;
@@ -143,27 +130,27 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
boolean mSkipPost = false;
// used by standard ui
- private OnClickListener mOnAutomatic = v -> {
+ private final OnClickListener mOnAutomatic = v -> {
mIsAutomaticChosen = true;
applyAlertingBehavior(BEHAVIOR_AUTOMATIC, true /* userTriggered */);
};
// used by standard ui
- private OnClickListener mOnAlert = v -> {
+ private final OnClickListener mOnAlert = v -> {
mChosenImportance = IMPORTANCE_DEFAULT;
mIsAutomaticChosen = false;
applyAlertingBehavior(BEHAVIOR_ALERTING, true /* userTriggered */);
};
// used by standard ui
- private OnClickListener mOnSilent = v -> {
+ private final OnClickListener mOnSilent = v -> {
mChosenImportance = IMPORTANCE_LOW;
mIsAutomaticChosen = false;
applyAlertingBehavior(BEHAVIOR_SILENT, true /* userTriggered */);
};
// used by standard ui
- private OnClickListener mOnDismissSettings = v -> {
+ private final OnClickListener mOnDismissSettings = v -> {
mPressedApply = true;
mGutsContainer.closeControls(v, /* save= */ true);
};
@@ -181,13 +168,6 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
mAutomaticDescriptionView = findViewById(R.id.automatic_summary);
}
- // Specify a CheckSaveListener to override when/if the user's changes are committed.
- public interface CheckSaveListener {
- // Invoked when importance has changed and the NotificationInfo wants to try to save it.
- // Listener should run saveImportance unless the change should be canceled.
- void checkSave(Runnable saveImportance, StatusBarNotification sbn);
- }
-
public interface OnSettingsClickListener {
void onClick(View v, NotificationChannel channel, int appUid);
}
@@ -216,7 +196,8 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
boolean isNonblockable,
boolean wasShownHighPriority,
AssistantFeedbackController assistantFeedbackController,
- MetricsLogger metricsLogger, OnClickListener onCloseClick)
+ MetricsLogger metricsLogger,
+ OnClickListener onCloseClick)
throws RemoteException {
mINotificationManager = iNotificationManager;
mMetricsLogger = metricsLogger;
@@ -623,7 +604,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
intent,
PackageManager.MATCH_DEFAULT_ONLY
);
- if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) {
+ if (resolveInfos == null || resolveInfos.isEmpty() || resolveInfos.get(0) == null) {
return null;
}
final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
@@ -758,6 +739,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
/**
* Returns a LogMaker with all available notification information.
* Caller should set category, type, and maybe subtype, before passing it to mMetricsLogger.
+ *
* @return LogMaker
*/
private LogMaker getLogMaker() {
@@ -769,10 +751,11 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
/**
* Returns an initialized LogMaker for logging importance changes.
* The caller may override the type before passing it to mMetricsLogger.
+ *
* @return LogMaker
*/
private LogMaker importanceChangeLogMaker() {
- Integer chosenImportance =
+ int chosenImportance =
mChosenImportance != null ? mChosenImportance : mStartingChannelImportance;
return getLogMaker().setCategory(MetricsEvent.ACTION_SAVE_IMPORTANCE)
.setType(MetricsEvent.TYPE_ACTION)
@@ -782,6 +765,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
/**
* Returns an initialized LogMaker for logging open/close of the info display.
* The caller may override the type before passing it to mMetricsLogger.
+ *
* @return LogMaker
*/
private LogMaker notificationControlsLogMaker() {
@@ -799,7 +783,9 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
@Retention(SOURCE)
@IntDef({BEHAVIOR_ALERTING, BEHAVIOR_SILENT, BEHAVIOR_AUTOMATIC})
- private @interface AlertingBehavior {}
+ private @interface AlertingBehavior {
+ }
+
private static final int BEHAVIOR_ALERTING = 0;
private static final int BEHAVIOR_SILENT = 1;
private static final int BEHAVIOR_AUTOMATIC = 2;
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 3941700496f4..5a29a699a7e6 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
@@ -97,6 +97,7 @@ constructor(
stackScrollLayout,
MAGNETIC_TRANSLATION_MULTIPLIERS.size,
)
+ currentMagneticListeners.swipedListener()?.cancelTranslationAnimations()
newListeners.forEach {
if (currentMagneticListeners.contains(it)) {
it?.cancelMagneticAnimations()
@@ -214,22 +215,32 @@ constructor(
}
override fun onMagneticInteractionEnd(row: ExpandableNotificationRow, velocity: Float?) {
- if (!row.isSwipedTarget()) return
-
- when (currentState) {
- State.PULLING -> {
- snapNeighborsBack(velocity)
- currentState = State.IDLE
- }
- State.DETACHED -> {
- currentState = State.IDLE
+ if (row.isSwipedTarget()) {
+ when (currentState) {
+ State.PULLING -> {
+ snapNeighborsBack(velocity)
+ currentState = State.IDLE
+ }
+ State.DETACHED -> {
+ // Cancel any detaching animation that may be occurring
+ currentMagneticListeners.swipedListener()?.cancelMagneticAnimations()
+ currentState = State.IDLE
+ }
+ else -> {}
}
- else -> {}
+ } else {
+ // A magnetic neighbor may be dismissing. In this case, we need to cancel any snap back
+ // magnetic animation to let the external dismiss animation proceed.
+ val listener = currentMagneticListeners.find { it == row.magneticRowListener }
+ listener?.cancelMagneticAnimations()
}
}
override fun reset() {
- currentMagneticListeners.forEach { it?.cancelMagneticAnimations() }
+ currentMagneticListeners.forEach {
+ it?.cancelMagneticAnimations()
+ it?.cancelTranslationAnimations()
+ }
currentState = State.IDLE
currentMagneticListeners = listOf()
currentRoundableTargets = null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt
index 46036d4c1fad..5959ef1e093b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticRowListener.kt
@@ -42,6 +42,9 @@ interface MagneticRowListener {
/** Cancel any animations related to the magnetic interactions of the row */
fun cancelMagneticAnimations()
+ /** Cancel any other animations related to the row's translation */
+ fun cancelTranslationAnimations()
+
/** Can the row be dismissed. */
fun canRowBeDismissed(): Boolean
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 804824569f1e..810d0b43b0dd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -62,6 +62,7 @@ import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.view.OneShotPreDrawListener;
import com.android.systemui.Dumpable;
import com.android.systemui.ExpandHelper;
+import com.android.systemui.Flags;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.classifier.Classifier;
import com.android.systemui.classifier.FalsingCollector;
@@ -462,6 +463,13 @@ public class NotificationStackScrollLayoutController implements Dumpable {
}
@Override
+ public void onMagneticInteractionEnd(View view, float velocity) {
+ if (view instanceof ExpandableNotificationRow row) {
+ mMagneticNotificationRowManager.onMagneticInteractionEnd(row, velocity);
+ }
+ }
+
+ @Override
public float getTotalTranslationLength(View animView) {
return mView.getTotalTranslationLength(animView);
}
@@ -503,14 +511,6 @@ public class NotificationStackScrollLayoutController implements Dumpable {
public void onDragCancelled(View v) {
}
- @Override
- public void onDragCancelledWithVelocity(View v, float finalVelocity) {
- if (v instanceof ExpandableNotificationRow row) {
- mMagneticNotificationRowManager.onMagneticInteractionEnd(
- row, finalVelocity);
- }
- }
-
/**
* Handles cleanup after the given {@code view} has been fully swiped out (including
* re-invoking dismiss logic in case the notification has not made its way out yet).
@@ -538,10 +538,6 @@ public class NotificationStackScrollLayoutController implements Dumpable {
*/
public void handleChildViewDismissed(View view) {
- if (view instanceof ExpandableNotificationRow row) {
- mMagneticNotificationRowManager.onMagneticInteractionEnd(
- row, null /* velocity */);
- }
// The View needs to clean up the Swipe states, e.g. roundness.
mView.onSwipeEnd();
if (mView.getClearAllInProgress()) {
@@ -613,11 +609,22 @@ public class NotificationStackScrollLayoutController implements Dumpable {
@Override
public void onBeginDrag(View v) {
+ mView.onSwipeBegin(v);
+ }
+
+ @Override
+ public void setMagneticAndRoundableTargets(View v) {
if (v instanceof ExpandableNotificationRow row) {
mMagneticNotificationRowManager.setMagneticAndRoundableTargets(
row, mView, mSectionsManager);
}
- mView.onSwipeBegin(v);
+ }
+
+ @Override
+ public void onChildSnapBackOvershoots() {
+ if (Flags.magneticNotificationSwipes()) {
+ mNotificationRoundnessManager.setViewsAffectedBySwipe(null, null, null);
+ }
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
index d476d482226d..6f4047f48205 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java
@@ -362,7 +362,8 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc
superSnapChild(animView, targetLeft, velocity);
}
- mCallback.onDragCancelledWithVelocity(animView, velocity);
+ mCallback.onMagneticInteractionEnd(animView, velocity);
+ mCallback.onDragCancelled(animView);
if (targetLeft == 0) {
handleMenuCoveredOrDismissed();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
index a339bc98457e..58326dbb3a34 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java
@@ -61,6 +61,7 @@ import kotlinx.coroutines.flow.StateFlowKt;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Objects;
/**
* The header group on Keyguard.
@@ -103,6 +104,9 @@ public class KeyguardStatusBarView extends RelativeLayout {
*/
private int mCutoutSideNudge = 0;
+ @Nullable
+ private WindowInsets mPreviousInsets = null;
+
private DisplayCutout mDisplayCutout;
private int mRoundedCornerPadding = 0;
// right and left padding applied to this view to account for cutouts and rounded corners
@@ -284,9 +288,12 @@ public class KeyguardStatusBarView extends RelativeLayout {
WindowInsets updateWindowInsets(
WindowInsets insets,
StatusBarContentInsetsProvider insetsProvider) {
- mLayoutState = LAYOUT_NONE;
- if (updateLayoutConsideringCutout(insetsProvider)) {
- requestLayout();
+ if (!Objects.equals(mPreviousInsets, insets)) {
+ mLayoutState = LAYOUT_NONE;
+ if (updateLayoutConsideringCutout(insetsProvider)) {
+ requestLayout();
+ }
+ mPreviousInsets = new WindowInsets(insets);
}
return super.onApplyWindowInsets(insets);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index b2c4ef95242b..01de925f3d78 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -65,6 +65,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags;
import com.android.systemui.bouncer.ui.BouncerView;
import com.android.systemui.bouncer.util.BouncerTestUtilsKt;
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor;
@@ -170,6 +171,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
private final Lazy<SceneInteractor> mSceneInteractorLazy;
private final Lazy<DeviceEntryInteractor> mDeviceEntryInteractorLazy;
private final DismissCallbackRegistry mDismissCallbackRegistry;
+ private final CommunalSceneInteractor mCommunalSceneInteractor;
private Job mListenForAlternateBouncerTransitionSteps = null;
private Job mListenForKeyguardAuthenticatedBiometricsHandled = null;
@@ -406,7 +408,8 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
@Main DelayableExecutor executor,
Lazy<DeviceEntryInteractor> deviceEntryInteractorLazy,
DismissCallbackRegistry dismissCallbackRegistry,
- Lazy<BouncerInteractor> bouncerInteractor
+ Lazy<BouncerInteractor> bouncerInteractor,
+ CommunalSceneInteractor communalSceneInteractor
) {
mContext = context;
mExecutor = executor;
@@ -443,6 +446,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
mStatusBarKeyguardViewManagerInteractor = statusBarKeyguardViewManagerInteractor;
mDeviceEntryInteractorLazy = deviceEntryInteractorLazy;
mDismissCallbackRegistry = dismissCallbackRegistry;
+ mCommunalSceneInteractor = communalSceneInteractor;
}
KeyguardTransitionInteractor mKeyguardTransitionInteractor;
@@ -1364,11 +1368,13 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
}
mStatusBarStateController.setLeaveOpenOnKeyguardHide(false);
- boolean hideBouncerOverDream = isBouncerShowing()
- && mDreamOverlayStateController.isOverlayActive();
+ boolean hideBouncerOverDreamOrHub = isBouncerShowing()
+ && (mDreamOverlayStateController.isOverlayActive()
+ || mCommunalSceneInteractor.isIdleOnCommunal().getValue());
mCentralSurfaces.endAffordanceLaunch();
// The second condition is for SIM card locked bouncer
- if (hideBouncerOverDream || (primaryBouncerIsScrimmed() && !needsFullscreenBouncer())) {
+ if (hideBouncerOverDreamOrHub
+ || (primaryBouncerIsScrimmed() && !needsFullscreenBouncer())) {
hideBouncer(false);
updateStates();
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt
index 095f0cba8a46..afeecfac6f6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/DarkIconInteractor.kt
@@ -15,15 +15,63 @@
*/
package com.android.systemui.statusbar.phone.domain.interactor
+import android.graphics.Rect
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher.DarkChange
import com.android.systemui.statusbar.phone.data.repository.DarkIconRepository
import com.android.systemui.statusbar.phone.domain.model.DarkState
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/** States pertaining to calculating colors for icons in dark mode. */
+@SysUISingleton
class DarkIconInteractor @Inject constructor(private val repository: DarkIconRepository) {
/** Dark-mode state for tinting icons. */
fun darkState(displayId: Int): Flow<DarkState> =
- repository.darkState(displayId).map { DarkState(it.areas, it.tint) }
+ repository.darkState(displayId).map { DarkState(it.areas, it.tint, it.darkIntensity) }
+
+ /**
+ * Given a display id: returns a flow of [IsAreaDark], a function that can tell you if a given
+ * [Rect] should be tinted dark or not. This flow ignores [DarkChange.tint] and
+ * [DarkChange.darkIntensity]
+ */
+ fun isAreaDark(displayId: Int): Flow<IsAreaDark> {
+ return repository.darkState(displayId).toIsAreaDark()
+ }
+
+ companion object {
+ /**
+ * Convenience function to convert between the repository's [darkState] into [IsAreaDark]
+ * type flows.
+ */
+ @JvmStatic
+ fun Flow<DarkChange>.toIsAreaDark(): Flow<IsAreaDark> =
+ map { darkChange ->
+ DarkStateWithoutIntensity(darkChange.areas, darkChange.darkIntensity < 0.5f)
+ }
+ .distinctUntilChanged()
+ .map { darkState ->
+ IsAreaDark { viewBounds: Rect ->
+ if (DarkIconDispatcher.isInAreas(darkState.areas, viewBounds)) {
+ darkState.isDark
+ } else {
+ false
+ }
+ }
+ }
+ .conflate()
+ .distinctUntilChanged()
+ }
+}
+
+/** So we can map between [DarkState] and a single boolean, but based on intensity */
+private data class DarkStateWithoutIntensity(val areas: Collection<Rect>, val isDark: Boolean)
+
+/** Given a region on screen, determine if the foreground should be dark or light */
+fun interface IsAreaDark {
+ fun isDark(viewBounds: Rect): Boolean
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.kt
index 3cab7cf859b4..d62e02538b99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/model/DarkState.kt
@@ -24,4 +24,6 @@ data class DarkState(
val areas: Collection<Rect>,
/** Tint color to apply to UI elements that fall within [areas]. */
val tint: Int,
+ /** _How_ dark the area is. Less than 0.5 is dark, otherwise light */
+ val darkIntensity: Float,
)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/data/repository/BatteryRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/data/repository/BatteryRepository.kt
new file mode 100644
index 000000000000..41793d2b0536
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/data/repository/BatteryRepository.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.data.repository
+
+import android.content.Context
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.shared.settings.data.repository.SystemSettingsRepository
+import com.android.systemui.statusbar.policy.BatteryController
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlin.time.Duration.Companion.minutes
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.scan
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+/**
+ * Repository-style state for battery information. Currently we just use the [BatteryController] as
+ * our source of truth, but we could (should?) migrate away from that eventually.
+ */
+@SysUISingleton
+class BatteryRepository
+@Inject
+constructor(
+ @Application context: Context,
+ @Background scope: CoroutineScope,
+ @Background bgDispatcher: CoroutineDispatcher,
+ private val controller: BatteryController,
+ settingsRepository: SystemSettingsRepository,
+) {
+ private val batteryState: StateFlow<BatteryCallbackState> =
+ conflatedCallbackFlow<(BatteryCallbackState) -> BatteryCallbackState> {
+ val callback =
+ object : BatteryController.BatteryStateChangeCallback {
+ override fun onBatteryLevelChanged(
+ level: Int,
+ pluggedIn: Boolean,
+ charging: Boolean,
+ ) {
+ trySend { prev -> prev.copy(level = level, isPluggedIn = pluggedIn) }
+ }
+
+ override fun onPowerSaveChanged(isPowerSave: Boolean) {
+ trySend { prev -> prev.copy(isPowerSaveEnabled = isPowerSave) }
+ }
+
+ override fun onIsBatteryDefenderChanged(isBatteryDefender: Boolean) {
+ trySend { prev ->
+ prev.copy(isBatteryDefenderEnabled = isBatteryDefender)
+ }
+ }
+
+ override fun onBatteryUnknownStateChanged(isUnknown: Boolean) {
+ // If the state is unknown, then all other fields are invalid
+ trySend { prev ->
+ if (isUnknown) {
+ // Forget everything before now
+ BatteryCallbackState(isStateUnknown = true)
+ } else {
+ prev.copy(isStateUnknown = false)
+ }
+ }
+ }
+ }
+
+ controller.addCallback(callback)
+ awaitClose { controller.removeCallback(callback) }
+ }
+ .scan(initial = BatteryCallbackState()) { state, eventF -> eventF(state) }
+ .flowOn(bgDispatcher)
+ .stateIn(scope, SharingStarted.Lazily, BatteryCallbackState())
+
+ /**
+ * True if the phone is plugged in. Note that this does not always mean the device is charging
+ */
+ val isPluggedIn = batteryState.map { it.isPluggedIn }
+
+ /** Is power saver enabled */
+ val isPowerSaveEnabled = batteryState.map { it.isPowerSaveEnabled }
+
+ /** Battery defender means the device is plugged in but not charging to protect the battery */
+ val isBatteryDefenderEnabled = batteryState.map { it.isBatteryDefenderEnabled }
+
+ /** The current level [0-100] */
+ val level = batteryState.map { it.level }
+
+ /** State unknown means that we can't detect a battery */
+ val isStateUnknown = batteryState.map { it.isStateUnknown }
+
+ /**
+ * [Settings.System.SHOW_BATTERY_PERCENT]. A user setting to indicate whether we should show the
+ * battery percentage in the home screen status bar
+ */
+ val isShowBatteryPercentSettingEnabled = run {
+ val default =
+ context.resources.getBoolean(
+ com.android.internal.R.bool.config_defaultBatteryPercentageSetting
+ )
+ settingsRepository
+ .boolSetting(name = Settings.System.SHOW_BATTERY_PERCENT, defaultValue = default)
+ .flowOn(bgDispatcher)
+ .stateIn(scope, SharingStarted.Lazily, default)
+ }
+
+ /** Get and re-fetch the estimate every 2 minutes while active */
+ private val estimate: Flow<String?> = flow {
+ while (true) {
+ val estimate = fetchEstimate()
+ emit(estimate)
+ delay(2.minutes)
+ }
+ }
+
+ /**
+ * If available, this flow yields a string that describes the approximate time remaining for the
+ * current battery charge and usage information. While subscribed, the estimate is updated every
+ * 2 minutes.
+ */
+ val batteryTimeRemainingEstimate: Flow<String?> = estimate.flowOn(bgDispatcher)
+
+ private suspend fun fetchEstimate() = suspendCancellableCoroutine { continuation ->
+ val callback =
+ BatteryController.EstimateFetchCompletion { estimate -> continuation.resume(estimate) }
+
+ controller.getEstimatedTimeRemainingString(callback)
+ }
+}
+
+/** Data object to track the current battery callback state */
+private data class BatteryCallbackState(
+ val level: Int? = null,
+ val isPluggedIn: Boolean = false,
+ val isPowerSaveEnabled: Boolean = false,
+ val isBatteryDefenderEnabled: Boolean = false,
+ val isStateUnknown: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt
new file mode 100644
index 000000000000..8fdb6ee57587
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.battery.data.repository.BatteryRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class BatteryInteractor @Inject constructor(repo: BatteryRepository) {
+ /** The current level in the range of [0-100] */
+ val level = repo.level.filterNotNull()
+
+ /** Whether the battery has been fully charged */
+ val isFull = level.map { it >= 100 }
+
+ /**
+ * For the sake of battery views, consider it to be "charging" if plugged in. This allows users
+ * to easily confirm that the device is properly plugged in, even if its' technically not
+ * charging due to issues with the source.
+ */
+ val isCharging = repo.isPluggedIn
+
+ /**
+ * The critical level (see [CRITICAL_LEVEL]) defines the level below which we might want to
+ * display an error UI. E.g., show the battery as red.
+ */
+ val isCritical = level.map { it <= CRITICAL_LEVEL }
+
+ /** @see [BatteryRepository.isStateUnknown] for docs. The battery cannot be detected */
+ val isStateUnknown = repo.isStateUnknown
+
+ /** @see [BatteryRepository.isBatteryDefenderEnabled] */
+ val isBatteryDefenderEnabled = repo.isBatteryDefenderEnabled
+
+ /** @see [BatteryRepository.isPowerSaveEnabled] */
+ val powerSave = repo.isPowerSaveEnabled
+
+ /** @see [BatteryRepository.isShowBatteryPercentSettingEnabled] */
+ val isBatteryPercentSettingEnabled = repo.isShowBatteryPercentSettingEnabled
+
+ /**
+ * The battery attribution (@see [BatteryAttributionModel]) describes the attribution that best
+ * represents the current battery charging state. If charging, the attribution is
+ * [BatteryAttributionModel.Charging], etc.
+ *
+ * This flow can be used to canonically describe the battery state charging state.
+ */
+ val batteryAttributionType =
+ combine(isCharging, powerSave, isBatteryDefenderEnabled) { charging, powerSave, defend ->
+ if (charging) {
+ BatteryAttributionModel.Charging
+ } else if (powerSave) {
+ BatteryAttributionModel.PowerSave
+ } else if (defend) {
+ BatteryAttributionModel.Defend
+ } else {
+ null
+ }
+ }
+
+ /** @see [BatteryRepository.batteryTimeRemainingEstimate] */
+ val batteryTimeRemainingEstimate = repo.batteryTimeRemainingEstimate
+
+ companion object {
+ /** Level below which we consider to be critically low */
+ private const val CRITICAL_LEVEL = 20
+ }
+}
+
+/** The charging state, and therefore attribution for the battery */
+enum class BatteryAttributionModel {
+ Defend,
+ PowerSave,
+ Charging,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/shared/ui/BatteryColors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/shared/ui/BatteryColors.kt
new file mode 100644
index 000000000000..d58b9a5c6825
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/shared/ui/BatteryColors.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the License);
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an AS IS BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.shared.ui
+
+import androidx.compose.ui.graphics.Color
+
+sealed interface BatteryColors {
+ val glyph: Color
+ val fill: Color
+ val background: Color
+
+ data object LightThemeDefaultColors : BatteryColors {
+ override val glyph = Color.White
+ override val fill = Color.Black
+ override val background = Color(0xFF8C8C8C)
+ }
+
+ data object LightThemeChargingColors : BatteryColors {
+ override val glyph = Color(0xFF446600)
+ override val fill = Color(0xFFB4FF1E)
+ override val background = Color(0xFFD6FF83)
+ }
+
+ data object LightThemeErrorColors : BatteryColors {
+ override val glyph = Color(0xFF79063A)
+ override val fill = Color(0xFFFF0166)
+ override val background = Color(0xFFFF8CBA)
+ }
+
+ data object LightThemePowerSaveColors : BatteryColors {
+ override val glyph = Color(0xFF5A4E00)
+ override val fill = Color(0xFFFFDA17)
+ override val background = Color(0xFFFFEB7F)
+ }
+
+ data object DarkThemeDefaultColors : BatteryColors {
+ override val glyph = Color.Black
+ override val fill = Color.White
+ override val background = Color(0xFFC5C5C5)
+ }
+
+ data object DarkThemeChargingColors : BatteryColors {
+ override val glyph = Color(0xFF446600)
+ override val fill = Color(0xFFB4FF1E)
+ override val background = Color(0xFFD6FF83)
+ }
+
+ data object DarkThemeErrorColors : BatteryColors {
+ override val glyph = Color(0xFF79063A)
+ override val fill = Color(0xFFFF0166)
+ override val background = Color(0xFFFF8CBA)
+ }
+
+ data object DarkThemePowerSaveColors : BatteryColors {
+ override val glyph = Color(0xFF5A4E00)
+ override val fill = Color(0xFFFFDA17)
+ override val background = Color(0xFFFFEB7F)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/shared/ui/BatteryFrame.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/shared/ui/BatteryFrame.kt
new file mode 100644
index 000000000000..02deb95f64f9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/shared/ui/BatteryFrame.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.shared.ui
+
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.addSvg
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlin.math.min
+
+/** Instead of reading from xml, we can define the battery here with a single Path */
+object BatteryFrame {
+ val pathSpec =
+ PathSpec(
+ path =
+ Path().apply {
+ addSvg(
+ "M17.5 0H2C0.895431 0 0 0.895431 0 2V10C0 11.1046 0.89543 12 2 12H17.5C18.6046 12 19.5 11.1046 19.5 10V8H19.9231C20.5178 8 21 7.51785 21 6.92308V5.07692C21 4.48215 20.5178 4 19.9231 4H19.5V2C19.5 0.895431 18.6046 0 17.5 0Z"
+ )
+ },
+ viewportHeight = 12.dp,
+ viewportWidth = 21.dp,
+ )
+
+ /** The width of the drawable that is usable for inside elements */
+ const val innerWidth = 19.5f
+
+ /** The height of the drawable that is usable for inside elements */
+ const val innerHeight = 12f
+}
+
+/**
+ * Encapsulates both the [Path] and the drawn dimensions of the internal SVG path. Use [scaleTo] to
+ * determine the appropriate scale factor (x and y) to fit the frame into its container
+ */
+data class PathSpec(val path: Path, val viewportWidth: Dp, val viewportHeight: Dp) {
+ /** Return the appropriate scale to achieve FIT_CENTER-type scaling */
+ fun scaleTo(w: Float, h: Float): Float {
+ // FIT_CENTER for the path, this determines how much we need to scale up to fit the bounds
+ // without skewing
+ val xScale = w / viewportWidth.value
+ val yScale = h / viewportHeight.value
+
+ return min(xScale, yScale)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/shared/ui/BatteryGlyph.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/shared/ui/BatteryGlyph.kt
new file mode 100644
index 000000000000..9be6a487c4fa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/shared/ui/BatteryGlyph.kt
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.shared.ui
+
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.addSvg
+import androidx.compose.ui.graphics.drawscope.DrawScope
+
+/**
+ * *What*
+ *
+ * Here we define the glyphs that can show in the battery icon. Anything that can render inside of
+ * the composed icon must be defined here, as an svg path with a [width] and [height]. The
+ * dimensions of the glyphs should be relative to a 19.5x12 canvas.
+ *
+ * *Why*
+ *
+ * In short:
+ * 1. text rendering is too heavyweight for what we need here.
+ * 2. We don't want to rely on the existence of fonts for correctness
+ *
+ * We need _exactly_ the glyphs representing "0" -> "9", plus a small handful of attributions to
+ * render inside of the battery asset itself. Doing this with text + other svg assets means that we
+ * need to lean on the entire text rendering stack just to get 1-3 characters to show. This would
+ * also end up taking into account things like ellipsizing, which we straight up do not need.
+ *
+ * Secondly, we want this to work at all display sizes _without depending on the source font for
+ * correctness_. This icon should render correctly as if it were a collection of pre-baked svg
+ * assets.
+ *
+ * *How can I change the font*
+ *
+ * In order to customize the look of these glyphs, you can do the following:
+ * 1. Render your asset (0-9 digit, or other symbol) into a 19.5x12 canvas
+ * 2. Make sure that this asset fits in all potential other contexts 2a. If you are updating a
+ * number, make sure it fits with all other numbers, and next to any attributions (charging
+ * symbol, power save symbol, etc.) 2b. If you are updating a symbol, make sure likewise that it
+ * fits next to all number pairings
+ * 3. Trace the glyph into an SVG path. _Ensure that there is no extra whitespace around the SVG
+ * path!_
+ * 4. Update or add the glyph here, copying the SVG path and updating the [width] and [height] to
+ * the appropriate value from the svg view box
+ *
+ * *What about localization?*
+ *
+ * Localization will be handled manually. Given that we are throwing away the text system, we will
+ * have to discern every textual variant of the 0-9 glyphs and override their values here based on
+ * the locale. This is still being worked on and the design is TBD.
+ *
+ * *Why are there "Large" variants?*
+ *
+ * To keep things simple, we just package up a given attribution potentially twice. If displaying by
+ * itself, then we can use the "large" variant of the given glyph. Else, we consider it to be inline
+ * amongst other glyphs and use the default version. The selection between which one to use happens
+ * down in the view model layer.
+ */
+
+/** Top-level, common interface. Glyphs are all defined on a 19.5x12 canvas */
+interface Glyph {
+ /** The exact width of this glyph, on the 19.5x12 canvas */
+ val width: Float
+ /** The exact height of this glyph, on the 19.5x12 canvas */
+ val height: Float
+
+ fun draw(drawScope: DrawScope, colors: BatteryColors)
+}
+
+/** If you have just a single path, we can draw you for free */
+interface SinglePathGlyph : Glyph {
+ /** A single path defines this glyph, thus its draw function is simple */
+ val path: Path
+
+ /** Draw this glyph in the given [drawScope], with the given [colors] */
+ override fun draw(drawScope: DrawScope, colors: BatteryColors) {
+ drawScope.apply { drawPath(path, color = colors.glyph) }
+ }
+}
+
+/** Text bad, glyph good */
+sealed interface BatteryGlyph : Glyph {
+ data object Bolt : BatteryGlyph, SinglePathGlyph {
+ override val path: Path =
+ Path().apply {
+ addSvg(
+ "M2.766,4.23L1.588,6.545C1.448,6.869 1.808,7.171 2.046,6.931L5.913,3.12C6.106,2.958 6.004,2.657 5.815,2.679L3.08,2.679L4.46,0.489C4.468,0.477 4.474,0.464 4.479,0.451C4.594,0.132 4.236,-0.149 4.006,0.094L0.1,3.783C-0.068,3.921 -0.005,4.238 0.2,4.23L2.766,4.23Z"
+ )
+ }
+
+ override val width: Float = 6.01f
+ override val height: Float = 7.02f
+ }
+
+ data object BoltLarge : BatteryGlyph, SinglePathGlyph {
+ override val path =
+ Path().apply {
+ addSvg(
+ "M7.191,3L4.031,3L4.721,0.5C4.791,0.25 4.601,0 4.341,0C4.231,0 4.121,0.05 4.051,0.13L0.081,4.49C-0.099,4.69 0.041,5 0.311,5L3.471,5L2.781,7.5C2.711,7.75 2.901,8 3.161,8C3.271,8 3.381,7.95 3.451,7.87L7.421,3.51C7.601,3.31 7.461,3 7.191,3Z"
+ )
+ }
+
+ override val width: Float = 7.5f
+ override val height: Float = 8f
+ }
+
+ data object Plus : BatteryGlyph, SinglePathGlyph {
+ override val path =
+ Path().apply {
+ addSvg(
+ "M3.719,0.724C3.719,0.324 3.395,0 2.996,0C2.596,0 2.272,0.324 2.272,0.724L2.272,2.276L0.719,2.276C0.32,2.276 -0.004,2.6 -0.004,3C-0.004,3.4 0.32,3.724 0.719,3.724L2.272,3.724L2.272,5.276C2.272,5.676 2.596,6 2.996,6C3.395,6 3.719,5.676 3.719,5.276L3.719,3.724L5.272,3.724C5.672,3.724 5.996,3.4 5.996,3C5.996,2.6 5.672,2.276 5.272,2.276L3.719,2.276L3.719,0.724Z"
+ )
+ }
+
+ override val width: Float = 6f
+ override val height: Float = 6f
+ }
+
+ data object PlusLarge : BatteryGlyph, SinglePathGlyph {
+ override val path =
+ Path().apply {
+ addSvg(
+ "M5.586,1.086C5.586,0.486 5.1,0 4.5,0C3.9,0 3.414,0.486 3.414,1.086L3.414,3.414L1.086,3.414C0.486,3.414 0,3.9 0,4.5C0,5.1 0.486,5.586 1.086,5.586L3.414,5.586L3.414,7.914C3.414,8.514 3.9,9 4.5,9C5.1,9 5.586,8.514 5.586,7.914L5.586,5.586L7.914,5.586C8.514,5.586 9,5.1 9,4.5C9,3.9 8.514,3.414 7.914,3.414L5.586,3.414L5.586,1.086Z"
+ )
+ }
+
+ override val width: Float = 9f
+ override val height: Float = 9f
+ }
+
+ data object Defend : BatteryGlyph {
+ private val fgPath =
+ Path().apply {
+ addSvg(
+ "M4.915,1.774C5.027,1.811 5.129,1.84 5.214,1.861C5.332,1.889 5.431,1.99 5.427,2.111C5.38,3.714 4.811,5.322 3.203,5.964L3.203,1.036C3.319,1.034 3.418,1.061 3.502,1.119C3.679,1.24 3.92,1.367 4.173,1.482C4.426,1.597 4.691,1.7 4.915,1.774Z"
+ )
+ }
+
+ private val bgPath =
+ Path().apply {
+ addSvg(
+ "M3.602,0.118C3.373,-0.039 2.967,-0.039 2.738,0.118C2.486,0.29 2.144,0.47 1.784,0.633C1.425,0.797 1.049,0.943 0.73,1.048C0.571,1.1 0.426,1.143 0.305,1.172C0.138,1.212 -0.002,1.356 0.003,1.527C0.07,3.804 0.886,6.088 3.17,7C5.454,6.088 6.27,3.804 6.337,1.527C6.342,1.356 6.202,1.212 6.035,1.172C5.914,1.143 5.769,1.1 5.61,1.048C5.291,0.943 4.915,0.797 4.556,0.633C4.196,0.47 3.854,0.29 3.602,0.118Z"
+ )
+ }
+
+ override fun draw(drawScope: DrawScope, colors: BatteryColors) {
+ drawScope.apply {
+ // Bg path first
+ drawPath(path = bgPath, color = colors.glyph)
+ // Then fg path, so it renders on top. Use the fill color so it matches
+ drawPath(path = fgPath, color = colors.fill)
+ }
+ }
+
+ override val width = 6.33f
+ override val height = 7f
+ }
+
+ data object DefendLarge : BatteryGlyph {
+ private val fgPath =
+ Path().apply {
+ addSvg(
+ "M3.5,6.919C4.12,6.559 5.75,5.349 5.98,2.409C5.32,2.159 4.37,1.729 3.5,1.129L3.5,6.919Z"
+ )
+ }
+
+ private val bgPath =
+ Path().apply {
+ addSvg(
+ "M3.5,8.009C3.29,8.009 0.17,6.639 0,2.079C0,1.859 0.13,1.659 0.33,1.589C0.92,1.379 2.18,0.879 3.19,0.109C3.28,0.039 3.39,-0.001 3.5,-0.001C3.61,-0.001 3.72,0.039 3.81,0.109C4.82,0.879 6.08,1.379 6.67,1.589C6.88,1.659 7.01,1.859 7,2.079C6.83,6.639 3.71,8.009 3.5,8.009Z"
+ )
+ }
+
+ override fun draw(drawScope: DrawScope, colors: BatteryColors) {
+ drawScope.apply {
+ // Bg path first
+ drawPath(path = bgPath, color = colors.glyph)
+ // Then fg path, so it renders on top. Use the fill color so it matches
+ drawPath(path = fgPath, color = colors.fill, alpha = 1f)
+ }
+ }
+
+ override val width = 7.01f
+ override val height = 8f
+ }
+
+ data object One : BatteryGlyph, SinglePathGlyph {
+ override val path =
+ Path().apply {
+ addSvg(
+ "M3.308,9.524C3.044,9.524 2.816,9.429 2.626,9.238C2.435,9.047 2.34,8.818 2.34,8.549L2.34,2.517L1.339,3.141C1.135,3.262 0.916,3.304 0.682,3.265C0.448,3.221 0.266,3.1 0.136,2.901C0.01,2.705 -0.029,2.491 0.019,2.257C0.067,2.019 0.19,1.837 0.39,1.711L2.769,0.19C2.847,0.138 2.925,0.095 3.003,0.06C3.081,0.021 3.189,0.001 3.328,0.001C3.596,0.001 3.822,0.097 4.004,0.287C4.186,0.478 4.277,0.712 4.277,0.989L4.277,8.549C4.277,8.818 4.181,9.047 3.991,9.238C3.8,9.429 3.572,9.524 3.308,9.524Z"
+ )
+ }
+
+ override val width = 4.28f
+ override val height = 9.52f
+ }
+
+ data object Two : BatteryGlyph, SinglePathGlyph {
+ override val path =
+ Path().apply {
+ addSvg(
+ "M1.091,9.47C0.779,9.47 0.519,9.37 0.311,9.171C0.103,8.967 -0.001,8.716 -0.001,8.417C-0.001,8.274 0.021,8.157 0.064,8.066C0.107,7.975 0.155,7.891 0.207,7.813L2.131,4.562C2.378,4.181 2.577,3.839 2.729,3.535C2.881,3.228 2.957,2.946 2.957,2.69L2.957,2.398C2.957,2.173 2.907,1.999 2.807,1.878C2.712,1.757 2.564,1.696 2.365,1.696C2.222,1.696 2.103,1.737 2.008,1.819C1.912,1.898 1.839,2.006 1.787,2.144C1.735,2.279 1.674,2.42 1.605,2.567C1.535,2.714 1.418,2.827 1.254,2.905C1.093,2.983 0.913,3 0.714,2.957C0.515,2.914 0.35,2.803 0.22,2.625C0.09,2.443 0.036,2.229 0.058,1.982C0.084,1.731 0.188,1.438 0.37,1.105C0.556,0.771 0.822,0.504 1.169,0.305C1.516,0.101 1.96,-0.001 2.502,-0.001C3.217,-0.001 3.795,0.199 4.237,0.598C4.683,0.996 4.906,1.551 4.906,2.261L4.906,2.502C4.906,2.935 4.818,3.358 4.64,3.77C4.462,4.181 4.222,4.619 3.918,5.083L2.411,7.702L2.437,7.748L4.302,7.748C4.54,7.748 4.744,7.832 4.913,8.001C5.086,8.17 5.173,8.372 5.173,8.606C5.173,8.844 5.086,9.048 4.913,9.217C4.744,9.385 4.54,9.47 4.302,9.47L1.091,9.47Z"
+ )
+ }
+
+ override val width = 5.17f
+ override val height = 9.47f
+ }
+
+ data object Three : BatteryGlyph, SinglePathGlyph {
+ override val path =
+ Path().apply {
+ addSvg(
+ "M2.433,9.633C1.883,9.633 1.434,9.531 1.088,9.327C0.741,9.123 0.473,8.842 0.282,8.482C0.096,8.122 0,7.826 -0.004,7.592C-0.004,7.353 0.059,7.154 0.184,6.993C0.314,6.833 0.477,6.731 0.672,6.688C0.867,6.645 1.044,6.66 1.205,6.733C1.365,6.807 1.48,6.905 1.549,7.026C1.623,7.147 1.686,7.284 1.738,7.436C1.79,7.587 1.866,7.715 1.965,7.819C2.065,7.923 2.204,7.975 2.381,7.975C2.594,7.975 2.763,7.904 2.888,7.76C3.014,7.613 3.077,7.384 3.077,7.071L3.077,6.298C3.077,5.973 3.01,5.741 2.875,5.602C2.745,5.464 2.555,5.394 2.303,5.394L2.154,5.394C1.954,5.394 1.781,5.321 1.634,5.174C1.491,5.026 1.419,4.851 1.419,4.647C1.419,4.443 1.489,4.27 1.627,4.127C1.77,3.98 1.939,3.906 2.134,3.906L2.277,3.906C2.511,3.906 2.68,3.839 2.784,3.704C2.893,3.566 2.947,3.334 2.947,3.009L2.947,2.391C2.947,2.118 2.895,1.921 2.791,1.8C2.691,1.679 2.548,1.618 2.362,1.618C2.214,1.618 2.095,1.655 2.004,1.728C1.918,1.798 1.848,1.895 1.796,2.021C1.749,2.147 1.692,2.264 1.627,2.372C1.562,2.48 1.45,2.571 1.289,2.645C1.129,2.714 0.956,2.73 0.769,2.69C0.587,2.647 0.431,2.545 0.301,2.385C0.176,2.22 0.122,2.01 0.139,1.754C0.161,1.499 0.267,1.224 0.457,0.929C0.652,0.634 0.912,0.407 1.237,0.246C1.567,0.082 1.974,-0.001 2.459,-0.001C3.183,-0.001 3.755,0.188 4.175,0.565C4.6,0.942 4.812,1.453 4.812,2.099L4.812,2.573C4.812,3.089 4.7,3.503 4.474,3.815C4.249,4.123 3.928,4.346 3.512,4.484L3.512,4.536C3.998,4.632 4.37,4.844 4.63,5.174C4.895,5.498 5.027,5.96 5.027,6.558L5.027,7.163C5.027,7.938 4.793,8.545 4.325,8.983C3.861,9.416 3.231,9.633 2.433,9.633Z"
+ )
+ }
+
+ override val width = 5.03f
+ override val height = 9.63f
+ }
+
+ data object Four : BatteryGlyph, SinglePathGlyph {
+ override val path =
+ Path().apply {
+ addSvg(
+ "M3.839,9.544C3.579,9.544 3.356,9.451 3.169,9.264C2.987,9.078 2.896,8.855 2.896,8.595L2.896,6.691L3.078,6.457L3.078,1.926L4.099,2.556L3.065,2.556L1.668,5.93L3.709,5.93L4.099,5.819L4.892,5.819C5.121,5.819 5.319,5.902 5.483,6.066C5.652,6.227 5.737,6.422 5.737,6.652C5.737,6.881 5.652,7.078 5.483,7.243C5.319,7.408 5.121,7.49 4.892,7.49L1.174,7.49C0.84,7.49 0.561,7.382 0.335,7.165C0.11,6.944 -0.003,6.673 -0.003,6.352C-0.003,6.205 0.015,6.099 0.049,6.034C0.084,5.969 0.121,5.898 0.16,5.819L2.461,0.717C2.552,0.526 2.699,0.359 2.903,0.216C3.111,0.073 3.334,0.002 3.572,0.002C3.906,0.002 4.19,0.123 4.424,0.366C4.662,0.609 4.781,0.901 4.781,1.243L4.781,8.595C4.781,8.855 4.688,9.078 4.502,9.264C4.315,9.451 4.094,9.544 3.839,9.544Z"
+ )
+ }
+
+ override val width = 5.74f
+ override val height = 9.54f
+ }
+
+ data object Five : BatteryGlyph, SinglePathGlyph {
+ override val path =
+ Path().apply {
+ addSvg(
+ "M2.442,9.473C1.935,9.473 1.51,9.388 1.168,9.219C0.826,9.05 0.555,8.818 0.356,8.524C0.156,8.225 0.042,7.926 0.011,7.627C-0.015,7.328 0.039,7.089 0.174,6.912C0.312,6.734 0.477,6.625 0.668,6.587C0.863,6.548 1.036,6.563 1.188,6.632C1.344,6.701 1.456,6.792 1.526,6.905C1.595,7.013 1.658,7.141 1.714,7.289C1.775,7.432 1.853,7.553 1.948,7.653C2.043,7.748 2.176,7.796 2.345,7.796C2.561,7.796 2.73,7.72 2.852,7.568C2.977,7.416 3.04,7.159 3.04,6.795L3.04,5.716C3.04,5.382 2.982,5.146 2.865,5.007C2.752,4.864 2.598,4.793 2.403,4.793C2.265,4.793 2.152,4.825 2.065,4.89C1.983,4.951 1.907,5.031 1.838,5.131C1.755,5.235 1.63,5.315 1.461,5.371C1.296,5.427 1.103,5.425 0.882,5.365C0.657,5.299 0.477,5.165 0.343,4.962C0.208,4.754 0.15,4.533 0.167,4.299L0.369,1.062C0.39,0.776 0.514,0.529 0.739,0.321C0.965,0.108 1.222,0.002 1.513,0.002L3.82,0.002C4.058,0.002 4.262,0.087 4.431,0.256C4.6,0.425 4.685,0.626 4.685,0.86C4.685,1.098 4.6,1.302 4.431,1.471C4.262,1.64 4.058,1.724 3.82,1.724L1.896,1.724L1.753,3.85L1.805,3.863C1.944,3.703 2.119,3.575 2.332,3.48C2.548,3.384 2.795,3.337 3.073,3.337C3.662,3.337 4.13,3.551 4.477,3.98C4.823,4.409 4.997,5.029 4.997,5.839L4.997,6.717C4.997,7.618 4.771,8.303 4.321,8.771C3.87,9.239 3.244,9.473 2.442,9.473Z"
+ )
+ }
+
+ override val width = 4.99f
+ override val height = 9.47f
+ }
+
+ data object Six : BatteryGlyph, SinglePathGlyph {
+ override val path =
+ Path().apply {
+ addSvg(
+ "M2.628,9.519C1.795,9.519 1.15,9.268 0.69,8.765C0.231,8.262 0.001,7.56 0.001,6.659L0.001,5.99C0.001,5.383 0.09,4.83 0.268,4.332C0.45,3.829 0.742,3.231 1.145,2.538L2.289,0.445C2.411,0.233 2.591,0.094 2.829,0.029C3.067,-0.036 3.293,-0.006 3.505,0.12C3.722,0.246 3.858,0.432 3.915,0.679C3.975,0.922 3.936,1.151 3.798,1.368L3.161,2.59C2.944,2.967 2.662,3.318 2.316,3.643C1.969,3.964 1.73,4.726 1.6,5.931L0.339,6.016C0.374,5.231 0.643,4.603 1.145,4.13C1.648,3.658 2.289,3.422 3.069,3.422C3.702,3.422 4.222,3.647 4.629,4.098C5.037,4.544 5.24,5.188 5.24,6.029L5.24,6.711C5.24,7.569 5.009,8.251 4.545,8.758C4.081,9.265 3.442,9.519 2.628,9.519ZM2.621,7.907C2.855,7.907 3.033,7.822 3.154,7.654C3.275,7.48 3.336,7.207 3.336,6.834L3.336,5.976C3.336,5.621 3.275,5.352 3.154,5.17C3.033,4.989 2.855,4.897 2.621,4.897C2.391,4.897 2.216,4.989 2.095,5.17C1.973,5.352 1.913,5.623 1.913,5.983L1.913,6.834C1.913,7.207 1.973,7.48 2.095,7.654C2.216,7.822 2.391,7.907 2.621,7.907Z"
+ )
+ }
+
+ override val width = 5.24f
+ override val height = 9.52f
+ }
+
+ data object Seven : BatteryGlyph, SinglePathGlyph {
+ override val path =
+ Path().apply {
+ addSvg(
+ "M1.481,9.278C1.234,9.187 1.056,9.02 0.948,8.777C0.839,8.53 0.835,8.283 0.935,8.036L3.112,1.77L3.093,1.724L0.87,1.724C0.632,1.724 0.426,1.64 0.252,1.471C0.083,1.298 -0.001,1.094 -0.001,0.86C-0.001,0.626 0.083,0.425 0.252,0.256C0.426,0.087 0.632,0.002 0.87,0.002L4.191,0.002C4.503,0.002 4.763,0.104 4.971,0.308C5.184,0.511 5.29,0.763 5.29,1.062C5.29,1.205 5.277,1.315 5.251,1.393C5.225,1.467 5.19,1.582 5.147,1.737L2.709,8.712C2.618,8.963 2.452,9.145 2.209,9.258C1.971,9.366 1.728,9.373 1.481,9.278Z"
+ )
+ }
+
+ override val width = 5.29f
+ override val height = 9.34f
+ }
+
+ data object Eight : BatteryGlyph, SinglePathGlyph {
+ override val path =
+ Path().apply {
+ addSvg(
+ "M2.611,9.633C1.788,9.633 1.147,9.411 0.687,8.97C0.228,8.528 -0.002,7.945 -0.002,7.221L-0.002,6.538C-0.002,6.019 0.117,5.592 0.356,5.258C0.594,4.92 0.913,4.679 1.311,4.536L1.311,4.484C0.973,4.342 0.7,4.123 0.492,3.828C0.289,3.533 0.187,3.158 0.187,2.704L0.187,2.19C0.187,1.527 0.406,0.996 0.843,0.598C1.285,0.199 1.875,-0.001 2.611,-0.001C3.344,-0.001 3.929,0.194 4.366,0.584C4.808,0.974 5.029,1.51 5.029,2.19L5.029,2.704C5.029,3.167 4.921,3.546 4.704,3.841C4.492,4.136 4.223,4.35 3.898,4.484L3.898,4.536C4.301,4.679 4.622,4.918 4.86,5.252C5.099,5.581 5.218,6.01 5.218,6.538L5.218,7.215C5.218,7.947 4.99,8.534 4.535,8.976C4.085,9.414 3.443,9.633 2.611,9.633ZM2.611,8.008C2.837,8.008 3.01,7.93 3.131,7.774C3.257,7.618 3.32,7.386 3.32,7.078L3.32,6.181C3.32,5.873 3.257,5.646 3.131,5.498C3.01,5.347 2.837,5.271 2.611,5.271C2.377,5.271 2.2,5.347 2.078,5.498C1.957,5.646 1.896,5.873 1.896,6.181L1.896,7.078C1.896,7.386 1.957,7.618 2.078,7.774C2.204,7.93 2.382,8.008 2.611,8.008ZM2.605,3.958C2.813,3.958 2.969,3.889 3.073,3.75C3.181,3.607 3.235,3.405 3.235,3.145L3.235,2.405C3.235,2.136 3.181,1.934 3.073,1.8C2.964,1.666 2.811,1.598 2.611,1.598C2.408,1.598 2.252,1.666 2.143,1.8C2.035,1.934 1.981,2.136 1.981,2.405L1.981,3.145C1.981,3.405 2.033,3.607 2.137,3.75C2.245,3.889 2.401,3.958 2.605,3.958Z"
+ )
+ }
+
+ override val width = 5.22f
+ override val height = 9.63f
+ }
+
+ data object Nine : BatteryGlyph, SinglePathGlyph {
+ override val path =
+ Path().apply {
+ addSvg(
+ "M2.612,0.003C3.444,0.003 4.089,0.254 4.549,0.757C5.008,1.26 5.238,1.962 5.238,2.863L5.238,3.532C5.238,4.139 5.147,4.694 4.965,5.196C4.787,5.695 4.495,6.291 4.087,6.984L2.95,9.077C2.828,9.289 2.648,9.428 2.41,9.493C2.172,9.558 1.947,9.528 1.734,9.402C1.518,9.276 1.379,9.092 1.318,8.85C1.262,8.603 1.303,8.371 1.442,8.154L2.079,6.932C2.295,6.555 2.577,6.206 2.924,5.885C3.27,5.561 3.509,4.796 3.639,3.591L4.9,3.507C4.861,4.291 4.59,4.919 4.087,5.391C3.589,5.864 2.95,6.1 2.17,6.1C1.537,6.1 1.017,5.877 0.61,5.431C0.202,4.98 -0.001,4.334 -0.001,3.493L-0.001,2.811C-0.001,1.953 0.231,1.27 0.694,0.763C1.158,0.256 1.797,0.003 2.612,0.003ZM2.618,1.615C2.384,1.615 2.207,1.702 2.085,1.875C1.964,2.044 1.903,2.315 1.903,2.687L1.903,3.545C1.903,3.901 1.964,4.169 2.085,4.352C2.207,4.534 2.384,4.624 2.618,4.624C2.848,4.624 3.023,4.534 3.145,4.352C3.266,4.169 3.327,3.899 3.327,3.539L3.327,2.687C3.327,2.315 3.266,2.044 3.145,1.875C3.023,1.702 2.848,1.615 2.618,1.615Z"
+ )
+ }
+
+ override val width = 5.24f
+ override val height = 9.52f
+ }
+
+ data object Zero : BatteryGlyph, SinglePathGlyph {
+ override val path =
+ Path().apply {
+ addSvg(
+ "M2.728,9.633C1.87,9.633 1.201,9.318 0.72,8.69C0.239,8.062 -0.002,7.071 -0.002,5.719L-0.002,3.906C-0.002,2.554 0.239,1.566 0.72,0.942C1.201,0.314 1.87,-0.001 2.728,-0.001C3.587,-0.001 4.256,0.314 4.737,0.942C5.218,1.566 5.458,2.552 5.458,3.9L5.458,5.719C5.458,7.067 5.214,8.057 4.724,8.69C4.239,9.318 3.574,9.633 2.728,9.633ZM2.728,7.878C2.98,7.878 3.168,7.758 3.294,7.52C3.424,7.277 3.489,6.848 3.489,6.233L3.489,3.399C3.489,2.784 3.426,2.357 3.3,2.118C3.175,1.876 2.984,1.754 2.728,1.754C2.473,1.754 2.282,1.876 2.156,2.118C2.035,2.357 1.975,2.784 1.975,3.399L1.975,6.233C1.975,6.848 2.037,7.277 2.163,7.52C2.293,7.758 2.482,7.878 2.728,7.878Z"
+ )
+ }
+
+ override val width = 5.46f
+ override val height = 9.63f
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/binder/UnifiedBatteryViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/binder/UnifiedBatteryViewBinder.kt
new file mode 100644
index 000000000000..903844efa3f0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/binder/UnifiedBatteryViewBinder.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.ui.binder
+
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.core.view.isVisible
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark
+import com.android.systemui.statusbar.pipeline.battery.ui.composable.UnifiedBattery
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel
+import kotlinx.coroutines.flow.Flow
+
+/** In cases where the battery needs to be bound to an existing android view */
+object UnifiedBatteryViewBinder {
+ /** Seats the [UnifiedBattery] into the given [ComposeView] root. */
+ @JvmStatic
+ fun bind(
+ view: ComposeView,
+ viewModelFactory: BatteryViewModel.Factory,
+ isAreaDark: Flow<IsAreaDark>,
+ ) {
+ view.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ view.apply {
+ isVisible = true
+ setViewCompositionStrategy(
+ ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
+ )
+ setContent {
+ val isDark by isAreaDark.collectAsStateWithLifecycle(IsAreaDark { true })
+ UnifiedBattery(viewModelFactory = viewModelFactory, isDark = isDark)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithEstimate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithEstimate.kt
new file mode 100644
index 000000000000..2ee86ee0a679
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithEstimate.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.ui.composable
+
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel
+
+@Composable
+fun BatteryWithEstimate(
+ viewModelFactory: BatteryViewModel.Factory,
+ isDark: IsAreaDark,
+ showEstimate: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ val viewModel =
+ rememberViewModel(traceName = "BatteryWithEstimate") { viewModelFactory.create() }
+
+ Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
+ UnifiedBattery(
+ viewModelFactory = viewModelFactory,
+ isDark = isDark,
+ modifier =
+ Modifier.fillMaxHeight()
+ .padding(vertical = 2.dp)
+ .align(Alignment.Bottom)
+ .aspectRatio(viewModel.aspectRatio),
+ )
+ if (showEstimate) {
+ viewModel.batteryTimeRemainingEstimate?.let {
+ Spacer(modifier.width(2.dp))
+ Text(text = it, color = Color.White)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt
new file mode 100644
index 000000000000..732ea6ac6790
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.ui.composable
+
+import android.graphics.Rect
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.drawscope.clipRect
+import androidx.compose.ui.graphics.drawscope.inset
+import androidx.compose.ui.graphics.drawscope.scale
+import androidx.compose.ui.layout.onLayoutRectChanged
+import com.android.systemui.common.ui.compose.load
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark
+import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryColors
+import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryFrame
+import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryGlyph
+import com.android.systemui.statusbar.pipeline.battery.shared.ui.PathSpec
+import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel
+import kotlin.math.ceil
+
+/**
+ * Draws a battery directly on to a [Canvas]. The canvas is scaled to fill its container, and the
+ * resulting battery is scaled using a FIT_CENTER type scaling that preserves the aspect ratio.
+ */
+@Composable
+fun BatteryCanvas(
+ path: PathSpec,
+ innerWidth: Float,
+ innerHeight: Float,
+ glyphs: List<BatteryGlyph>,
+ level: Int,
+ isFull: Boolean,
+ colorsProvider: () -> BatteryColors,
+ modifier: Modifier = Modifier,
+ contentDescription: String = "",
+) {
+
+ val totalWidth by
+ remember(glyphs) {
+ mutableFloatStateOf(
+ if (glyphs.isEmpty()) {
+ 0f
+ } else {
+ // Pads in between each glyph, skipping the first
+ glyphs.drop(1).fold(glyphs.first().width) { acc: Float, next: BatteryGlyph ->
+ acc + INTER_GLYPH_PADDING_PX + next.width
+ }
+ }
+ )
+ }
+
+ Canvas(modifier = modifier.fillMaxSize(), contentDescription = contentDescription) {
+ val scale = path.scaleTo(size.width, size.height)
+ val colors = colorsProvider()
+
+ scale(scale, pivot = Offset.Zero) {
+ if (isFull) {
+ // Saves a layer since we don't need background here
+ drawPath(path = path.path, color = colors.fill)
+ } else {
+ // First draw the body
+ drawPath(path.path, colors.background)
+ // Then draw the body, clipped to the fill level
+ clipRect(0f, 0f, innerWidth, innerHeight) {
+ drawRoundRect(
+ color = colors.fill,
+ topLeft = Offset.Zero,
+ size = Size(width = level.scaledLevel(), height = innerHeight),
+ cornerRadius = CornerRadius(2f),
+ )
+ }
+ }
+
+ // Now draw the glyphs
+ var horizontalOffset = (BatteryFrame.innerWidth - totalWidth) / 2
+ for (glyph in glyphs) {
+ // Move the glyph to the right spot
+ val verticalOffset = (BatteryFrame.innerHeight - glyph.height) / 2
+ inset(horizontalOffset, verticalOffset) { glyph.draw(this, colors) }
+
+ horizontalOffset += glyph.width + INTER_GLYPH_PADDING_PX
+ }
+ }
+ }
+}
+
+// Experimentally-determined value
+private const val INTER_GLYPH_PADDING_PX = 0.8f
+
+@Composable
+fun UnifiedBattery(
+ viewModelFactory: BatteryViewModel.Factory,
+ isDark: IsAreaDark,
+ modifier: Modifier = Modifier,
+) {
+ val viewModel = rememberViewModel(traceName = "UnifiedBattery") { viewModelFactory.create() }
+ val path = viewModel.batteryFrame
+
+ var bounds by remember { mutableStateOf(Rect()) }
+
+ val colorProvider = {
+ if (isDark.isDark(bounds)) {
+ viewModel.colorProfile.dark
+ } else {
+ viewModel.colorProfile.light
+ }
+ }
+
+ BatteryCanvas(
+ path = path,
+ innerWidth = viewModel.innerWidth,
+ innerHeight = viewModel.innerHeight,
+ glyphs = viewModel.glyphList,
+ level = viewModel.level,
+ isFull = viewModel.isFull,
+ colorsProvider = colorProvider,
+ modifier =
+ modifier.onLayoutRectChanged { relativeLayoutBounds ->
+ bounds =
+ with(relativeLayoutBounds.boundsInScreen) { Rect(left, top, right, bottom) }
+ },
+ contentDescription = viewModel.contentDescription.load() ?: "",
+ )
+}
+
+/** Calculate the right-edge of the clip for the fill-rect, based on a level of [0-100] */
+private fun Int.scaledLevel(): Float {
+ val endSide = BatteryFrame.innerWidth
+ return ceil((toFloat() / 100f) * endSide)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/model/AttributionGlyph.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/model/AttributionGlyph.kt
new file mode 100644
index 000000000000..aef8afe7c8ae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/model/AttributionGlyph.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.ui.model
+
+import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryGlyph
+
+/**
+ * Wrapper around potentially 2 glpyhs. This will allow the composable to draw the larger one if it
+ * is the only one displayed. For example, if the percentage is not showing and the device is
+ * plugged in, then we can show the larger charging bolt.
+ */
+data class AttributionGlyph(
+ /** Can be used when this glyph is alongside any others */
+ val inline: BatteryGlyph,
+ /** Can be used when this glyph is the only foreground element */
+ val standalone: BatteryGlyph,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt
new file mode 100644
index 000000000000..d0d099e74cb1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.ui.viewmodel
+
+import android.content.Context
+import androidx.compose.runtime.getValue
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryAttributionModel.Charging
+import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryAttributionModel.Defend
+import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryAttributionModel.PowerSave
+import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryInteractor
+import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryColors
+import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryFrame
+import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryGlyph
+import com.android.systemui.statusbar.pipeline.battery.ui.model.AttributionGlyph
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+
+/** View-model for the unified, compose-based battery icon. */
+@OptIn(ExperimentalCoroutinesApi::class)
+class BatteryViewModel
+@AssistedInject
+constructor(interactor: BatteryInteractor, @Application context: Context) : ExclusiveActivatable() {
+ private val hydrator: Hydrator = Hydrator("BatteryViewModel.hydrator")
+
+ val batteryFrame = BatteryFrame.pathSpec
+ val innerWidth = BatteryFrame.innerWidth
+ val innerHeight = BatteryFrame.innerHeight
+ val aspectRatio = BatteryFrame.innerWidth / BatteryFrame.innerHeight
+
+ val level by
+ hydrator.hydratedStateOf(traceName = "level", initialValue = 0, source = interactor.level)
+
+ val isFull by
+ hydrator.hydratedStateOf(
+ traceName = "isFull",
+ initialValue = false,
+ source = interactor.isFull,
+ )
+
+ /** The current attribution, if any */
+ private val attributionGlyph: Flow<AttributionGlyph?> =
+ interactor.batteryAttributionType.map {
+ when (it) {
+ Charging ->
+ AttributionGlyph(
+ inline = BatteryGlyph.Bolt,
+ standalone = BatteryGlyph.BoltLarge,
+ )
+
+ PowerSave ->
+ AttributionGlyph(
+ inline = BatteryGlyph.Plus,
+ standalone = BatteryGlyph.PlusLarge,
+ )
+
+ Defend ->
+ AttributionGlyph(
+ inline = BatteryGlyph.Defend,
+ standalone = BatteryGlyph.DefendLarge,
+ )
+
+ else -> null
+ }
+ }
+
+ /** A [List<BatteryGlyph>] representation of the current [level] */
+ private val levelGlyphs: Flow<List<BatteryGlyph>> =
+ interactor.level.map { it.glyphRepresentation() }
+
+ private val _glyphList: Flow<List<BatteryGlyph>> =
+ interactor.isBatteryPercentSettingEnabled.flatMapLatest {
+ if (it) {
+ combine(interactor.isFull, levelGlyphs, attributionGlyph) {
+ isFull,
+ levelGlyphs,
+ attr ->
+ // Don't ever show "100<attr>", since it won't fit. Just show the attr
+ if (isFull && attr != null) {
+ listOf(attr.standalone)
+ } else if (attr != null) {
+ levelGlyphs + attr.inline
+ } else {
+ levelGlyphs
+ }
+ }
+ } else {
+ attributionGlyph.map { attr ->
+ if (attr == null) {
+ emptyList()
+ } else {
+ listOf(attr.standalone)
+ }
+ }
+ }
+ }
+
+ /** For the status bar battery, this is the complete set of glyphs to show */
+ val glyphList: List<BatteryGlyph> by
+ hydrator.hydratedStateOf(
+ traceName = "glyphList",
+ initialValue = emptyList(),
+ source = _glyphList,
+ )
+
+ private val _colorProfile: Flow<ColorProfile> =
+ combine(interactor.batteryAttributionType, interactor.isCritical) { attr, isCritical ->
+ when (attr) {
+ Charging,
+ Defend ->
+ ColorProfile(
+ dark = BatteryColors.DarkThemeChargingColors,
+ light = BatteryColors.LightThemeChargingColors,
+ )
+ PowerSave ->
+ ColorProfile(
+ dark = BatteryColors.DarkThemePowerSaveColors,
+ light = BatteryColors.LightThemePowerSaveColors,
+ )
+ else ->
+ if (isCritical) {
+ ColorProfile(
+ dark = BatteryColors.DarkThemeErrorColors,
+ light = BatteryColors.LightThemeErrorColors,
+ )
+ } else {
+ ColorProfile(
+ dark = BatteryColors.DarkThemeDefaultColors,
+ light = BatteryColors.LightThemeDefaultColors,
+ )
+ }
+ }
+ }
+
+ /** For the current battery state, what is the relevant color profile to use */
+ val colorProfile: ColorProfile by
+ hydrator.hydratedStateOf(
+ traceName = "colorProfile",
+ initialValue =
+ ColorProfile(
+ dark = BatteryColors.DarkThemeDefaultColors,
+ light = BatteryColors.LightThemeDefaultColors,
+ ),
+ source = _colorProfile,
+ )
+
+ val contentDescription: ContentDescription by
+ hydrator.hydratedStateOf(
+ traceName = "contentDescription",
+ initialValue = ContentDescription.Loaded(null),
+ source =
+ combine(
+ interactor.batteryAttributionType,
+ interactor.isStateUnknown,
+ interactor.level,
+ ) { attr, isUnknown, level ->
+ when {
+ isUnknown ->
+ ContentDescription.Resource(R.string.accessibility_battery_unknown)
+ attr == Defend -> {
+ val descr =
+ context.getString(
+ R.string.accessibility_battery_level_charging_paused,
+ level,
+ )
+
+ ContentDescription.Loaded(descr)
+ }
+ attr == Charging -> {
+ val descr =
+ context.getString(
+ R.string.accessibility_battery_level_charging,
+ level,
+ )
+ ContentDescription.Loaded(descr)
+ }
+ else -> {
+ val descr =
+ context.getString(R.string.accessibility_battery_level, level)
+ ContentDescription.Loaded(descr)
+ }
+ }
+ },
+ )
+
+ val batteryTimeRemainingEstimate: String? by
+ hydrator.hydratedStateOf(
+ traceName = "timeRemainingEstimate",
+ initialValue = null,
+ source = interactor.batteryTimeRemainingEstimate,
+ )
+
+ override suspend fun onActivated(): Nothing {
+ hydrator.activate()
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): BatteryViewModel
+ }
+
+ companion object {
+ fun Int.glyphRepresentation(): List<BatteryGlyph> = toString().map { it.toGlyph() }
+
+ private fun Char.toGlyph(): BatteryGlyph =
+ when (this) {
+ '0' -> BatteryGlyph.Zero
+ '1' -> BatteryGlyph.One
+ '2' -> BatteryGlyph.Two
+ '3' -> BatteryGlyph.Three
+ '4' -> BatteryGlyph.Four
+ '5' -> BatteryGlyph.Five
+ '6' -> BatteryGlyph.Six
+ '7' -> BatteryGlyph.Seven
+ '8' -> BatteryGlyph.Eight
+ '9' -> BatteryGlyph.Nine
+ else -> throw IllegalArgumentException("cannot make glyph from char ($this)")
+ }
+ }
+}
+
+/** Wrap the light and dark color into a single object so the view can decide which one it needs */
+data class ColorProfile(val dark: BatteryColors, val light: BatteryColors)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
index 2fc22867e702..7879f971e193 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.policy.ui.dialog
-import android.annotation.UiThread;
+import android.annotation.UiThread
import android.app.Dialog
import android.content.Context
import android.content.Intent
@@ -44,6 +44,8 @@ import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dialog.ui.composable.AlertDialogContent
import com.android.systemui.plugins.ActivityStarter
@@ -60,6 +62,8 @@ import com.android.systemui.util.Assert
import javax.inject.Inject
import javax.inject.Provider
import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@SysUISingleton
@@ -73,7 +77,9 @@ constructor(
// Using a provider to avoid a circular dependency.
private val viewModel: Provider<ModesDialogViewModel>,
private val dialogEventLogger: ModesDialogEventLogger,
+ @Application private val applicationCoroutineScope: CoroutineScope,
@Main private val mainCoroutineContext: CoroutineContext,
+ @Background private val bgContext: CoroutineContext,
private val shadeDisplayContextRepository: ShadeDialogContextInteractor,
) : SystemUIDialog.Delegate {
// NOTE: This should only be accessed/written from the main thread.
@@ -185,6 +191,18 @@ constructor(
* launches it normally without animating.
*/
fun launchFromDialog(intent: Intent) {
+ // TODO: b/394571336 - Remove this method and inline "actual" if b/394571336 fixed.
+ // Workaround for Compose bug, see b/394241061 and b/394571336 -- Need to post on the main
+ // thread so that dialog dismissal doesn't crash after a long press inside it (the *double*
+ // jump, out and back in, is because mainCoroutineContext is .immediate).
+ applicationCoroutineScope.launch {
+ withContext(bgContext) {
+ withContext(mainCoroutineContext) { actualLaunchFromDialog(intent) }
+ }
+ }
+ }
+
+ private fun actualLaunchFromDialog(intent: Intent) {
Assert.isMainThread()
if (currentDialog == null) {
Log.w(
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index ad97b21ea60b..c960b5525d96 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -21,6 +21,7 @@ import android.annotation.SuppressLint
import android.annotation.UserIdInt
import android.app.admin.DevicePolicyManager
import android.content.Context
+import android.content.Intent
import android.content.IntentFilter
import android.content.pm.UserInfo
import android.content.res.Resources
@@ -84,6 +85,9 @@ interface UserRepository {
/** [UserInfo] of the currently-selected user. */
val selectedUserInfo: Flow<UserInfo>
+ /** Tracks whether the main user is unlocked. */
+ fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean>
+
/** User ID of the main user. */
val mainUserId: Int
@@ -284,6 +288,18 @@ constructor(
}
.stateIn(applicationScope, SharingStarted.Eagerly, false)
+ override fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> =
+ broadcastDispatcher
+ .broadcastFlow(IntentFilter(Intent.ACTION_USER_UNLOCKED))
+ .map { getUnlockedState(userHandle) }
+ .onStart { emit(getUnlockedState(userHandle)) }
+
+ private suspend fun getUnlockedState(userHandle: UserHandle?): Boolean {
+ return withContext(backgroundDispatcher) {
+ userHandle?.let { user -> manager.isUserUnlocked(user) } ?: false
+ }
+ }
+
@SuppressLint("MissingPermission")
override val isLogoutToSystemUserEnabled: StateFlow<Boolean> =
selectedUser
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt
new file mode 100644
index 000000000000..ef29a387e06f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserLockedInteractor.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.user.domain.interactor
+
+import android.os.UserHandle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.user.data.repository.UserRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+@SysUISingleton
+class UserLockedInteractor @Inject constructor(val userRepository: UserRepository) {
+ fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> =
+ userRepository.isUserUnlocked(userHandle)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
index 7e7527ea4be3..735da46667c5 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
@@ -16,6 +16,7 @@
package com.android.systemui.util.kotlin
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.util.time.SystemClock
import com.android.systemui.util.time.SystemClockImpl
import java.util.concurrent.atomic.AtomicReference
@@ -33,7 +34,6 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
-import com.android.app.tracing.coroutines.launchTraced as launch
/**
* Returns a new [Flow] that combines the two most recent emissions from [this] using [transform].
@@ -246,24 +246,24 @@ fun <T> Flow<T>.throttle(periodMs: Long, clock: SystemClock = SystemClockImpl())
}
inline fun <T1, T2, T3, T4, T5, T6, R> combine(
- flow: Flow<T1>,
- flow2: Flow<T2>,
- flow3: Flow<T3>,
- flow4: Flow<T4>,
- flow5: Flow<T5>,
- flow6: Flow<T6>,
- crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R
+ flow: Flow<T1>,
+ flow2: Flow<T2>,
+ flow3: Flow<T3>,
+ flow4: Flow<T4>,
+ flow5: Flow<T5>,
+ flow6: Flow<T6>,
+ crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R,
): Flow<R> {
- return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) {
- args: Array<*> ->
+ return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*>
+ ->
@Suppress("UNCHECKED_CAST")
transform(
- args[0] as T1,
- args[1] as T2,
- args[2] as T3,
- args[3] as T4,
- args[4] as T5,
- args[5] as T6,
+ args[0] as T1,
+ args[1] as T2,
+ args[2] as T3,
+ args[3] as T4,
+ args[4] as T5,
+ args[5] as T6,
)
}
}
@@ -276,7 +276,7 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
flow5: Flow<T5>,
flow6: Flow<T6>,
flow7: Flow<T7>,
- crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
+ crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R,
): Flow<R> {
return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) {
args: Array<*> ->
@@ -288,7 +288,7 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
args[3] as T4,
args[4] as T5,
args[5] as T6,
- args[6] as T7
+ args[6] as T7,
)
}
}
@@ -302,7 +302,7 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine(
flow6: Flow<T6>,
flow7: Flow<T7>,
flow8: Flow<T8>,
- crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R
+ crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R,
): Flow<R> {
return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) {
args: Array<*> ->
@@ -315,7 +315,7 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine(
args[4] as T5,
args[5] as T6,
args[6] as T7,
- args[7] as T8
+ args[7] as T8,
)
}
}
@@ -330,7 +330,7 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, R> combine(
flow7: Flow<T7>,
flow8: Flow<T8>,
flow9: Flow<T9>,
- crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R
+ crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9) -> R,
): Flow<R> {
return kotlinx.coroutines.flow.combine(
flow,
@@ -341,7 +341,7 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, R> combine(
flow6,
flow7,
flow8,
- flow9
+ flow9,
) { args: Array<*> ->
@Suppress("UNCHECKED_CAST")
transform(
@@ -352,8 +352,8 @@ inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, R> combine(
args[4] as T5,
args[5] as T6,
args[6] as T7,
- args[6] as T8,
- args[6] as T9,
+ args[7] as T8,
+ args[8] as T9,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt
index 6b7de982e00a..22a74c86e0f1 100644
--- a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt
@@ -16,10 +16,8 @@
package com.android.systemui.window.data.repository
-import android.annotation.SuppressLint
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
/** Repository that maintains state for the window blur effect. */
@@ -28,6 +26,4 @@ class WindowRootViewBlurRepository @Inject constructor() {
val blurRadius = MutableStateFlow(0)
val isBlurOpaque = MutableStateFlow(false)
-
- @SuppressLint("SharedFlowCreation") val onBlurApplied = MutableSharedFlow<Int>()
}
diff --git a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt
index e21e0a1cadc7..9e369347dea5 100644
--- a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.window.domain.interactor
+import android.annotation.SuppressLint
import android.util.Log
import com.android.systemui.Flags
import com.android.systemui.communal.domain.interactor.CommunalInteractor
@@ -28,6 +29,7 @@ import com.android.systemui.window.data.repository.WindowRootViewBlurRepository
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -52,6 +54,8 @@ constructor(
private val communalInteractor: CommunalInteractor,
private val repository: WindowRootViewBlurRepository,
) {
+ @SuppressLint("SharedFlowCreation") private val _onBlurAppliedEvent = MutableSharedFlow<Int>()
+
private var isBouncerTransitionInProgress: StateFlow<Boolean> =
if (Flags.bouncerUiRevamp()) {
keyguardTransitionInteractor
@@ -68,7 +72,7 @@ constructor(
* root view.
*/
suspend fun onBlurApplied(appliedBlurRadius: Int) {
- repository.onBlurApplied.emit(appliedBlurRadius)
+ _onBlurAppliedEvent.emit(appliedBlurRadius)
}
/** Radius of blur to be applied on the window root view. */
@@ -77,7 +81,7 @@ constructor(
/**
* Emits the applied blur radius whenever blur is successfully applied to the window root view.
*/
- val onBlurAppliedEvent: Flow<Int> = repository.onBlurApplied
+ val onBlurAppliedEvent: Flow<Int> = _onBlurAppliedEvent
/** Whether the blur applied is opaque or transparent. */
val isBlurOpaque: Flow<Boolean> =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index e8054c07eac8..4ccfa29d4ba0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -206,7 +206,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
private @Mock ShadeInteractor mShadeInteractor;
private @Mock ShadeWindowLogger mShadeWindowLogger;
private @Mock SelectedUserInteractor mSelectedUserInteractor;
- private @Mock UserTracker.Callback mUserTrackerCallback;
private @Mock KeyguardInteractor mKeyguardInteractor;
private @Mock KeyguardTransitionBootInteractor mKeyguardTransitionBootInteractor;
private @Captor ArgumentCaptor<KeyguardStateController.Callback>
@@ -281,7 +280,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
() -> mShadeInteractor,
mShadeWindowLogger,
() -> mSelectedUserInteractor,
- mock(UserTracker.class),
+ mUserTracker,
mKosmos.getNotificationShadeWindowModel(),
mSecureSettings,
mKosmos::getCommunalInteractor,
@@ -319,7 +318,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
} catch (Exception e) {
// Just so we don't have to add the exception signature to every test.
- fail(e.getMessage());
+ fail();
}
}
@@ -331,156 +330,18 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
/* First test the default behavior: handleUserSwitching() is not invoked */
when(mUserTracker.isUserSwitching()).thenReturn(false);
+ mViewMediator.mUpdateCallback = mock(KeyguardUpdateMonitorCallback.class);
mViewMediator.onSystemReady();
TestableLooper.get(this).processAllMessages();
- verify(mUserTrackerCallback, never()).onUserChanging(eq(userId), eq(mContext),
- any(Runnable.class));
+ verify(mViewMediator.mUpdateCallback, never()).onUserSwitching(userId);
/* Next test user switching is already in progress when started */
when(mUserTracker.isUserSwitching()).thenReturn(true);
mViewMediator.onSystemReady();
TestableLooper.get(this).processAllMessages();
- verify(mUserTrackerCallback).onUserChanging(eq(userId), eq(mContext),
- any(Runnable.class));
- }
-
- @Test
- @TestableLooper.RunWithLooper(setAsMainLooper = true)
- public void testGoingAwayFollowedByBeforeUserSwitchDoesNotHideKeyguard() {
- setCurrentUser(/* userId= */1099, /* isSecure= */false);
-
- // Setup keyguard
- mViewMediator.onSystemReady();
- processAllMessagesAndBgExecutorMessages();
- mViewMediator.setShowingLocked(true, "");
-
- // Request keyguard going away
- when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true);
- mViewMediator.mKeyguardGoingAwayRunnable.run();
-
- // After the request, begin a switch to a new secure user
- int nextUserId = 500;
- setCurrentUser(nextUserId, /* isSecure= */true);
- Runnable result = mock(Runnable.class);
- mViewMediator.handleBeforeUserSwitching(nextUserId, result);
- processAllMessagesAndBgExecutorMessages();
- verify(result).run();
-
- // After that request has begun, have WM tell us to exit keyguard
- RemoteAnimationTarget[] apps = new RemoteAnimationTarget[]{
- mock(RemoteAnimationTarget.class)
- };
- RemoteAnimationTarget[] wallpapers = new RemoteAnimationTarget[]{
- mock(RemoteAnimationTarget.class)
- };
- IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class);
- mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers,
- null, callback);
- processAllMessagesAndBgExecutorMessages();
-
- // The call to exit should be rejected, and keyguard should still be visible
- verify(mKeyguardUnlockAnimationController, never()).notifyStartSurfaceBehindRemoteAnimation(
- any(), any(), any(), anyLong(), anyBoolean());
- try {
- assertATMSLockScreenShowing(true);
- } catch (Exception e) {
- fail(e.getMessage());
- }
- assertTrue(mViewMediator.isShowingAndNotOccluded());
- }
-
- @Test
- @TestableLooper.RunWithLooper(setAsMainLooper = true)
- public void testUserSwitchToSecureUserShowsBouncer() {
- setCurrentUser(/* userId= */1099, /* isSecure= */true);
-
- // Setup keyguard
- mViewMediator.onSystemReady();
- processAllMessagesAndBgExecutorMessages();
- mViewMediator.setShowingLocked(true, "");
-
- // After the request, begin a switch to a new secure user
- int nextUserId = 500;
- setCurrentUser(nextUserId, /* isSecure= */true);
-
- Runnable beforeResult = mock(Runnable.class);
- mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult);
- processAllMessagesAndBgExecutorMessages();
- verify(beforeResult).run();
-
- // Dismiss should not be called while user switch is in progress
- Runnable onSwitchResult = mock(Runnable.class);
- mViewMediator.handleUserSwitching(nextUserId, onSwitchResult);
- processAllMessagesAndBgExecutorMessages();
- verify(onSwitchResult).run();
- verify(mStatusBarKeyguardViewManager, never()).dismissAndCollapse();
-
- // The attempt to dismiss only comes on user switch complete, which will trigger a call to
- // show the bouncer in StatusBarKeyguardViewManager
- mViewMediator.handleUserSwitchComplete(nextUserId);
- TestableLooper.get(this).moveTimeForward(600);
- processAllMessagesAndBgExecutorMessages();
-
- verify(mStatusBarKeyguardViewManager).dismissAndCollapse();
- }
-
- @Test
- @TestableLooper.RunWithLooper(setAsMainLooper = true)
- public void testUserSwitchToInsecureUserDismissesKeyguard() {
- int userId = 1099;
- when(mUserTracker.getUserId()).thenReturn(userId);
-
- // Setup keyguard
- mViewMediator.onSystemReady();
- processAllMessagesAndBgExecutorMessages();
- mViewMediator.setShowingLocked(true, "");
-
- // After the request, begin a switch to an insecure user
- int nextUserId = 500;
- when(mLockPatternUtils.isSecure(nextUserId)).thenReturn(false);
-
- Runnable beforeResult = mock(Runnable.class);
- mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult);
- processAllMessagesAndBgExecutorMessages();
- verify(beforeResult).run();
-
- // The call to dismiss comes during the user switch
- Runnable onSwitchResult = mock(Runnable.class);
- mViewMediator.handleUserSwitching(nextUserId, onSwitchResult);
- processAllMessagesAndBgExecutorMessages();
- verify(onSwitchResult).run();
-
- verify(mStatusBarKeyguardViewManager).dismissAndCollapse();
- }
-
- @Test
- @TestableLooper.RunWithLooper(setAsMainLooper = true)
- public void testUserSwitchToSecureUserWhileKeyguardNotVisibleShowsKeyguard() {
- setCurrentUser(/* userId= */1099, /* isSecure= */true);
-
- // Setup keyguard as not visible
- mViewMediator.onSystemReady();
- processAllMessagesAndBgExecutorMessages();
- mViewMediator.setShowingLocked(false, "");
- processAllMessagesAndBgExecutorMessages();
-
- // Begin a switch to a new secure user
- int nextUserId = 500;
- setCurrentUser(nextUserId, /* isSecure= */true);
-
- Runnable beforeResult = mock(Runnable.class);
- mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult);
- processAllMessagesAndBgExecutorMessages();
- verify(beforeResult).run();
-
- try {
- assertATMSLockScreenShowing(true);
- } catch (Exception e) {
- fail();
- }
- assertTrue(mViewMediator.isShowingAndNotOccluded());
+ verify(mViewMediator.mUpdateCallback).onUserSwitching(userId);
}
@Test
@@ -1244,7 +1105,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
processAllMessagesAndBgExecutorMessages();
verify(mStatusBarKeyguardViewManager, never()).reset(anyBoolean());
-
+ assertATMSAndKeyguardViewMediatorStatesMatch();
}
@Test
@@ -1288,7 +1149,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class);
when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true);
- mViewMediator.mKeyguardGoingAwayRunnable.run();
mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers,
null, callback);
processAllMessagesAndBgExecutorMessages();
@@ -1343,6 +1203,13 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
// The captor will have the most recent setLockScreenShown call's value.
assertEquals(showing, showingCaptor.getValue());
+
+ // We're now just after the last setLockScreenShown call. If we expect the lockscreen to be
+ // showing, ensure that we didn't subsequently ask for it to go away.
+ if (showing) {
+ orderedSetLockScreenShownCalls.verify(mActivityTaskManagerService, never())
+ .keyguardGoingAway(anyInt());
+ }
}
/**
@@ -1504,7 +1371,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
mKeyguardTransitionBootInteractor,
mKosmos::getCommunalSceneInteractor,
mock(WindowManagerOcclusionManager.class));
- mViewMediator.mUserChangedCallback = mUserTrackerCallback;
mViewMediator.start();
mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null);
@@ -1518,10 +1384,4 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
private void captureKeyguardUpdateMonitorCallback() {
verify(mUpdateMonitor).registerCallback(mKeyguardUpdateMonitorCallbackCaptor.capture());
}
-
- private void setCurrentUser(int userId, boolean isSecure) {
- when(mUserTracker.getUserId()).thenReturn(userId);
- when(mSelectedUserInteractor.getSelectedUserId()).thenReturn(userId);
- when(mLockPatternUtils.isSecure(userId)).thenReturn(isSecure);
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
index 86094d1a0fef..88c2697fe2f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java
@@ -1519,7 +1519,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
assertThat(getNumberOfConnectDeviceButtons()).isEqualTo(1);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void selectedDevicesAddedInSameOrder() {
when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true);
@@ -1537,7 +1537,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
assertThat(items.get(1).getMediaDevice().get()).isEqualTo(mMediaDevice2);
}
- @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void selectedDevicesAddedInReverseOrder() {
when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true);
@@ -1555,7 +1555,7 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
assertThat(items.get(1).getMediaDevice().get()).isEqualTo(mMediaDevice1);
}
- @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_SESSION_GROUPING)
+ @EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING)
@Test
public void firstSelectedDeviceIsFirstDeviceInGroupIsTrue() {
when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
index b75dd0402175..bc16f7e0df5f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextViewTest.kt
@@ -58,6 +58,7 @@ class SimpleDigitalClockTextViewTest : SysuiTestCase() {
},
ClockMessageBuffers(messageBuffer),
messageBuffer,
+ vibrator = null,
),
isLargeClock = false,
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/data/repository/BatteryRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/data/repository/BatteryRepositoryTest.kt
new file mode 100644
index 000000000000..3953ebefafc6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/data/repository/BatteryRepositoryTest.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.data.repository
+
+import android.content.testableContext
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.backgroundScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shared.settings.data.repository.fakeSystemSettingsRepository
+import com.android.systemui.shared.settings.data.repository.systemSettingsRepository
+import com.android.systemui.statusbar.policy.batteryController
+import com.android.systemui.statusbar.policy.fake
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlin.time.Duration.Companion.minutes
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceTimeBy
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BatteryRepositoryTest : SysuiTestCase() {
+ val kosmos = testKosmos()
+
+ val Kosmos.underTest by
+ Kosmos.Fixture {
+ BatteryRepository(
+ testableContext,
+ backgroundScope,
+ testDispatcher,
+ batteryController,
+ systemSettingsRepository,
+ )
+ }
+
+ @Test
+ fun pluggedIn() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.isPluggedIn)
+
+ assertThat(latest).isFalse()
+
+ batteryController.fake._isPluggedIn = true
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun powerSave() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.isPowerSaveEnabled)
+
+ assertThat(latest).isFalse()
+
+ batteryController.fake._isPowerSave = true
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun defend() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.isBatteryDefenderEnabled)
+
+ assertThat(latest).isFalse()
+
+ batteryController.fake._isDefender = true
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun level() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.level)
+
+ batteryController.fake._level = 42
+
+ assertThat(latest).isEqualTo(42)
+
+ batteryController.fake._level = 84
+
+ assertThat(latest).isEqualTo(84)
+ }
+
+ @Test
+ fun isStateUnknown() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.isStateUnknown)
+
+ assertThat(latest).isFalse()
+
+ batteryController.fake._isStateUnknown = true
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun showBatteryPercentSetting() =
+ kosmos.runTest {
+ // Set the default to true, so it's detectable in test
+ testableContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_defaultBatteryPercentageSetting,
+ true,
+ )
+
+ val latest by collectLastValue(underTest.isShowBatteryPercentSettingEnabled)
+
+ assertThat(latest).isTrue()
+
+ fakeSystemSettingsRepository.setInt(Settings.System.SHOW_BATTERY_PERCENT, 0)
+
+ assertThat(latest).isFalse()
+
+ fakeSystemSettingsRepository.setInt(Settings.System.SHOW_BATTERY_PERCENT, 1)
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun batteryRemainingEstimateString_queriesEveryTwoMinutes() =
+ kosmos.runTest {
+ batteryController.fake._estimatedTimeRemainingString = null
+
+ val latest by collectLastValue(underTest.batteryTimeRemainingEstimate)
+
+ assertThat(latest).isNull()
+
+ batteryController.fake._estimatedTimeRemainingString = "test time remaining"
+
+ testScope.advanceTimeBy(2.minutes)
+
+ assertThat(latest).isEqualTo("test time remaining")
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractorTest.kt
new file mode 100644
index 000000000000..df45e2e21052
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractorTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.statusbar.policy.batteryController
+import com.android.systemui.statusbar.policy.fake
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BatteryInteractorTest : SysuiTestCase() {
+ val kosmos = testKosmos()
+ val Kosmos.underTest by Kosmos.Fixture { batteryInteractor }
+
+ @Test
+ fun batteryAttributionType() =
+ kosmos.runTest {
+ batteryController.fake._isPluggedIn = false
+ batteryController.fake._isDefender = false
+ batteryController.fake._isPowerSave = false
+
+ val latest by collectLastValue(underTest.batteryAttributionType)
+
+ assertThat(latest).isNull()
+
+ batteryController.fake._isDefender = true
+
+ assertThat(latest).isEqualTo(BatteryAttributionModel.Defend)
+
+ batteryController.fake._isPowerSave = true
+
+ assertThat(latest).isEqualTo(BatteryAttributionModel.PowerSave)
+
+ batteryController.fake._isPluggedIn = true
+
+ assertThat(latest).isEqualTo(BatteryAttributionModel.Charging)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelTest.kt
new file mode 100644
index 000000000000..6f4c745e8e7e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.ui.viewmodel
+
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.shared.settings.data.repository.fakeSystemSettingsRepository
+import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryGlyph
+import com.android.systemui.statusbar.policy.batteryController
+import com.android.systemui.statusbar.policy.fake
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BatteryViewModelTest : SysuiTestCase() {
+ val kosmos = testKosmos()
+ val Kosmos.underTest by Kosmos.Fixture { batteryViewModel }
+
+ @Before
+ fun setUp() {
+ kosmos.useUnconfinedTestDispatcher()
+ kosmos.underTest.activateIn(kosmos.testScope)
+ }
+
+ @Test
+ fun glyphList_notCharging_settingOff_isEmpty() =
+ kosmos.runTest {
+ fakeSystemSettingsRepository.setInt(Settings.System.SHOW_BATTERY_PERCENT, 0)
+ batteryController.fake._isPluggedIn = false
+ batteryController.fake._level = 42
+
+ assertThat(underTest.glyphList).isEmpty()
+ }
+
+ @Test
+ fun glyphList_notCharging_settingOn_hasOnlyLevelGlyphs() =
+ kosmos.runTest {
+ fakeSystemSettingsRepository.setInt(Settings.System.SHOW_BATTERY_PERCENT, 1)
+ batteryController.fake._isPluggedIn = false
+ batteryController.fake._level = 42
+
+ assertThat(underTest.glyphList).isEqualTo(listOf(BatteryGlyph.Four, BatteryGlyph.Two))
+ }
+
+ @Test
+ fun glyphList_charging_settingOn_notFull_hasLevelAndInlineGlyph() =
+ kosmos.runTest {
+ fakeSystemSettingsRepository.setInt(Settings.System.SHOW_BATTERY_PERCENT, 1)
+ batteryController.fake._isPluggedIn = true
+ batteryController.fake._level = 39
+
+ assertThat(underTest.glyphList)
+ .isEqualTo(listOf(BatteryGlyph.Three, BatteryGlyph.Nine, BatteryGlyph.Bolt))
+ }
+
+ @Test
+ fun glyphList_charging_settingOn_isFull_onlyHasLargeBolt() =
+ kosmos.runTest {
+ fakeSystemSettingsRepository.setInt(Settings.System.SHOW_BATTERY_PERCENT, 1)
+ batteryController.fake._isPluggedIn = true
+ batteryController.fake._level = 100
+
+ assertThat(underTest.glyphList).isEqualTo(listOf(BatteryGlyph.BoltLarge))
+ }
+
+ @Test
+ fun glyphList_charging_settingOff_notFull_onlyHasLargeGlyph() =
+ kosmos.runTest {
+ fakeSystemSettingsRepository.setInt(Settings.System.SHOW_BATTERY_PERCENT, 0)
+ batteryController.fake._isPluggedIn = true
+ batteryController.fake._level = 39
+
+ assertThat(underTest.glyphList).isEqualTo(listOf(BatteryGlyph.BoltLarge))
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt
index c936b914f44e..26618484669b 100644
--- a/packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/os/UserManagerKosmos.kt
@@ -17,6 +17,15 @@
package android.os
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.user.data.repository.FakeUserRepository.Companion.MAIN_USER_ID
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
-var Kosmos.userManager by Kosmos.Fixture { mock<UserManager>() }
+var Kosmos.userManager by
+ Kosmos.Fixture {
+ mock<UserManager> {
+ whenever(it.mainUser).thenReturn(UserHandle(MAIN_USER_ID))
+ whenever(it.getUserSerialNumber(eq(MAIN_USER_ID))).thenReturn(0)
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index b0a6de1f931a..0f21a16147f0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -42,7 +42,9 @@ import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.settings.userTracker
import com.android.systemui.statusbar.phone.fakeManagedProfileController
+import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.domain.interactor.userLockedInteractor
import com.android.systemui.util.mockito.mock
val Kosmos.communalInteractor by Fixture {
@@ -70,6 +72,7 @@ val Kosmos.communalInteractor by Fixture {
batteryInteractor = batteryInteractor,
dockManager = dockManager,
posturingInteractor = posturingInteractor,
+ userLockedInteractor = userLockedInteractor,
)
}
@@ -98,10 +101,8 @@ suspend fun Kosmos.setCommunalV2Enabled(enabled: Boolean) {
suspend fun Kosmos.setCommunalAvailable(available: Boolean) {
setCommunalEnabled(available)
- with(fakeKeyguardRepository) {
- setIsEncryptedOrLockdown(!available)
- setKeyguardShowing(available)
- }
+ fakeKeyguardRepository.setKeyguardShowing(available)
+ fakeUserRepository.setUserUnlocked(FakeUserRepository.MAIN_USER_ID, available)
}
suspend fun Kosmos.setCommunalV2Available(available: Boolean) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt
index 2e59788663f7..4480539b9a15 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/ui/viewmodel/DreamUserActionsViewModelKosmos.kt
@@ -16,18 +16,20 @@
package com.android.systemui.dreams.ui.viewmodel
-import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.domain.interactor.shadeModeInteractor
-val Kosmos.dreamUserActionsViewModel by
- Kosmos.Fixture {
- DreamUserActionsViewModel(
- communalInteractor = communalInteractor,
- deviceUnlockedInteractor = deviceUnlockedInteractor,
- shadeInteractor = shadeInteractor,
- shadeModeInteractor = shadeModeInteractor,
- )
+val Kosmos.dreamUserActionsViewModelFactory by Fixture {
+ object : DreamUserActionsViewModel.Factory {
+ override fun create(): DreamUserActionsViewModel {
+ return DreamUserActionsViewModel(
+ deviceUnlockedInteractor = deviceUnlockedInteractor,
+ shadeInteractor = shadeInteractor,
+ shadeModeInteractor = shadeModeInteractor,
+ )
+ }
}
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelKosmos.kt
index ec83157eb108..4f1774f957d6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenUserActionsViewModelKosmos.kt
@@ -16,7 +16,6 @@
package com.android.systemui.keyguard.ui.viewmodel
-import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
@@ -27,7 +26,6 @@ import com.android.systemui.shade.domain.interactor.shadeModeInteractor
val Kosmos.lockscreenUserActionsViewModel by Fixture {
LockscreenUserActionsViewModel(
deviceEntryInteractor = deviceEntryInteractor,
- communalInteractor = communalInteractor,
shadeInteractor = shadeInteractor,
shadeModeInteractor = shadeModeInteractor,
occlusionInteractor = sceneContainerOcclusionInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
index ce298bb90ba2..825e0143800b 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt
@@ -5,6 +5,8 @@ import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.classifier.domain.interactor.falsingInteractor
import com.android.systemui.haptics.msdl.msdlPlayer
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.keyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.lightRevealScrimViewModel
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
@@ -105,6 +107,8 @@ val Kosmos.sceneContainerViewModelFactory by Fixture {
lightRevealScrim = lightRevealScrimViewModel,
wallpaperViewModel = wallpaperViewModel,
keyguardInteractor = keyguardInteractor,
+ burnIn = aodBurnInViewModel,
+ clock = keyguardClockViewModel,
)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
index eb352baab0e4..2efc09f2682f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneInteractorKosmos.kt
@@ -23,6 +23,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.scene.data.repository.sceneContainerRepository
import com.android.systemui.scene.domain.resolver.sceneFamilyResolvers
import com.android.systemui.scene.shared.logger.sceneLogger
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
val Kosmos.sceneInteractor: SceneInteractor by
Kosmos.Fixture {
@@ -34,5 +35,6 @@ val Kosmos.sceneInteractor: SceneInteractor by
deviceUnlockedInteractor = { deviceUnlockedInteractor },
keyguardEnabledInteractor = { keyguardEnabledInteractor },
disabledContentInteractor = disabledContentInteractor,
+ shadeModeInteractor = shadeModeInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
index f5eebb46c2ec..60c0f342b874 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
@@ -77,6 +77,14 @@ class FakeSceneDataSource(initialSceneKey: SceneKey, val testScope: TestScope) :
showOverlay(to, transitionKey)
}
+ override fun instantlyShowOverlay(overlay: OverlayKey) {
+ showOverlay(overlay)
+ }
+
+ override fun instantlyHideOverlay(overlay: OverlayKey) {
+ hideOverlay(overlay)
+ }
+
/**
* Pauses scene and overlay changes.
*
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt
deleted file mode 100644
index 923b36d4f2cf..000000000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallModelBuilder.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.phone.ongoingcall.shared.model
-
-import android.app.PendingIntent
-import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
-
-/** Helper for building [OngoingCallModel.InCall] instances in tests. */
-fun inCallModel(
- startTimeMs: Long,
- notificationIcon: StatusBarIconView? = null,
- intent: PendingIntent? = null,
- notificationKey: String = "test",
- appName: String = "",
- promotedContent: PromotedNotificationContentModel? = null,
-) =
- OngoingCallModel.InCall(
- startTimeMs,
- notificationIcon,
- intent,
- notificationKey,
- appName,
- promotedContent,
- )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
new file mode 100644
index 000000000000..7bcedcaa99d1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone.ongoingcall.shared.model
+
+import android.app.PendingIntent
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
+import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
+import com.android.systemui.statusbar.notification.shared.CallType
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
+import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
+import org.mockito.kotlin.mock
+
+/** Helper for building [OngoingCallModel.InCall] instances in tests. */
+fun inCallModel(
+ startTimeMs: Long,
+ notificationIcon: StatusBarIconView? = null,
+ intent: PendingIntent? = null,
+ notificationKey: String = "test",
+ appName: String = "",
+ promotedContent: PromotedNotificationContentModel? = null,
+) =
+ OngoingCallModel.InCall(
+ startTimeMs,
+ notificationIcon,
+ intent,
+ notificationKey,
+ appName,
+ promotedContent,
+ )
+
+object OngoingCallTestHelper {
+ /**
+ * Sets the call state to be no call, and does it correctly based on whether
+ * [StatusBarChipsModernization] is enabled or not.
+ */
+ fun setNoCallState(kosmos: Kosmos) {
+ if (StatusBarChipsModernization.isEnabled) {
+ // TODO(b/372657935): Maybe don't clear *all* notifications
+ kosmos.activeNotificationListRepository.activeNotifications.value =
+ ActiveNotificationsStore()
+ } else {
+ kosmos.ongoingCallRepository.setOngoingCallState(OngoingCallModel.NoCall)
+ }
+ }
+
+ /**
+ * Sets the ongoing call state correctly based on whether [StatusBarChipsModernization] is
+ * enabled or not.
+ */
+ fun setOngoingCallState(
+ kosmos: Kosmos,
+ startTimeMs: Long = 1000L,
+ key: String = "notif",
+ statusBarChipIconView: StatusBarIconView? = createStatusBarIconViewOrNull(),
+ promotedContent: PromotedNotificationContentModel? = null,
+ contentIntent: PendingIntent? = null,
+ uid: Int = DEFAULT_UID,
+ ) {
+ if (StatusBarChipsModernization.isEnabled) {
+ kosmos.activeNotificationListRepository.activeNotifications.value =
+ ActiveNotificationsStore.Builder()
+ .apply {
+ addIndividualNotif(
+ activeNotificationModel(
+ key = key,
+ whenTime = startTimeMs,
+ callType = CallType.Ongoing,
+ statusBarChipIcon = statusBarChipIconView,
+ contentIntent = contentIntent,
+ promotedContent = promotedContent,
+ uid = uid,
+ )
+ )
+ }
+ .build()
+ } else {
+ kosmos.ongoingCallRepository.setOngoingCallState(
+ inCallModel(
+ startTimeMs = startTimeMs,
+ notificationIcon = statusBarChipIconView,
+ intent = contentIntent,
+ notificationKey = key,
+ promotedContent = promotedContent,
+ )
+ )
+ }
+ }
+
+ private fun createStatusBarIconViewOrNull(): StatusBarIconView? =
+ if (StatusBarConnectedDisplays.isEnabled) {
+ null
+ } else {
+ mock<StatusBarIconView>()
+ }
+
+ private const val DEFAULT_UID = 886
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/data/repository/BatteryRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/data/repository/BatteryRepositoryKosmos.kt
new file mode 100644
index 000000000000..ab651cc3eadf
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/data/repository/BatteryRepositoryKosmos.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.data.repository
+
+import android.content.testableContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shared.settings.data.repository.systemSettingsRepository
+import com.android.systemui.statusbar.policy.batteryController
+
+/** Use [Kosmos.batteryController.fake] to make the repo have the state you want */
+val Kosmos.batteryRepository by
+ Kosmos.Fixture {
+ BatteryRepository(
+ testableContext,
+ testScope.backgroundScope,
+ testDispatcher,
+ batteryController,
+ systemSettingsRepository,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractorKosmos.kt
new file mode 100644
index 000000000000..6cab3f6863c7
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractorKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.pipeline.battery.data.repository.batteryRepository
+
+val Kosmos.batteryInteractor by Kosmos.Fixture { BatteryInteractor(batteryRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelKosmos.kt
new file mode 100644
index 000000000000..c6cf0063986a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModelKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.battery.ui.viewmodel
+
+import android.content.testableContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.pipeline.battery.domain.interactor.batteryInteractor
+
+val Kosmos.batteryViewModel by
+ Kosmos.Fixture { BatteryViewModel(batteryInteractor, testableContext) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/BatteryControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/BatteryControllerKosmos.kt
index b31a45caa517..c4bebbb2e07c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/BatteryControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/BatteryControllerKosmos.kt
@@ -18,6 +18,8 @@ package com.android.systemui.statusbar.policy
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.util.mockito.mock
-val Kosmos.batteryController: BatteryController by Fixture { mock<BatteryController>() }
+val Kosmos.batteryController: BatteryController by Fixture { FakeBatteryControllerImpl() }
+
+val BatteryController.fake
+ get() = this as FakeBatteryControllerImpl
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeBatteryControllerImpl.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeBatteryControllerImpl.kt
new file mode 100644
index 000000000000..fa96314bc65e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeBatteryControllerImpl.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy
+
+import android.os.Bundle
+import com.android.systemui.animation.Expandable
+import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
+import com.android.systemui.statusbar.policy.BatteryController.EstimateFetchCompletion
+import java.io.PrintWriter
+import java.lang.ref.WeakReference
+
+class FakeBatteryControllerImpl : BatteryController {
+ var listeners = mutableSetOf<BatteryStateChangeCallback>()
+ private set
+
+ var _level = 50
+ set(value) {
+ if (field != value) {
+ field = value
+ listeners.forEach { it.onBatteryLevelChanged(field, _isPluggedIn, _isPluggedIn) }
+ }
+ }
+
+ var _isPowerSave = false
+ set(value) {
+ if (field != value) {
+ field = value
+ listeners.forEach { it.onPowerSaveChanged(field) }
+ }
+ }
+
+ var _isPluggedIn = false
+ set(value) {
+ if (field != value) {
+ field = value
+ listeners.forEach { it.onBatteryLevelChanged(_level, field, field) }
+ }
+ }
+
+ var _isStateUnknown = false
+ set(value) {
+ if (field != value) {
+ field = value
+ listeners.forEach { it.onBatteryUnknownStateChanged(field) }
+ }
+ }
+
+ var _isDefender = false
+ set(value) {
+ if (field != value) {
+ field = value
+ listeners.forEach { it.onIsBatteryDefenderChanged(field) }
+ }
+ }
+
+ var _isWirelessCharging = false
+ set(value) {
+ if (field != value) {
+ field = value
+ listeners.forEach { it.onWirelessChargingChanged(field) }
+ }
+ }
+
+ var _isAodPowerSave = false
+
+ var _isReverseSupported = false
+ var _isReverseOn = false
+ var _isExtremeBatterySaverOn = false
+ var _isChargingSourceDock = false
+
+ var _estimatedTimeRemainingString: String? = null
+
+ override fun dump(pw: PrintWriter?, args: Array<out String>?) {
+ // nop
+ }
+
+ override fun dispatchDemoCommand(command: String?, args: Bundle?) {
+ // nop
+ }
+
+ override fun addCallback(listener: BatteryStateChangeCallback) {
+ listeners += listener
+
+ listener.onBatteryLevelChanged(_level, _isPluggedIn, _isPluggedIn)
+ listener.onPowerSaveChanged(_isPowerSave)
+ listener.onBatteryUnknownStateChanged(_isStateUnknown)
+ listener.onWirelessChargingChanged(_isWirelessCharging)
+ listener.onIsBatteryDefenderChanged(_isDefender)
+ }
+
+ override fun removeCallback(listener: BatteryStateChangeCallback) {
+ listeners -= listener
+ }
+
+ override fun setPowerSaveMode(powerSave: Boolean) {
+ setPowerSaveMode(powerSave, null)
+ }
+
+ override fun setPowerSaveMode(powerSave: Boolean, expandable: Expandable?) {
+ _isPowerSave = powerSave
+ }
+
+ override fun getLastPowerSaverStartExpandable(): WeakReference<Expandable>? {
+ return null
+ }
+
+ override fun clearLastPowerSaverStartExpandable() {
+ // nop
+ }
+
+ override fun isPluggedIn() = _isPluggedIn
+
+ override fun isPluggedInWireless(): Boolean {
+ return false
+ }
+
+ override fun isPowerSave() = _isPowerSave
+
+ override fun isAodPowerSave() = _isAodPowerSave
+
+ override fun init() {
+ // nop
+ }
+
+ override fun isWirelessCharging(): Boolean {
+ return false
+ }
+
+ override fun isReverseSupported() = _isReverseSupported
+
+ override fun isReverseOn() = _isReverseOn
+
+ override fun setReverseState(isReverse: Boolean) {
+ _isReverseOn = isReverse
+ }
+
+ override fun isExtremeSaverOn() = _isExtremeBatterySaverOn
+
+ override fun isChargingSourceDock() = _isChargingSourceDock
+
+ // Just pretend that it's cached and returns instantly
+ override fun getEstimatedTimeRemainingString(completion: EstimateFetchCompletion?) {
+ completion?.onBatteryRemainingEstimateRetrieved(_estimatedTimeRemainingString)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt
index ef043e0177a5..2c0bd326525c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt
@@ -19,6 +19,8 @@ package com.android.systemui.statusbar.policy.ui.dialog
import android.content.mockedContext
import com.android.systemui.animation.dialogTransitionAnimator
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.backgroundCoroutineContext
import com.android.systemui.kosmos.mainCoroutineContext
import com.android.systemui.plugins.activityStarter
import com.android.systemui.shade.data.repository.shadeDialogContextInteractor
@@ -37,7 +39,9 @@ var Kosmos.modesDialogDelegate: ModesDialogDelegate by
activityStarter,
{ modesDialogViewModel },
modesDialogEventLogger,
+ applicationCoroutineScope,
mainCoroutineContext,
+ backgroundCoroutineContext,
shadeDialogContextInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt
index 3571a737704b..4710813087d3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelKosmos.kt
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.policy.ui.dialog.viewmodel
-import android.content.mockedContext
+import android.content.applicationContext
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor
@@ -27,7 +27,7 @@ import javax.inject.Provider
val Kosmos.modesDialogViewModel: ModesDialogViewModel by
Kosmos.Fixture {
ModesDialogViewModel(
- mockedContext,
+ applicationContext,
zenModeInteractor,
testDispatcher,
Provider { modesDialogDelegate }.get(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 85d582a27faf..145bb93d9cb0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.yield
@SysUISingleton
@@ -76,6 +77,11 @@ class FakeUserRepository @Inject constructor() : UserRepository {
override val isLogoutToSystemUserEnabled: StateFlow<Boolean> =
_isLogoutToSystemUserEnabled.asStateFlow()
+ private val _userUnlockedState = MutableStateFlow(emptyMap<UserHandle, Boolean>())
+
+ override fun isUserUnlocked(userHandle: UserHandle?): Flow<Boolean> =
+ _userUnlockedState.map { it[userHandle] ?: false }
+
override var mainUserId: Int = MAIN_USER_ID
override var lastSelectedNonGuestUserId: Int = mainUserId
@@ -176,6 +182,14 @@ class FakeUserRepository @Inject constructor() : UserRepository {
fun setGuestUserAutoCreated(value: Boolean) {
_isGuestUserAutoCreated = value
}
+
+ fun setUserUnlocked(userId: Int, unlocked: Boolean) {
+ setUserUnlocked(UserHandle(userId), unlocked)
+ }
+
+ fun setUserUnlocked(userHandle: UserHandle, unlocked: Boolean) {
+ _userUnlockedState.update { it + (userHandle to unlocked) }
+ }
}
@Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt
new file mode 100644
index 000000000000..fd955089cdc7
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/UserLockedInteractorKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.user.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.user.data.repository.userRepository
+
+val Kosmos.userLockedInteractor by
+ Kosmos.Fixture { UserLockedInteractor(userRepository = userRepository) }
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
index 0354d2be60c9..2ef11f4b78e1 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
@@ -20,11 +20,14 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M
import android.content.Context;
import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
+import android.widget.ImageButton;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
@@ -51,6 +54,8 @@ public class AutoclickTypePanel {
private final LinearLayout mDragButton;
private final LinearLayout mScrollButton;
+ private LinearLayout mSelectedButton;
+
public AutoclickTypePanel(Context context, WindowManager windowManager) {
mContext = context;
mWindowManager = windowManager;
@@ -80,6 +85,40 @@ public class AutoclickTypePanel {
// Initializes panel as collapsed state and only displays the left click button.
hideAllClickTypeButtons();
mLeftClickButton.setVisibility(View.VISIBLE);
+ setSelectedButton(/* selectedButton= */ mLeftClickButton);
+ }
+
+ /** Sets the selected button and updates the newly and previously selected button styling. */
+ private void setSelectedButton(@NonNull LinearLayout selectedButton) {
+ // Updates the previously selected button styling.
+ if (mSelectedButton != null) {
+ toggleSelectedButtonStyle(mSelectedButton, /* isSelected= */ false);
+ }
+
+ mSelectedButton = selectedButton;
+
+ // Updates the newly selected button styling.
+ toggleSelectedButtonStyle(selectedButton, /* isSelected= */ true);
+ }
+
+ private void toggleSelectedButtonStyle(@NonNull LinearLayout button, boolean isSelected) {
+ // Sets icon background color.
+ GradientDrawable gradientDrawable = (GradientDrawable) button.getBackground();
+ gradientDrawable.setColor(
+ mContext.getColor(
+ isSelected
+ ? R.color.materialColorPrimary
+ : R.color.materialColorSurfaceContainer));
+
+ // Sets icon color.
+ ImageButton imageButton = (ImageButton) button.getChildAt(/* index= */ 0);
+ Drawable drawable = imageButton.getDrawable();
+ drawable.mutate()
+ .setTint(
+ mContext.getColor(
+ isSelected
+ ? R.color.materialColorSurfaceContainer
+ : R.color.materialColorPrimary));
}
public void show() {
@@ -97,6 +136,9 @@ public class AutoclickTypePanel {
// buttons except the one user selected.
hideAllClickTypeButtons();
button.setVisibility(View.VISIBLE);
+
+ // Sets the newly selected button.
+ setSelectedButton(/* selectedButton= */ button);
} else {
// If the panel is already collapsed, we just need to expand it.
showAllClickTypeButtons();
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index 43764442e2cf..d0ee7af1bbfb 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -27,6 +27,7 @@ import android.annotation.WorkerThread;
import android.app.appfunctions.AppFunctionException;
import android.app.appfunctions.AppFunctionManager;
import android.app.appfunctions.AppFunctionManagerHelper;
+import android.app.appfunctions.AppFunctionManagerHelper.AppFunctionNotFoundException;
import android.app.appfunctions.AppFunctionRuntimeMetadata;
import android.app.appfunctions.AppFunctionStaticMetadataHelper;
import android.app.appfunctions.ExecuteAppFunctionAidlRequest;
@@ -513,7 +514,9 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub {
e = e.getCause();
}
int resultCode = AppFunctionException.ERROR_SYSTEM_ERROR;
- if (e instanceof AppSearchException appSearchException) {
+ if (e instanceof AppFunctionNotFoundException) {
+ resultCode = AppFunctionException.ERROR_FUNCTION_NOT_FOUND;
+ } else if (e instanceof AppSearchException appSearchException) {
resultCode =
mapAppSearchResultFailureCodeToExecuteAppFunctionResponse(
appSearchException.getResultCode());
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index c385fbad02a5..b75728e9f97c 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -30,7 +30,6 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_BLOCKED_
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
-import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -55,9 +54,9 @@ import android.companion.virtual.VirtualDeviceParams;
import android.companion.virtual.audio.IAudioConfigChangedCallback;
import android.companion.virtual.audio.IAudioRoutingCallback;
import android.companion.virtual.camera.VirtualCameraConfig;
-import android.companion.virtual.flags.Flags;
import android.companion.virtual.sensor.VirtualSensor;
import android.companion.virtual.sensor.VirtualSensorEvent;
+import android.companion.virtualdevice.flags.Flags;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.AttributionSource;
@@ -265,7 +264,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
UserHandle.SYSTEM);
}
- if (android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (Flags.activityControlApi()) {
try {
mActivityListener.onActivityLaunchBlocked(
displayId,
@@ -280,7 +279,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
@Override
public void onSecureWindowShown(int displayId, @NonNull ActivityInfo activityInfo) {
- if (android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (Flags.activityControlApi()) {
try {
mActivityListener.onSecureWindowShown(
displayId,
@@ -318,7 +317,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
@Override
public void onSecureWindowHidden(int displayId) {
- if (android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (Flags.activityControlApi()) {
try {
mActivityListener.onSecureWindowHidden(displayId);
} catch (RemoteException e) {
@@ -682,7 +681,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
checkCallerIsDeviceOwner();
final int displayId = exemption.getDisplayId();
if (exemption.getComponentName() == null || displayId != Display.INVALID_DISPLAY) {
- if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (!Flags.activityControlApi()) {
return;
}
}
@@ -719,7 +718,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
checkCallerIsDeviceOwner();
final int displayId = exemption.getDisplayId();
if (exemption.getComponentName() == null || displayId != Display.INVALID_DISPLAY) {
- if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (!Flags.activityControlApi()) {
return;
}
}
@@ -921,7 +920,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
}
break;
case POLICY_TYPE_BLOCKED_ACTIVITY:
- if (android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (Flags.activityControlApi()) {
synchronized (mVirtualDeviceLock) {
mDevicePolicies.put(policyType, devicePolicy);
}
@@ -938,7 +937,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
@VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
@VirtualDeviceParams.DevicePolicy int devicePolicy) {
checkCallerIsDeviceOwner();
- if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (!Flags.activityControlApi()) {
return;
}
synchronized (mVirtualDeviceLock) {
@@ -1508,7 +1507,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
}
private PowerManager.WakeLock createAndAcquireWakeLockForDisplay(int displayId) {
- if (android.companion.virtualdevice.flags.Flags.deviceAwareDisplayPower()) {
+ if (Flags.deviceAwareDisplayPower()) {
return null;
}
final long token = Binder.clearCallingIdentity();
@@ -1531,7 +1530,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
// infinite blocking loop.
return false;
}
- if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
+ if (!Flags.activityControlApi()) {
return true;
}
// Do not show the dialog if disabled by policy.
@@ -1868,8 +1867,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub
}
private static boolean isVirtualCameraEnabled() {
- return Flags.virtualCamera() && virtualCameraServiceDiscovery()
- && nativeVirtualCameraServiceBuildFlagEnabled();
+ return nativeVirtualCameraServiceBuildFlagEnabled();
}
// Returns true if virtual_camera service is enabled in this build.
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 7db63a5c4a11..f34016905502 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -10430,9 +10430,7 @@ public class ActivityManagerService extends IActivityManager.Stub
synchronized(this) {
mConstants.dump(pw);
- synchronized (mProcLock) {
- mOomAdjuster.dumpCachedAppOptimizerSettings(pw);
- }
+ mOomAdjuster.dumpCachedAppOptimizerSettings(pw);
mOomAdjuster.dumpCacheOomRankerSettings(pw);
pw.println();
if (dumpAll) {
@@ -10806,7 +10804,7 @@ public class ActivityManagerService extends IActivityManager.Stub
|| DUMP_RECENTS_CMD.equals(cmd) || DUMP_RECENTS_SHORT_CMD.equals(cmd)
|| DUMP_TOP_RESUMED_ACTIVITY.equals(cmd)
|| DUMP_VISIBLE_ACTIVITIES.equals(cmd)) {
- mAtmInternal.dump(cmd, fd, pw, args, opti, /* dumpAll= */ true , dumpClient,
+ mAtmInternal.dump(cmd, fd, pw, args, opti, /* dumpAll= */ true, dumpClient,
dumpPackage, dumpDisplayId);
} else if ("binder-proxies".equals(cmd)) {
if (opti >= args.length) {
@@ -10896,7 +10894,8 @@ public class ActivityManagerService extends IActivityManager.Stub
name = args[opti];
opti++;
newArgs = new String[args.length - opti];
- if (args.length > 2) System.arraycopy(args, opti, newArgs, 0, args.length - opti);
+ if (args.length > 2) System.arraycopy(args, opti, newArgs, 0,
+ args.length - opti);
}
if (!mCpHelper.dumpProvider(fd, pw, name, newArgs, 0, dumpAll)) {
pw.println("No providers match: " + name);
@@ -10919,7 +10918,7 @@ public class ActivityManagerService extends IActivityManager.Stub
if (args.length > 2) System.arraycopy(args, opti, newArgs, 0,
args.length - opti);
}
- int[] users = dumpUserId == UserHandle.USER_ALL ? null : new int[] { dumpUserId };
+ int[] users = dumpUserId == UserHandle.USER_ALL ? null : new int[]{dumpUserId};
if (!mServices.dumpService(fd, pw, name, users, newArgs, 0, dumpAll)) {
pw.println("No services match: " + name);
pw.println("Use -h for help.");
@@ -10948,9 +10947,10 @@ public class ActivityManagerService extends IActivityManager.Stub
mConstants.dump(pw);
}
synchronized (mProcLock) {
- mOomAdjuster.dumpCachedAppOptimizerSettings(pw);
mOomAdjuster.dumpCacheOomRankerSettings(pw);
}
+ } else if ("cao".equals(cmd)) {
+ mOomAdjuster.dumpCachedAppOptimizerSettings(pw);
} else if ("timers".equals(cmd)) {
AnrTimer.dump(pw, true);
} else if ("services".equals(cmd) || "s".equals(cmd)) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index c237897f1229..5c2bd0a0e90f 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -4344,6 +4344,7 @@ final class ActivityManagerShellCommand extends ShellCommand {
pw.println(" lru: raw LRU process list");
pw.println(" binder-proxies: stats on binder objects and IPCs");
pw.println(" settings: currently applied config settings");
+ pw.println(" cao: cached app optimizer state");
pw.println(" timers: the current ANR timer state");
pw.println(" service [COMP_SPEC]: service client-side state");
pw.println(" package [PACKAGE_NAME]: all state related to given package");
diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java
index ce526e510053..b677297dfef2 100644
--- a/services/core/java/com/android/server/am/CachedAppOptimizer.java
+++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java
@@ -85,6 +85,8 @@ import com.android.internal.os.BinderfsStatsReader;
import com.android.internal.os.ProcLocksReader;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.ServiceThread;
+import com.android.server.am.compaction.CompactionStatsManager;
+import com.android.server.am.compaction.SingleCompactionStats;
import dalvik.annotation.optimization.NeverCompile;
@@ -94,11 +96,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.EnumMap;
import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Executor;
@@ -343,12 +341,6 @@ public class CachedAppOptimizer {
// on swap resources as file.
static final double COMPACT_DOWNGRADE_FREE_SWAP_THRESHOLD = 0.2;
- // Size of history for the last 20 compactions for any process
- static final int LAST_COMPACTED_ANY_PROCESS_STATS_HISTORY_SIZE = 20;
-
- // Amount of processes supported to record for their last compaction.
- static final int LAST_COMPACTION_FOR_PROCESS_STATS_SIZE = 256;
-
static final int DO_FREEZE = 1;
static final int REPORT_UNFREEZE = 2;
@@ -521,6 +513,9 @@ public class CachedAppOptimizer {
// Handler on which compaction runs.
@VisibleForTesting
Handler mCompactionHandler;
+ @VisibleForTesting
+ CompactionStatsManager mCompactStatsManager;
+
private Handler mFreezeHandler;
@GuardedBy("mProcLock")
private boolean mFreezerOverride = false;
@@ -529,156 +524,6 @@ public class CachedAppOptimizer {
@VisibleForTesting volatile long mFreezerDebounceTimeout = DEFAULT_FREEZER_DEBOUNCE_TIMEOUT;
@VisibleForTesting volatile boolean mFreezerExemptInstPkg = DEFAULT_FREEZER_EXEMPT_INST_PKG;
- // Maps process ID to last compaction statistics for processes that we've fully compacted. Used
- // when evaluating throttles that we only consider for "full" compaction, so we don't store
- // data for "some" compactions. Uses LinkedHashMap to ensure insertion order is kept and
- // facilitate removal of the oldest entry.
- @VisibleForTesting
- @GuardedBy("mProcLock")
- LinkedHashMap<Integer, SingleCompactionStats> mLastCompactionStats =
- new LinkedHashMap<Integer, SingleCompactionStats>() {
- @Override
- protected boolean removeEldestEntry(Map.Entry eldest) {
- return size() > LAST_COMPACTION_FOR_PROCESS_STATS_SIZE;
- }
- };
-
- LinkedList<SingleCompactionStats> mCompactionStatsHistory =
- new LinkedList<SingleCompactionStats>() {
- @Override
- public boolean add(SingleCompactionStats e) {
- if (size() >= LAST_COMPACTED_ANY_PROCESS_STATS_HISTORY_SIZE) {
- this.remove();
- }
- return super.add(e);
- }
- };
-
- class AggregatedCompactionStats {
- // Throttling stats
- public long mFullCompactRequested;
- public long mSomeCompactRequested;
- public long mFullCompactPerformed;
- public long mSomeCompactPerformed;
- public long mProcCompactionsNoPidThrottled;
- public long mProcCompactionsOomAdjThrottled;
- public long mProcCompactionsTimeThrottled;
- public long mProcCompactionsRSSThrottled;
- public long mProcCompactionsMiscThrottled;
-
- // Memory stats
- public long mTotalDeltaAnonRssKBs;
- public long mTotalZramConsumedKBs;
- public long mTotalAnonMemFreedKBs;
- public long mSumOrigAnonRss;
- public double mMaxCompactEfficiency;
- public double mMaxSwapEfficiency;
-
- // Cpu time
- public long mTotalCpuTimeMillis;
-
- public long getThrottledSome() { return mSomeCompactRequested - mSomeCompactPerformed; }
-
- public long getThrottledFull() { return mFullCompactRequested - mFullCompactPerformed; }
-
- public void addMemStats(long anonRssSaved, long zramConsumed, long memFreed,
- long origAnonRss, long totalCpuTimeMillis) {
- final double compactEfficiency = memFreed / (double) origAnonRss;
- if (compactEfficiency > mMaxCompactEfficiency) {
- mMaxCompactEfficiency = compactEfficiency;
- }
- final double swapEfficiency = anonRssSaved / (double) origAnonRss;
- if (swapEfficiency > mMaxSwapEfficiency) {
- mMaxSwapEfficiency = swapEfficiency;
- }
- mTotalDeltaAnonRssKBs += anonRssSaved;
- mTotalZramConsumedKBs += zramConsumed;
- mTotalAnonMemFreedKBs += memFreed;
- mSumOrigAnonRss += origAnonRss;
- mTotalCpuTimeMillis += totalCpuTimeMillis;
- }
-
- @NeverCompile
- public void dump(PrintWriter pw) {
- long totalCompactRequested = mSomeCompactRequested + mFullCompactRequested;
- long totalCompactPerformed = mSomeCompactPerformed + mFullCompactPerformed;
- pw.println(" Performed / Requested:");
- pw.println(" Some: (" + mSomeCompactPerformed + "/" + mSomeCompactRequested + ")");
- pw.println(" Full: (" + mFullCompactPerformed + "/" + mFullCompactRequested + ")");
-
- long throttledSome = getThrottledSome();
- long throttledFull = getThrottledFull();
-
- if (throttledSome > 0 || throttledFull > 0) {
- pw.println(" Throttled:");
- pw.println(" Some: " + throttledSome + " Full: " + throttledFull);
- pw.println(" Throttled by Type:");
- final long compactionsThrottled = totalCompactRequested - totalCompactPerformed;
- // Any throttle that was not part of the previous categories
- final long unaccountedThrottled = compactionsThrottled
- - mProcCompactionsNoPidThrottled - mProcCompactionsOomAdjThrottled
- - mProcCompactionsTimeThrottled - mProcCompactionsRSSThrottled
- - mProcCompactionsMiscThrottled;
- pw.println(" NoPid: " + mProcCompactionsNoPidThrottled
- + " OomAdj: " + mProcCompactionsOomAdjThrottled + " Time: "
- + mProcCompactionsTimeThrottled + " RSS: " + mProcCompactionsRSSThrottled
- + " Misc: " + mProcCompactionsMiscThrottled
- + " Unaccounted: " + unaccountedThrottled);
- final double compactThrottlePercentage =
- (compactionsThrottled / (double) totalCompactRequested) * 100.0;
- pw.println(" Throttle Percentage: " + compactThrottlePercentage);
- }
-
- if (mFullCompactPerformed > 0) {
- pw.println(" -----Memory Stats----");
- pw.println(" Total Delta Anon RSS (KB) : " + mTotalDeltaAnonRssKBs);
- pw.println(" Total Physical ZRAM Consumed (KB): " + mTotalZramConsumedKBs);
- // Anon Mem Freed = Delta Anon RSS - ZRAM Consumed
- pw.println(" Total Anon Memory Freed (KB): " + mTotalAnonMemFreedKBs);
- pw.println(" Avg Swap Efficiency (KB) (Delta Anon RSS/Orig Anon RSS): "
- + (mTotalDeltaAnonRssKBs / (double) mSumOrigAnonRss));
- pw.println(" Max Swap Efficiency: " + mMaxSwapEfficiency);
- // This tells us how much anon memory we were able to free thanks to running
- // compaction
- pw.println(" Avg Compaction Efficiency (Anon Freed/Anon RSS): "
- + (mTotalAnonMemFreedKBs / (double) mSumOrigAnonRss));
- pw.println(" Max Compaction Efficiency: " + mMaxCompactEfficiency);
- // This tells us how effective is the compression algorithm in physical memory
- pw.println(" Avg Compression Ratio (1 - ZRAM Consumed/DeltaAnonRSS): "
- + (1.0 - mTotalZramConsumedKBs / (double) mTotalDeltaAnonRssKBs));
- long avgKBsPerProcCompact = mFullCompactPerformed > 0
- ? (mTotalAnonMemFreedKBs / mFullCompactPerformed)
- : 0;
- pw.println(" Avg Anon Mem Freed/Compaction (KB) : " + avgKBsPerProcCompact);
- double compactionCost =
- mTotalCpuTimeMillis / (mTotalAnonMemFreedKBs / 1024.0); // ms/MB
- pw.println(" Compaction Cost (ms/MB): " + compactionCost);
- }
- }
- }
-
- class AggregatedProcessCompactionStats extends AggregatedCompactionStats {
- public final String processName;
-
- AggregatedProcessCompactionStats(String processName) { this.processName = processName; }
- }
-
- class AggregatedSourceCompactionStats extends AggregatedCompactionStats {
- public final CompactSource sourceType;
-
- AggregatedSourceCompactionStats(CompactSource sourceType) { this.sourceType = sourceType; }
- }
-
- private final LinkedHashMap<String, AggregatedProcessCompactionStats> mPerProcessCompactStats =
- new LinkedHashMap<>(256);
- private final EnumMap<CompactSource, AggregatedSourceCompactionStats> mPerSourceCompactStats =
- new EnumMap<>(CompactSource.class);
- private long mTotalCompactionDowngrades;
- private long mSystemCompactionsPerformed;
- private long mSystemTotalMemFreed;
- private EnumMap<CancelCompactReason, Integer> mTotalCompactionsCancelled =
- new EnumMap<>(CancelCompactReason.class);
-
private final ProcessDependencies mProcessDependencies;
private final ProcLocksReader mProcLocksReader;
@@ -758,10 +603,15 @@ public class CachedAppOptimizer {
}
}
- @GuardedBy("mProcLock")
@NeverCompile
void dump(PrintWriter pw) {
- pw.println("CachedAppOptimizer settings");
+ dumpCompact(pw);
+ dumpFreezer(pw);
+ }
+
+ @NeverCompile
+ void dumpCompact(PrintWriter pw) {
+ pw.println("Compaction settings");
synchronized (mPhenotypeFlagLock) {
pw.println(" " + KEY_USE_COMPACTION + "=" + mUseCompaction);
pw.println(" " + KEY_COMPACT_THROTTLE_1 + "=" + mCompactThrottleSomeSome);
@@ -775,66 +625,30 @@ public class CachedAppOptimizer {
+ mFullAnonRssThrottleKb);
pw.println(" " + KEY_COMPACT_FULL_DELTA_RSS_THROTTLE_KB + "="
+ mFullDeltaRssThrottleKb);
- pw.println(" " + KEY_COMPACT_PROC_STATE_THROTTLE + "="
+ pw.println(" " + KEY_COMPACT_PROC_STATE_THROTTLE + "="
+ Arrays.toString(mProcStateThrottle.toArray(new Integer[0])));
+ }
- pw.println(" Per-Process Compaction Stats");
- long totalCompactPerformedSome = 0;
- long totalCompactPerformedFull = 0;
- for (AggregatedProcessCompactionStats stats : mPerProcessCompactStats.values()) {
- pw.println("-----" + stats.processName + "-----");
- totalCompactPerformedSome += stats.mSomeCompactPerformed;
- totalCompactPerformedFull += stats.mFullCompactPerformed;
- stats.dump(pw);
- pw.println();
- }
- pw.println();
- pw.println(" Per-Source Compaction Stats");
- for (AggregatedSourceCompactionStats stats : mPerSourceCompactStats.values()) {
- pw.println("-----" + stats.sourceType + "-----");
- stats.dump(pw);
- pw.println();
- }
- pw.println();
-
- pw.println("Total Compactions Performed by profile: " + totalCompactPerformedSome
- + " some, " + totalCompactPerformedFull + " full");
- pw.println("Total compactions downgraded: " + mTotalCompactionDowngrades);
- pw.println("Total compactions cancelled by reason: ");
- for (CancelCompactReason reason : mTotalCompactionsCancelled.keySet()) {
- pw.println(" " + reason + ": " + mTotalCompactionsCancelled.get(reason));
- }
- pw.println();
-
- pw.println(" System Compaction Memory Stats");
- pw.println(" Compactions Performed: " + mSystemCompactionsPerformed);
- pw.println(" Total Memory Freed (KB): " + mSystemTotalMemFreed);
- double avgKBsPerSystemCompact = mSystemCompactionsPerformed > 0
- ? mSystemTotalMemFreed / mSystemCompactionsPerformed
- : 0;
- pw.println(" Avg Mem Freed per Compact (KB): " + avgKBsPerSystemCompact);
- pw.println();
- pw.println(" Tracking last compaction stats for " + mLastCompactionStats.size()
- + " processes.");
- pw.println("Last Compaction per process stats:");
- pw.println(" (ProcessName,Source,DeltaAnonRssKBs,ZramConsumedKBs,AnonMemFreedKBs"
- + ",SwapEfficiency,CompactEfficiency,CompactCost(ms/MB),procState,oomAdj,"
- + "oomAdjReason)");
- for (Map.Entry<Integer, SingleCompactionStats> entry :
- mLastCompactionStats.entrySet()) {
- SingleCompactionStats stats = entry.getValue();
- stats.dump(pw);
- }
- pw.println();
- pw.println("Last 20 Compactions Stats:");
- pw.println(" (ProcessName,Source,DeltaAnonRssKBs,ZramConsumedKBs,AnonMemFreedKBs,"
- + "SwapEfficiency,CompactEfficiency,CompactCost(ms/MB),procState,oomAdj,"
- + "oomAdjReason)");
- for (SingleCompactionStats stats : mCompactionStatsHistory) {
- stats.dump(pw);
+ mCompactStatsManager.dump(pw);
+
+ synchronized (mProcLock) {
+ if (!mPendingCompactionProcesses.isEmpty()) {
+ pw.println(" Pending compactions:");
+ int size = mPendingCompactionProcesses.size();
+ for (int i = 0; i < size; i++) {
+ ProcessRecord app = mPendingCompactionProcesses.get(i);
+ pw.println(" pid: " + app.getPid() + ". name: " + app.processName
+ + ". hasPendingCompact: " + app.mOptRecord.hasPendingCompact());
+ }
}
- pw.println();
+ }
+ pw.println();
+ }
+ @NeverCompile
+ void dumpFreezer(PrintWriter pw) {
+ pw.println("Freezer settings");
+ synchronized (mPhenotypeFlagLock) {
pw.println(" " + KEY_USE_FREEZER + "=" + mUseFreezer);
pw.println(" " + KEY_FREEZER_STATSD_SAMPLE_RATE + "=" + mFreezerStatsdSampleRate);
pw.println(" " + KEY_FREEZER_DEBOUNCE_TIMEOUT + "=" + mFreezerDebounceTimeout);
@@ -858,16 +672,6 @@ public class CachedAppOptimizer {
+ " " + app.processName
+ (app.mOptRecord.isFreezeSticky() ? " (sticky)" : ""));
}
-
- if (!mPendingCompactionProcesses.isEmpty()) {
- pw.println(" Pending compactions:");
- size = mPendingCompactionProcesses.size();
- for (int i = 0; i < size; i++) {
- ProcessRecord app = mPendingCompactionProcesses.get(i);
- pw.println(" pid: " + app.getPid() + ". name: " + app.processName
- + ". hasPendingCompact: " + app.mOptRecord.hasPendingCompact());
- }
- }
}
}
}
@@ -877,26 +681,14 @@ public class CachedAppOptimizer {
ProcessRecord app, CompactProfile compactProfile, CompactSource source, boolean force) {
app.mOptRecord.setReqCompactSource(source);
app.mOptRecord.setReqCompactProfile(compactProfile);
- AggregatedSourceCompactionStats perSourceStats = getPerSourceAggregatedCompactStat(source);
- AggregatedCompactionStats perProcStats =
- getPerProcessAggregatedCompactStat(app.processName);
- switch (compactProfile) {
- case SOME:
- ++perProcStats.mSomeCompactRequested;
- ++perSourceStats.mSomeCompactRequested;
- break;
- case FULL:
- ++perProcStats.mFullCompactRequested;
- ++perSourceStats.mFullCompactRequested;
- break;
- default:
- Slog.e(TAG_AM,
- "Unimplemented compaction type, consider adding it.");
- return false;
+
+ if(compactProfile == null || compactProfile.equals(CompactProfile.NONE)) {
+ return false;
}
+ final String processName = (app.processName != null ? app.processName : "");
+ mCompactStatsManager.logCompactionRequested(source, compactProfile, processName);
if (!app.mOptRecord.hasPendingCompact()) {
- final String processName = (app.processName != null ? app.processName : "");
if (DEBUG_COMPACTION) {
Slog.d(TAG_AM,
"compactApp " + app.mOptRecord.getReqCompactSource().name() + " "
@@ -925,29 +717,6 @@ public class CachedAppOptimizer {
COMPACT_NATIVE_MSG, pid, compactProfile.ordinal()));
}
- private AggregatedProcessCompactionStats getPerProcessAggregatedCompactStat(
- String processName) {
- if (processName == null) {
- processName = "";
- }
- AggregatedProcessCompactionStats stats = mPerProcessCompactStats.get(processName);
- if (stats == null) {
- stats = new AggregatedProcessCompactionStats(processName);
- mPerProcessCompactStats.put(processName, stats);
- }
- return stats;
- }
-
- private AggregatedSourceCompactionStats getPerSourceAggregatedCompactStat(
- CompactSource source) {
- AggregatedSourceCompactionStats stats = mPerSourceCompactStats.get(source);
- if (stats == null) {
- stats = new AggregatedSourceCompactionStats(source);
- mPerSourceCompactStats.put(source, stats);
- }
- return stats;
- }
-
void compactAllSystem() {
if (useCompaction()) {
if (DEBUG_COMPACTION) {
@@ -1008,6 +777,7 @@ public class CachedAppOptimizer {
}
mCompactionHandler = new MemCompactionHandler();
+ mCompactStatsManager = CompactionStatsManager.getInstance();
Process.setThreadGroupAndCpuset(mCachedAppOptimizerThread.getThreadId(),
Process.THREAD_GROUP_SYSTEM);
@@ -1667,12 +1437,7 @@ public class CachedAppOptimizer {
cancelled = true;
}
if (cancelled) {
- if (mTotalCompactionsCancelled.containsKey(cancelReason)) {
- int count = mTotalCompactionsCancelled.get(cancelReason);
- mTotalCompactionsCancelled.put(cancelReason, count + 1);
- } else {
- mTotalCompactionsCancelled.put(cancelReason, 1);
- }
+ mCompactStatsManager.logCompactionCancelled(cancelReason);
if (DEBUG_COMPACTION) {
Slog.d(TAG_AM,
"Cancelled pending or running compactions for process: " +
@@ -1722,8 +1487,7 @@ public class CachedAppOptimizer {
// Downgrade compaction under swap memory pressure
if (swapFreePercent < COMPACT_DOWNGRADE_FREE_SWAP_THRESHOLD) {
profile = CompactProfile.SOME;
-
- ++mTotalCompactionDowngrades;
+ mCompactStatsManager.logCompactionDowngrade();
if (DEBUG_COMPACTION) {
Slog.d(TAG_AM,
"Downgraded compaction to "+ profile +" due to low swap."
@@ -1753,72 +1517,6 @@ public class CachedAppOptimizer {
}
}
- @VisibleForTesting
- static final class SingleCompactionStats {
- private static final float STATSD_SAMPLE_RATE = 0.1f;
- private static final Random mRandom = new Random();
- private final long[] mRssAfterCompaction;
- public CompactSource mSourceType;
- public String mProcessName;
- public final int mUid;
- public long mDeltaAnonRssKBs;
- public long mZramConsumedKBs;
- public long mAnonMemFreedKBs;
- public float mCpuTimeMillis;
- public long mOrigAnonRss;
- public int mProcState;
- public int mOomAdj;
- public @OomAdjReason int mOomAdjReason;
-
- SingleCompactionStats(long[] rss, CompactSource source, String processName,
- long deltaAnonRss, long zramConsumed, long anonMemFreed, long origAnonRss,
- long cpuTimeMillis, int procState, int oomAdj,
- @OomAdjReason int oomAdjReason, int uid) {
- mRssAfterCompaction = rss;
- mSourceType = source;
- mProcessName = processName;
- mUid = uid;
- mDeltaAnonRssKBs = deltaAnonRss;
- mZramConsumedKBs = zramConsumed;
- mAnonMemFreedKBs = anonMemFreed;
- mCpuTimeMillis = cpuTimeMillis;
- mOrigAnonRss = origAnonRss;
- mProcState = procState;
- mOomAdj = oomAdj;
- mOomAdjReason = oomAdjReason;
- }
-
- double getCompactEfficiency() { return mAnonMemFreedKBs / (double) mOrigAnonRss; }
-
- double getSwapEfficiency() { return mDeltaAnonRssKBs / (double) mOrigAnonRss; }
-
- double getCompactCost() {
- // mCpuTimeMillis / (anonMemFreedKBs/1024) and metric is in (ms/MB)
- return mCpuTimeMillis / (double) mAnonMemFreedKBs * 1024;
- }
-
- long[] getRssAfterCompaction() {
- return mRssAfterCompaction;
- }
-
- @NeverCompile
- void dump(PrintWriter pw) {
- pw.println(" (" + mProcessName + "," + mSourceType.name() + "," + mDeltaAnonRssKBs
- + "," + mZramConsumedKBs + "," + mAnonMemFreedKBs + ","
- + getSwapEfficiency() + "," + getCompactEfficiency()
- + "," + getCompactCost() + "," + mProcState + "," + mOomAdj + ","
- + OomAdjuster.oomAdjReasonToString(mOomAdjReason) + ")");
- }
-
- void sendStat() {
- if (mRandom.nextFloat() < STATSD_SAMPLE_RATE) {
- FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPACTED_V2, mUid, mProcState,
- mOomAdj, mDeltaAnonRssKBs, mZramConsumedKBs, mCpuTimeMillis, mOrigAnonRss,
- mOomAdjReason);
- }
- }
- }
-
private final class MemCompactionHandler extends Handler {
private MemCompactionHandler() {
super(mCachedAppOptimizerThread.getLooper());
@@ -1909,7 +1607,8 @@ public class CachedAppOptimizer {
private boolean shouldRssThrottleCompaction(
CompactProfile profile, int pid, String name, long[] rssBefore) {
long anonRssBefore = rssBefore[RSS_ANON_INDEX];
- SingleCompactionStats lastCompactionStats = mLastCompactionStats.get(pid);
+ SingleCompactionStats lastCompactionStats =
+ mCompactStatsManager.getLastCompactionStats(pid);
if (rssBefore[RSS_TOTAL_INDEX] == 0 && rssBefore[RSS_FILE_INDEX] == 0
&& rssBefore[RSS_ANON_INDEX] == 0 && rssBefore[RSS_SWAP_INDEX] == 0) {
@@ -1988,43 +1687,43 @@ public class CachedAppOptimizer {
oomAdjReason = opt.getLastOomAdjChangeReason();
}
- AggregatedSourceCompactionStats perSourceStats =
- getPerSourceAggregatedCompactStat(opt.getReqCompactSource());
- AggregatedProcessCompactionStats perProcessStats =
- getPerProcessAggregatedCompactStat(name);
-
long[] rssBefore;
if (pid == 0) {
// not a real process, either one being launched or one being killed
if (DEBUG_COMPACTION) {
Slog.d(TAG_AM, "Compaction failed, pid is 0");
}
- ++perSourceStats.mProcCompactionsNoPidThrottled;
- ++perProcessStats.mProcCompactionsNoPidThrottled;
+ mCompactStatsManager.logCompactionThrottled(
+ CompactionStatsManager.COMPACT_THROTTLE_REASON_NO_PID,
+ compactSource, name);
return;
}
if (!forceCompaction) {
if (shouldOomAdjThrottleCompaction(proc)) {
- ++perProcessStats.mProcCompactionsOomAdjThrottled;
- ++perSourceStats.mProcCompactionsOomAdjThrottled;
+ mCompactStatsManager.logCompactionThrottled(
+ CompactionStatsManager.COMPACT_THROTTLE_REASON_OOM_ADJ,
+ compactSource, name);
return;
}
if (shouldTimeThrottleCompaction(
proc, start, requestedProfile, compactSource)) {
- ++perProcessStats.mProcCompactionsTimeThrottled;
- ++perSourceStats.mProcCompactionsTimeThrottled;
+ mCompactStatsManager.logCompactionThrottled(
+ CompactionStatsManager.COMPACT_THROTTLE_REASON_TIME_TOO_SOON,
+ compactSource, name);
return;
}
if (shouldThrottleMiscCompaction(proc, procState)) {
- ++perProcessStats.mProcCompactionsMiscThrottled;
- ++perSourceStats.mProcCompactionsMiscThrottled;
+ mCompactStatsManager.logCompactionThrottled(
+ CompactionStatsManager.COMPACT_THROTTLE_REASON_PROC_STATE,
+ compactSource, name);
return;
}
rssBefore = mProcessDependencies.getRss(pid);
if (shouldRssThrottleCompaction(requestedProfile, pid, name, rssBefore)) {
- ++perProcessStats.mProcCompactionsRSSThrottled;
- ++perSourceStats.mProcCompactionsRSSThrottled;
+ mCompactStatsManager.logCompactionThrottled(
+ CompactionStatsManager.COMPACT_THROTTLE_REASON_DELTA_RSS,
+ compactSource, name);
return;
}
} else {
@@ -2065,40 +1764,19 @@ public class CachedAppOptimizer {
long deltaSwapRss = rssAfter[RSS_SWAP_INDEX] - rssBefore[RSS_SWAP_INDEX];
switch (opt.getReqCompactProfile()) {
case SOME:
- ++perSourceStats.mSomeCompactPerformed;
- ++perProcessStats.mSomeCompactPerformed;
+ mCompactStatsManager.logSomeCompactionPerformed(compactSource,
+ name);
break;
case FULL:
- ++perSourceStats.mFullCompactPerformed;
- ++perProcessStats.mFullCompactPerformed;
long anonRssSavings = -deltaAnonRss;
long zramConsumed = zramUsedKbAfter - zramUsedKbBefore;
long memFreed = anonRssSavings - zramConsumed;
long totalCpuTimeMillis = deltaCpuTimeNanos / 1000000;
long origAnonRss = rssBefore[RSS_ANON_INDEX];
-
- // Negative stats would skew averages and will likely be due to
- // noise of system doing other things so we put a floor at 0 to
- // avoid negative values.
- anonRssSavings = anonRssSavings > 0 ? anonRssSavings : 0;
- zramConsumed = zramConsumed > 0 ? zramConsumed : 0;
- memFreed = memFreed > 0 ? memFreed : 0;
-
- perProcessStats.addMemStats(anonRssSavings, zramConsumed, memFreed,
- origAnonRss, totalCpuTimeMillis);
- perSourceStats.addMemStats(anonRssSavings, zramConsumed, memFreed,
- origAnonRss, totalCpuTimeMillis);
- SingleCompactionStats memStats = new SingleCompactionStats(rssAfter,
- compactSource, name, anonRssSavings, zramConsumed, memFreed,
- origAnonRss, totalCpuTimeMillis, procState, newOomAdj,
- oomAdjReason, proc.uid);
- mLastCompactionStats.remove(pid);
- mLastCompactionStats.put(pid, memStats);
- mCompactionStatsHistory.add(memStats);
- if (!forceCompaction) {
- // Avoid polluting field metrics with forced compactions.
- memStats.sendStat();
- }
+ mCompactStatsManager.logFullCompactionPerformed(compactSource, name,
+ anonRssSavings, zramConsumed, memFreed, origAnonRss,
+ totalCpuTimeMillis, rssAfter, procState, newOomAdj,
+ oomAdjReason, proc.uid, pid, !forceCompaction);
break;
default:
// We likely missed adding this category, it needs to be added
@@ -2129,12 +1807,12 @@ public class CachedAppOptimizer {
break;
}
case COMPACT_SYSTEM_MSG: {
- ++mSystemCompactionsPerformed;
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "compactSystem");
long memFreedBefore = getMemoryFreedCompaction();
compactSystem();
long memFreedAfter = getMemoryFreedCompaction();
- mSystemTotalMemFreed += memFreedAfter - memFreedBefore;
+ long memFreed = memFreedAfter - memFreedBefore;
+ mCompactStatsManager.logSystemCompactionPerformed(memFreed);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
break;
}
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 363ba82cb332..cd40905b01da 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -4057,7 +4057,6 @@ public class OomAdjuster {
+ " mNewNumServiceProcs=" + mNewNumServiceProcs);
}
- @GuardedBy("mProcLock")
void dumpCachedAppOptimizerSettings(PrintWriter pw) {
mCachedAppOptimizer.dump(pw);
}
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index e0fbaf43ea43..27e9e44f1090 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -31,7 +31,6 @@ import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE;
import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL;
-import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK;
import static android.os.PowerWhitelistManager.REASON_BOOT_COMPLETED;
import static android.os.PowerWhitelistManager.REASON_LOCKED_BOOT_COMPLETED;
import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
@@ -3905,6 +3904,10 @@ class UserController implements Handler.Callback {
return mService.mWindowManager;
}
+ ActivityTaskManagerInternal getActivityTaskManagerInternal() {
+ return mService.mAtmInternal;
+ }
+
void activityManagerOnUserStopped(@UserIdInt int userId) {
LocalServices.getService(ActivityTaskManagerInternal.class).onUserStopped(userId);
}
@@ -4119,25 +4122,40 @@ class UserController implements Handler.Callback {
}
void lockDeviceNowAndWaitForKeyguardShown() {
+ if (getWindowManager().isKeyguardLocked()) {
+ Slogf.w(TAG, "Not locking the device since the keyguard is already locked");
+ return;
+ }
+
final TimingsTraceAndSlog t = new TimingsTraceAndSlog();
t.traceBegin("lockDeviceNowAndWaitForKeyguardShown");
final CountDownLatch latch = new CountDownLatch(1);
- Bundle bundle = new Bundle();
- bundle.putBinder(LOCK_ON_USER_SWITCH_CALLBACK, new IRemoteCallback.Stub() {
- public void sendResult(Bundle data) {
- latch.countDown();
- }
- });
- getWindowManager().lockNow(bundle);
+ ActivityTaskManagerInternal.ScreenObserver screenObserver =
+ new ActivityTaskManagerInternal.ScreenObserver() {
+ @Override
+ public void onAwakeStateChanged(boolean isAwake) {
+
+ }
+
+ @Override
+ public void onKeyguardStateChanged(boolean isShowing) {
+ if (isShowing) {
+ latch.countDown();
+ }
+ }
+ };
+
+ getActivityTaskManagerInternal().registerScreenObserver(screenObserver);
+ getWindowManager().lockDeviceNow();
try {
if (!latch.await(20, TimeUnit.SECONDS)) {
- throw new RuntimeException("User controller expected a callback while waiting "
- + "to show the keyguard. Timed out after 20 seconds.");
+ throw new RuntimeException("Keyguard is not shown in 20 seconds");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
+ getActivityTaskManagerInternal().unregisterScreenObserver(screenObserver);
t.traceEnd();
}
}
diff --git a/services/core/java/com/android/server/am/compaction/AggregatedCompactionStats.java b/services/core/java/com/android/server/am/compaction/AggregatedCompactionStats.java
new file mode 100644
index 000000000000..836670cd10eb
--- /dev/null
+++ b/services/core/java/com/android/server/am/compaction/AggregatedCompactionStats.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.am.compaction;
+
+import dalvik.annotation.optimization.NeverCompile;
+
+import java.io.PrintWriter;
+
+class AggregatedCompactionStats {
+ // Throttling stats
+ public long mFullCompactRequested;
+ public long mSomeCompactRequested;
+ public long mFullCompactPerformed;
+ public long mSomeCompactPerformed;
+ public long mProcCompactionsNoPidThrottled;
+ public long mProcCompactionsOomAdjThrottled;
+ public long mProcCompactionsTimeThrottled;
+ public long mProcCompactionsRSSThrottled;
+ public long mProcCompactionsMiscThrottled;
+
+ // Memory stats
+ public long mTotalDeltaAnonRssKBs;
+ public long mTotalZramConsumedKBs;
+ public long mTotalAnonMemFreedKBs;
+ public long mSumOrigAnonRss;
+ public double mMaxCompactEfficiency;
+ public double mMaxSwapEfficiency;
+
+ // Cpu time
+ public long mTotalCpuTimeMillis;
+
+ public long getThrottledSome() { return mSomeCompactRequested - mSomeCompactPerformed; }
+
+ public long getThrottledFull() { return mFullCompactRequested - mFullCompactPerformed; }
+
+ public void addMemStats(long anonRssSaved, long zramConsumed, long memFreed,
+ long origAnonRss, long totalCpuTimeMillis) {
+ final double compactEfficiency = memFreed / (double) origAnonRss;
+ if (compactEfficiency > mMaxCompactEfficiency) {
+ mMaxCompactEfficiency = compactEfficiency;
+ }
+ final double swapEfficiency = anonRssSaved / (double) origAnonRss;
+ if (swapEfficiency > mMaxSwapEfficiency) {
+ mMaxSwapEfficiency = swapEfficiency;
+ }
+ mTotalDeltaAnonRssKBs += anonRssSaved;
+ mTotalZramConsumedKBs += zramConsumed;
+ mTotalAnonMemFreedKBs += memFreed;
+ mSumOrigAnonRss += origAnonRss;
+ mTotalCpuTimeMillis += totalCpuTimeMillis;
+ }
+
+ @NeverCompile
+ public void dump(PrintWriter pw) {
+ long totalCompactRequested = mSomeCompactRequested + mFullCompactRequested;
+ long totalCompactPerformed = mSomeCompactPerformed + mFullCompactPerformed;
+ pw.println(" Performed / Requested:");
+ pw.println(" Some: (" + mSomeCompactPerformed + "/" + mSomeCompactRequested + ")");
+ pw.println(" Full: (" + mFullCompactPerformed + "/" + mFullCompactRequested + ")");
+
+ long throttledSome = getThrottledSome();
+ long throttledFull = getThrottledFull();
+
+ if (throttledSome > 0 || throttledFull > 0) {
+ pw.println(" Throttled:");
+ pw.println(" Some: " + throttledSome + " Full: " + throttledFull);
+ pw.println(" Throttled by Type:");
+ final long compactionsThrottled = totalCompactRequested - totalCompactPerformed;
+ // Any throttle that was not part of the previous categories
+ final long unaccountedThrottled = compactionsThrottled
+ - mProcCompactionsNoPidThrottled - mProcCompactionsOomAdjThrottled
+ - mProcCompactionsTimeThrottled - mProcCompactionsRSSThrottled
+ - mProcCompactionsMiscThrottled;
+ pw.println(" NoPid: " + mProcCompactionsNoPidThrottled
+ + " OomAdj: " + mProcCompactionsOomAdjThrottled + " Time: "
+ + mProcCompactionsTimeThrottled + " RSS: " + mProcCompactionsRSSThrottled
+ + " Misc: " + mProcCompactionsMiscThrottled
+ + " Unaccounted: " + unaccountedThrottled);
+ final double compactThrottlePercentage =
+ (compactionsThrottled / (double) totalCompactRequested) * 100.0;
+ pw.println(" Throttle Percentage: " + compactThrottlePercentage);
+ }
+
+ if (mFullCompactPerformed > 0) {
+ pw.println(" -----Memory Stats----");
+ pw.println(" Total Delta Anon RSS (KB) : " + mTotalDeltaAnonRssKBs);
+ pw.println(" Total Physical ZRAM Consumed (KB): " + mTotalZramConsumedKBs);
+ // Anon Mem Freed = Delta Anon RSS - ZRAM Consumed
+ pw.println(" Total Anon Memory Freed (KB): " + mTotalAnonMemFreedKBs);
+ pw.println(" Avg Swap Efficiency (KB) (Delta Anon RSS/Orig Anon RSS): "
+ + (mTotalDeltaAnonRssKBs / (double) mSumOrigAnonRss));
+ pw.println(" Max Swap Efficiency: " + mMaxSwapEfficiency);
+ // This tells us how much anon memory we were able to free thanks to running
+ // compaction
+ pw.println(" Avg Compaction Efficiency (Anon Freed/Anon RSS): "
+ + (mTotalAnonMemFreedKBs / (double) mSumOrigAnonRss));
+ pw.println(" Max Compaction Efficiency: " + mMaxCompactEfficiency);
+ // This tells us how effective is the compression algorithm in physical memory
+ pw.println(" Avg Compression Ratio (1 - ZRAM Consumed/DeltaAnonRSS): "
+ + (1.0 - mTotalZramConsumedKBs / (double) mTotalDeltaAnonRssKBs));
+ long avgKBsPerProcCompact = mFullCompactPerformed > 0
+ ? (mTotalAnonMemFreedKBs / mFullCompactPerformed)
+ : 0;
+ pw.println(" Avg Anon Mem Freed/Compaction (KB) : " + avgKBsPerProcCompact);
+ double compactionCost =
+ mTotalCpuTimeMillis / (mTotalAnonMemFreedKBs / 1024.0); // ms/MB
+ pw.println(" Compaction Cost (ms/MB): " + compactionCost);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/am/compaction/AggregatedProcessCompactionStats.java b/services/core/java/com/android/server/am/compaction/AggregatedProcessCompactionStats.java
new file mode 100644
index 000000000000..53019f93267a
--- /dev/null
+++ b/services/core/java/com/android/server/am/compaction/AggregatedProcessCompactionStats.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am.compaction;
+
+final class AggregatedProcessCompactionStats extends AggregatedCompactionStats {
+ public final String mProcessName;
+
+ public AggregatedProcessCompactionStats(String processName) { this.mProcessName = processName; }
+}
diff --git a/services/core/java/com/android/server/am/compaction/AggregatedSourceCompactionStats.java b/services/core/java/com/android/server/am/compaction/AggregatedSourceCompactionStats.java
new file mode 100644
index 000000000000..39a10d52a217
--- /dev/null
+++ b/services/core/java/com/android/server/am/compaction/AggregatedSourceCompactionStats.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am.compaction;
+
+import com.android.server.am.CachedAppOptimizer;
+
+final class AggregatedSourceCompactionStats extends AggregatedCompactionStats {
+ public final CachedAppOptimizer.CompactSource mSourceType;
+
+ public AggregatedSourceCompactionStats(CachedAppOptimizer.CompactSource sourceType) {
+ this.mSourceType = sourceType;
+ }
+} \ No newline at end of file
diff --git a/services/core/java/com/android/server/am/compaction/CompactionStatsManager.java b/services/core/java/com/android/server/am/compaction/CompactionStatsManager.java
new file mode 100644
index 000000000000..98368dacb86d
--- /dev/null
+++ b/services/core/java/com/android/server/am/compaction/CompactionStatsManager.java
@@ -0,0 +1,320 @@
+/*
+ * 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.compaction;
+
+import android.annotation.IntDef;
+import android.app.ActivityManagerInternal;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.am.CachedAppOptimizer;
+
+import dalvik.annotation.optimization.NeverCompile;
+
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.EnumMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+public final class CompactionStatsManager {
+ private static CompactionStatsManager sInstance;
+
+ private static String TAG = "CompactionStatsManager";
+
+ // Size of history for the last 20 compactions for any process
+ static final int LAST_COMPACTED_ANY_PROCESS_STATS_HISTORY_SIZE = 20;
+
+ // Amount of processes supported to record for their last compaction.
+ static final int LAST_COMPACTION_FOR_PROCESS_STATS_SIZE = 256;
+
+ public static final int COMPACT_THROTTLE_REASON_NO_PID = 0;
+ public static final int COMPACT_THROTTLE_REASON_OOM_ADJ = 1;
+ public static final int COMPACT_THROTTLE_REASON_TIME_TOO_SOON = 2;
+ public static final int COMPACT_THROTTLE_REASON_PROC_STATE = 3;
+ public static final int COMPACT_THROTTLE_REASON_DELTA_RSS = 4;
+ @IntDef(value = {
+ COMPACT_THROTTLE_REASON_NO_PID, COMPACT_THROTTLE_REASON_OOM_ADJ,
+ COMPACT_THROTTLE_REASON_TIME_TOO_SOON, COMPACT_THROTTLE_REASON_PROC_STATE,
+ COMPACT_THROTTLE_REASON_DELTA_RSS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CompactThrottleReason {}
+
+ private final LinkedHashMap<String, AggregatedProcessCompactionStats> mPerProcessCompactStats =
+ new LinkedHashMap<>(256);
+ private final EnumMap<CachedAppOptimizer.CompactSource, AggregatedSourceCompactionStats>
+ mPerSourceCompactStats =
+ new EnumMap<>(CachedAppOptimizer.CompactSource.class);
+
+ private long mTotalCompactionDowngrades;
+ private long mSystemCompactionsPerformed;
+ private long mSystemTotalMemFreed;
+ private EnumMap<CachedAppOptimizer.CancelCompactReason, Integer> mTotalCompactionsCancelled =
+ new EnumMap<>(CachedAppOptimizer.CancelCompactReason.class);
+
+ // Maps process ID to last compaction statistics for processes that we've fully compacted. Used
+ // when evaluating throttles that we only consider for "full" compaction, so we don't store
+ // data for "some" compactions. Uses LinkedHashMap to ensure insertion order is kept and
+ // facilitate removal of the oldest entry.
+ @VisibleForTesting
+ @GuardedBy("mProcLock")
+ LinkedHashMap<Integer, SingleCompactionStats> mLastCompactionStats =
+ new LinkedHashMap<Integer, SingleCompactionStats>() {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return size() > LAST_COMPACTION_FOR_PROCESS_STATS_SIZE;
+ }
+ };
+
+ LinkedList<SingleCompactionStats> mCompactionStatsHistory =
+ new LinkedList<SingleCompactionStats>() {
+ @Override
+ public boolean add(SingleCompactionStats e) {
+ if (size() >= LAST_COMPACTED_ANY_PROCESS_STATS_HISTORY_SIZE) {
+ this.remove();
+ }
+ return super.add(e);
+ }
+ };
+
+ public static CompactionStatsManager getInstance() {
+ if (sInstance == null) {
+ sInstance = new CompactionStatsManager();
+ }
+ return sInstance;
+ }
+
+ public SingleCompactionStats getLastCompactionStats(int pid) {
+ return mLastCompactionStats.get(pid);
+ }
+
+ @VisibleForTesting
+ public LinkedHashMap<Integer, SingleCompactionStats> getLastCompactionStats() {
+ return mLastCompactionStats;
+ }
+
+ @VisibleForTesting
+ public void reinit() {
+ sInstance = new CompactionStatsManager();
+ }
+
+
+ public void logCompactionRequested(CachedAppOptimizer.CompactSource source,
+ CachedAppOptimizer.CompactProfile compactProfile, String processName) {
+ AggregatedSourceCompactionStats perSourceStats = getPerSourceAggregatedCompactStat(source);
+ AggregatedCompactionStats perProcStats =
+ getPerProcessAggregatedCompactStat(processName);
+
+ switch (compactProfile) {
+ case SOME:
+ ++perProcStats.mSomeCompactRequested;
+ ++perSourceStats.mSomeCompactRequested;
+ break;
+ case FULL:
+ ++perProcStats.mFullCompactRequested;
+ ++perSourceStats.mFullCompactRequested;
+ break;
+ default:
+ Slog.e(TAG,
+ "Stats cannot be logged for compaction type."+compactProfile);
+ }
+ }
+ public void logCompactionThrottled(@CompactThrottleReason int reason,
+ CachedAppOptimizer.CompactSource source, String processName) {
+ AggregatedSourceCompactionStats perSourceStats =
+ getPerSourceAggregatedCompactStat(source);
+ AggregatedProcessCompactionStats perProcessStats =
+ getPerProcessAggregatedCompactStat(processName);
+
+ switch(reason) {
+ case COMPACT_THROTTLE_REASON_NO_PID:
+ ++perSourceStats.mProcCompactionsNoPidThrottled;
+ ++perProcessStats.mProcCompactionsNoPidThrottled;
+ break;
+ case COMPACT_THROTTLE_REASON_OOM_ADJ:
+ ++perProcessStats.mProcCompactionsOomAdjThrottled;
+ ++perSourceStats.mProcCompactionsOomAdjThrottled;
+ break;
+ case COMPACT_THROTTLE_REASON_TIME_TOO_SOON:
+ ++perProcessStats.mProcCompactionsTimeThrottled;
+ ++perSourceStats.mProcCompactionsTimeThrottled;
+ break;
+ case COMPACT_THROTTLE_REASON_PROC_STATE:
+ ++perProcessStats.mProcCompactionsMiscThrottled;
+ ++perSourceStats.mProcCompactionsMiscThrottled;
+ break;
+ case COMPACT_THROTTLE_REASON_DELTA_RSS:
+ ++perProcessStats.mProcCompactionsRSSThrottled;
+ ++perSourceStats.mProcCompactionsRSSThrottled;
+ break;
+ default:
+ break;
+ }
+ }
+
+ public void logSomeCompactionPerformed(CachedAppOptimizer.CompactSource source,
+ String processName) {
+ AggregatedSourceCompactionStats perSourceStats =
+ getPerSourceAggregatedCompactStat(source);
+ AggregatedProcessCompactionStats perProcessStats =
+ getPerProcessAggregatedCompactStat(processName);
+
+ ++perSourceStats.mSomeCompactPerformed;
+ ++perProcessStats.mSomeCompactPerformed;
+ }
+
+ public void logFullCompactionPerformed(
+ CachedAppOptimizer.CompactSource source, String processName, long anonRssSavings,
+ long zramConsumed, long memFreed, long origAnonRss, long totalCpuTimeMillis,
+ long[] rssAfterCompact, int procState, int newOomAdj,
+ @ActivityManagerInternal.OomAdjReason int oomAdjReason, int uid, int pid,
+ boolean logFieldMetric) {
+ AggregatedSourceCompactionStats perSourceStats =
+ getPerSourceAggregatedCompactStat(source);
+ AggregatedProcessCompactionStats perProcessStats =
+ getPerProcessAggregatedCompactStat(processName);
+
+ ++perSourceStats.mFullCompactPerformed;
+ ++perProcessStats.mFullCompactPerformed;
+
+ // Negative stats would skew averages and will likely be due to
+ // noise of system doing other things so we put a floor at 0 to
+ // avoid negative values.
+ anonRssSavings = anonRssSavings > 0 ? anonRssSavings : 0;
+ zramConsumed = zramConsumed > 0 ? zramConsumed : 0;
+ memFreed = memFreed > 0 ? memFreed : 0;
+
+ perProcessStats.addMemStats(anonRssSavings, zramConsumed, memFreed,
+ origAnonRss, totalCpuTimeMillis);
+ perSourceStats.addMemStats(anonRssSavings, zramConsumed, memFreed,
+ origAnonRss, totalCpuTimeMillis);
+ SingleCompactionStats memStats = new SingleCompactionStats(rssAfterCompact,
+ source, processName, anonRssSavings, zramConsumed, memFreed,
+ origAnonRss, totalCpuTimeMillis, procState, newOomAdj,
+ oomAdjReason, uid);
+ mLastCompactionStats.remove(pid);
+ mLastCompactionStats.put(pid, memStats);
+ mCompactionStatsHistory.add(memStats);
+ if (!logFieldMetric) {
+ memStats.sendStat();
+ }
+ }
+
+ public void logCompactionDowngrade() {
+ ++mTotalCompactionDowngrades;
+ }
+
+ public void logSystemCompactionPerformed(long memFreed) {
+ ++mSystemCompactionsPerformed;
+ mSystemTotalMemFreed += memFreed;
+ }
+
+ public void logCompactionCancelled(CachedAppOptimizer.CancelCompactReason cancelReason) {
+ if (mTotalCompactionsCancelled.containsKey(cancelReason)) {
+ int count = mTotalCompactionsCancelled.get(cancelReason);
+ mTotalCompactionsCancelled.put(cancelReason, count + 1);
+ } else {
+ mTotalCompactionsCancelled.put(cancelReason, 1);
+ }
+ }
+
+ private AggregatedProcessCompactionStats getPerProcessAggregatedCompactStat(
+ String processName) {
+ if (processName == null) {
+ processName = "";
+ }
+ AggregatedProcessCompactionStats stats = mPerProcessCompactStats.get(processName);
+ if (stats == null) {
+ stats = new AggregatedProcessCompactionStats(processName);
+ mPerProcessCompactStats.put(processName, stats);
+ }
+ return stats;
+ }
+
+ private AggregatedSourceCompactionStats getPerSourceAggregatedCompactStat(
+ CachedAppOptimizer.CompactSource source) {
+ AggregatedSourceCompactionStats stats = mPerSourceCompactStats.get(source);
+ if (stats == null) {
+ stats = new AggregatedSourceCompactionStats(source);
+ mPerSourceCompactStats.put(source, stats);
+ }
+ return stats;
+ }
+
+ @NeverCompile
+ public void dump(PrintWriter pw) {
+ pw.println(" Per-Process Compaction Stats");
+ long totalCompactPerformedSome = 0;
+ long totalCompactPerformedFull = 0;
+ for (AggregatedProcessCompactionStats stats : mPerProcessCompactStats.values()) {
+ pw.println("-----" + stats.mProcessName + "-----");
+ totalCompactPerformedSome += stats.mSomeCompactPerformed;
+ totalCompactPerformedFull += stats.mFullCompactPerformed;
+ stats.dump(pw);
+ pw.println();
+ }
+ pw.println();
+ pw.println(" Per-Source Compaction Stats");
+ for (AggregatedSourceCompactionStats stats : mPerSourceCompactStats.values()) {
+ pw.println("-----" + stats.mSourceType + "-----");
+ stats.dump(pw);
+ pw.println();
+ }
+ pw.println();
+
+ pw.println("Total Compactions Performed by profile: " + totalCompactPerformedSome
+ + " some, " + totalCompactPerformedFull + " full");
+ pw.println("Total compactions downgraded: " + mTotalCompactionDowngrades);
+ pw.println("Total compactions cancelled by reason: ");
+ for (CachedAppOptimizer.CancelCompactReason reason : mTotalCompactionsCancelled.keySet()) {
+ pw.println(" " + reason + ": " + mTotalCompactionsCancelled.get(reason));
+ }
+ pw.println();
+
+ pw.println(" System Compaction Memory Stats");
+ pw.println(" Compactions Performed: " + mSystemCompactionsPerformed);
+ pw.println(" Total Memory Freed (KB): " + mSystemTotalMemFreed);
+ double avgKBsPerSystemCompact = mSystemCompactionsPerformed > 0
+ ? mSystemTotalMemFreed / mSystemCompactionsPerformed
+ : 0;
+ pw.println(" Avg Mem Freed per Compact (KB): " + avgKBsPerSystemCompact);
+ pw.println();
+ pw.println(" Tracking last compaction stats for " + mLastCompactionStats.size()
+ + " processes.");
+ pw.println("Last Compaction per process stats:");
+ pw.println(" (ProcessName,Source,DeltaAnonRssKBs,ZramConsumedKBs,AnonMemFreedKBs"
+ + ",SwapEfficiency,CompactEfficiency,CompactCost(ms/MB),procState,oomAdj,"
+ + "oomAdjReason)");
+ for (Map.Entry<Integer, SingleCompactionStats> entry :
+ mLastCompactionStats.entrySet()) {
+ SingleCompactionStats stats = entry.getValue();
+ stats.dump(pw);
+ }
+ pw.println();
+ pw.println("Last 20 Compactions Stats:");
+ pw.println(" (ProcessName,Source,DeltaAnonRssKBs,ZramConsumedKBs,AnonMemFreedKBs,"
+ + "SwapEfficiency,CompactEfficiency,CompactCost(ms/MB),procState,oomAdj,"
+ + "oomAdjReason)");
+ for (SingleCompactionStats stats : mCompactionStatsHistory) {
+ stats.dump(pw);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/am/compaction/OWNERS b/services/core/java/com/android/server/am/compaction/OWNERS
new file mode 100644
index 000000000000..54253b49daa3
--- /dev/null
+++ b/services/core/java/com/android/server/am/compaction/OWNERS
@@ -0,0 +1,5 @@
+edgararriaga@google.com
+shayba@google.com
+
+# Fallback
+include /PERFORMANCE_OWNERS
diff --git a/services/core/java/com/android/server/am/compaction/SingleCompactionStats.java b/services/core/java/com/android/server/am/compaction/SingleCompactionStats.java
new file mode 100644
index 000000000000..c20087ac973e
--- /dev/null
+++ b/services/core/java/com/android/server/am/compaction/SingleCompactionStats.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am.compaction;
+
+import android.app.ActivityManagerInternal;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.am.CachedAppOptimizer;
+import com.android.server.am.OomAdjuster;
+
+import dalvik.annotation.optimization.NeverCompile;
+
+import java.io.PrintWriter;
+import java.util.Random;
+
+public final class SingleCompactionStats {
+ private static final float STATSD_SAMPLE_RATE = 0.1f;
+ private static final Random mRandom = new Random();
+ private final long[] mRssAfterCompaction;
+ public CachedAppOptimizer.CompactSource mSourceType;
+ public String mProcessName;
+ public final int mUid;
+ public long mDeltaAnonRssKBs;
+ public long mZramConsumedKBs;
+ public long mAnonMemFreedKBs;
+ public float mCpuTimeMillis;
+ public long mOrigAnonRss;
+ public int mProcState;
+ public int mOomAdj;
+ public @ActivityManagerInternal.OomAdjReason int mOomAdjReason;
+
+ SingleCompactionStats(long[] rss, CachedAppOptimizer.CompactSource source, String processName,
+ long deltaAnonRss, long zramConsumed, long anonMemFreed, long origAnonRss,
+ long cpuTimeMillis, int procState, int oomAdj,
+ @ActivityManagerInternal.OomAdjReason int oomAdjReason, int uid) {
+ mRssAfterCompaction = rss;
+ mSourceType = source;
+ mProcessName = processName;
+ mUid = uid;
+ mDeltaAnonRssKBs = deltaAnonRss;
+ mZramConsumedKBs = zramConsumed;
+ mAnonMemFreedKBs = anonMemFreed;
+ mCpuTimeMillis = cpuTimeMillis;
+ mOrigAnonRss = origAnonRss;
+ mProcState = procState;
+ mOomAdj = oomAdj;
+ mOomAdjReason = oomAdjReason;
+ }
+
+ double getCompactEfficiency() { return mAnonMemFreedKBs / (double) mOrigAnonRss; }
+
+ double getSwapEfficiency() { return mDeltaAnonRssKBs / (double) mOrigAnonRss; }
+
+ double getCompactCost() {
+ // mCpuTimeMillis / (anonMemFreedKBs/1024) and metric is in (ms/MB)
+ return mCpuTimeMillis / (double) mAnonMemFreedKBs * 1024;
+ }
+
+ public long[] getRssAfterCompaction() {
+ return mRssAfterCompaction;
+ }
+
+ @NeverCompile
+ void dump(PrintWriter pw) {
+ pw.println(" (" + mProcessName + "," + mSourceType.name() + "," + mDeltaAnonRssKBs
+ + "," + mZramConsumedKBs + "," + mAnonMemFreedKBs + ","
+ + getSwapEfficiency() + "," + getCompactEfficiency()
+ + "," + getCompactCost() + "," + mProcState + "," + mOomAdj + ","
+ + OomAdjuster.oomAdjReasonToString(mOomAdjReason) + ")");
+ }
+
+ void sendStat() {
+ if (mRandom.nextFloat() < STATSD_SAMPLE_RATE) {
+ FrameworkStatsLog.write(FrameworkStatsLog.APP_COMPACTED_V2, mUid, mProcState,
+ mOomAdj, mDeltaAnonRssKBs, mZramConsumedKBs, mCpuTimeMillis, mOrigAnonRss,
+ mOomAdjReason);
+ }
+ }
+} \ No newline at end of file
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 32c4e9b1727e..2a9762caaf79 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -72,6 +72,7 @@ import static android.content.Intent.EXTRA_REPLACING;
import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP;
import static android.os.Flags.binderFrozenStateChangeCallback;
+import static android.permission.flags.Flags.appOpsServiceHandlerFix;
import static android.permission.flags.Flags.checkOpValidatePackage;
import static android.permission.flags.Flags.deviceAwareAppOpNewSchemaEnabled;
import static android.permission.flags.Flags.useFrozenAwareRemoteCallbackList;
@@ -174,6 +175,7 @@ import com.android.internal.util.XmlUtils;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.IoThread;
import com.android.server.LocalManagerRegistry;
import com.android.server.LocalServices;
import com.android.server.LockGuard;
@@ -277,6 +279,7 @@ public class AppOpsService extends IAppOpsService.Stub {
final AtomicFile mStorageFile;
final AtomicFile mRecentAccessesFile;
private final @Nullable File mNoteOpCallerStacktracesFile;
+ /* AMS handler, this shouldn't be used for IO */
final Handler mHandler;
private final AppOpsRecentAccessPersistence mRecentAccessPersistence;
@@ -1411,7 +1414,7 @@ public class AppOpsService extends IAppOpsService.Stub {
@GuardedBy("this")
private void packageRemovedLocked(int uid, String packageName) {
- mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::clearHistory,
+ getIoHandler().post(PooledLambda.obtainRunnable(HistoricalRegistry::clearHistory,
mHistoricalRegistry, uid, packageName));
UidState uidState = mUidStates.get(uid);
@@ -1693,7 +1696,7 @@ public class AppOpsService extends IAppOpsService.Stub {
if (mWriteScheduled) {
mWriteScheduled = false;
mFastWriteScheduled = false;
- mHandler.removeCallbacks(mWriteRunner);
+ getIoHandler().removeCallbacks(mWriteRunner);
doWrite = true;
}
}
@@ -1979,7 +1982,7 @@ public class AppOpsService extends IAppOpsService.Stub {
new String[attributionChainExemptPackages.size()]) : null;
// Must not hold the appops lock
- mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOps,
+ getIoHandler().post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOps,
mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType,
filter, beginTimeMillis, endTimeMillis, flags, chainExemptPkgArray,
callback).recycleOnUse());
@@ -2010,7 +2013,8 @@ public class AppOpsService extends IAppOpsService.Stub {
new String[attributionChainExemptPackages.size()]) : null;
// Must not hold the appops lock
- mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOpsFromDiskRaw,
+ getIoHandler().post(PooledLambda.obtainRunnable(
+ HistoricalRegistry::getHistoricalOpsFromDiskRaw,
mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType,
filter, beginTimeMillis, endTimeMillis, flags, chainExemptPkgArray,
callback).recycleOnUse());
@@ -5074,7 +5078,7 @@ public class AppOpsService extends IAppOpsService.Stub {
private void scheduleWriteLocked() {
if (!mWriteScheduled) {
mWriteScheduled = true;
- mHandler.postDelayed(mWriteRunner, WRITE_DELAY);
+ getIoHandler().postDelayed(mWriteRunner, WRITE_DELAY);
}
}
@@ -5082,8 +5086,8 @@ public class AppOpsService extends IAppOpsService.Stub {
if (!mFastWriteScheduled) {
mWriteScheduled = true;
mFastWriteScheduled = true;
- mHandler.removeCallbacks(mWriteRunner);
- mHandler.postDelayed(mWriteRunner, 10*1000);
+ getIoHandler().removeCallbacks(mWriteRunner);
+ getIoHandler().postDelayed(mWriteRunner, 10 * 1000);
}
}
@@ -5957,7 +5961,8 @@ public class AppOpsService extends IAppOpsService.Stub {
final long token = Binder.clearCallingIdentity();
try {
synchronized (shell.mInternal) {
- shell.mInternal.mHandler.removeCallbacks(shell.mInternal.mWriteRunner);
+ shell.mInternal.getIoHandler().removeCallbacks(
+ shell.mInternal.mWriteRunner);
}
shell.mInternal.writeRecentAccesses();
shell.mInternal.mAppOpsCheckingService.writeState();
@@ -7884,4 +7889,12 @@ public class AppOpsService extends IAppOpsService.Stub {
return null;
}
}
+
+ private Handler getIoHandler() {
+ if (appOpsServiceHandlerFix()) {
+ return IoThread.getHandler();
+ } else {
+ return mHandler;
+ }
+ }
}
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
index 310f592ddf5c..b298d1c2ac66 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
@@ -390,6 +390,9 @@ public class HdmiCecMessageValidator {
private static boolean isValidPhysicalAddress(byte[] params, int offset) {
int physicalAddress = HdmiUtils.twoBytesToInt(params, offset);
+ if (physicalAddress == 0xFFFF) {
+ return false;
+ }
while (physicalAddress != 0) {
int maskedAddress = physicalAddress & 0xF000;
physicalAddress = (physicalAddress << 4) & 0xFFFF;
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index af021e5f515b..2ad5a1538da9 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -25,6 +25,7 @@ import static android.view.KeyEvent.KEYCODE_UNKNOWN;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static com.android.hardware.input.Flags.touchpadVisualizer;
+import static com.android.hardware.input.Flags.keyEventActivityDetection;
import static com.android.hardware.input.Flags.useKeyGestureEventHandler;
import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER;
@@ -61,6 +62,7 @@ import android.hardware.input.IInputDeviceBatteryState;
import android.hardware.input.IInputDevicesChangedListener;
import android.hardware.input.IInputManager;
import android.hardware.input.IInputSensorEventListener;
+import android.hardware.input.IKeyEventActivityListener;
import android.hardware.input.IKeyGestureEventListener;
import android.hardware.input.IKeyGestureHandler;
import android.hardware.input.IKeyboardBacklightListener;
@@ -89,11 +91,13 @@ import android.os.InputEventInjectionResult;
import android.os.InputEventInjectionSync;
import android.os.Looper;
import android.os.Message;
+import android.os.PermissionEnforcer;
import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.os.VibrationEffect;
import android.os.vibrator.StepSegment;
@@ -280,6 +284,16 @@ public class InputManagerService extends IInputManager.Stub
@GuardedBy("mAssociationsLock")
private final Map<String, Integer> mRuntimeAssociations = new ArrayMap<>();
+ final Object mKeyEventActivityLock = new Object();
+ @GuardedBy("mKeyEventActivityLock")
+ private List<IKeyEventActivityListener> mKeyEventActivityListenersToNotify =
+ new ArrayList<>();
+
+ // Rate limit for key event activity detection. Prevent the listener from being notified
+ // too frequently.
+ private static final long KEY_EVENT_ACTIVITY_RATE_LIMIT_INTERVAL_MS = 1000;
+ private long mLastKeyEventActivityTimeMs = 0;
+
// The associations of input devices to displays by port. Maps from {InputDevice#mName} (String)
// to {DisplayInfo#uniqueId} (String) so that events from the Input Device go to a
// specific display.
@@ -484,13 +498,16 @@ public class InputManagerService extends IInputManager.Stub
}
public InputManagerService(Context context) {
- this(new Injector(context, DisplayThread.get().getLooper(), new UEventManager() {}));
+ this(new Injector(context, DisplayThread.get().getLooper(), new UEventManager() {}),
+ context.getSystemService(PermissionEnforcer.class));
}
@VisibleForTesting
- InputManagerService(Injector injector) {
+ InputManagerService(Injector injector, PermissionEnforcer permissionEnforcer) {
// The static association map is accessed by both java and native code, so it must be
// initialized before initializing the native service.
+ super(permissionEnforcer);
+
mStaticAssociations = loadStaticInputPortAssociations();
mContext = injector.getContext();
@@ -2509,9 +2526,73 @@ public class InputManagerService extends IInputManager.Stub
return true;
}
+ @EnforcePermission(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY)
+ @Override // Binder Call
+ public boolean registerKeyEventActivityListener(@NonNull IKeyEventActivityListener listener) {
+ super.registerKeyEventActivityListener_enforcePermission();
+ Objects.requireNonNull(listener, "listener must not be null");
+ return InputManagerService.this.registerKeyEventActivityListenerInternal(listener);
+ }
+
+ @EnforcePermission(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY)
+ @Override // Binder Call
+ public boolean unregisterKeyEventActivityListener(@NonNull IKeyEventActivityListener listener) {
+ super.unregisterKeyEventActivityListener_enforcePermission();
+ Objects.requireNonNull(listener, "listener must not be null");
+ return InputManagerService.this.unregisterKeyEventActivityListenerInternal(listener);
+ }
+
+ /**
+ * Registers a listener for updates to key event activeness
+ */
+ private boolean registerKeyEventActivityListenerInternal(IKeyEventActivityListener listener) {
+ synchronized (mKeyEventActivityLock) {
+ if (!mKeyEventActivityListenersToNotify.contains(listener)) {
+ mKeyEventActivityListenersToNotify.add(listener);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Unregisters a listener for updates to key event activeness
+ */
+ private boolean unregisterKeyEventActivityListenerInternal(IKeyEventActivityListener listener) {
+ synchronized (mKeyEventActivityLock) {
+ return mKeyEventActivityListenersToNotify.removeIf(existingListener ->
+ existingListener.asBinder() == listener.asBinder());
+ }
+ }
+
+ private void notifyKeyActivityListeners(KeyEvent event) {
+ long currentTimeMs = SystemClock.uptimeMillis();
+ if (keyEventActivityDetection()
+ && event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0
+ && currentTimeMs - mLastKeyEventActivityTimeMs
+ >= KEY_EVENT_ACTIVITY_RATE_LIMIT_INTERVAL_MS) {
+ List<IKeyEventActivityListener> keyEventActivityListeners;
+ synchronized (mKeyEventActivityLock) {
+ keyEventActivityListeners = List.copyOf(mKeyEventActivityListenersToNotify);
+ }
+ for (IKeyEventActivityListener listener : keyEventActivityListeners) {
+ try {
+ listener.onKeyEventActivity();
+ } catch (RemoteException e) {
+ Slog.i(TAG,
+ "Could Not Notify Listener due to Remote Exception: " + e);
+ unregisterKeyEventActivityListener(listener);
+ }
+ }
+ mLastKeyEventActivityTimeMs = currentTimeMs;
+ }
+ }
+
// Native callback.
@SuppressWarnings("unused")
- private int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
+ @VisibleForTesting
+ public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
+ notifyKeyActivityListeners(event);
synchronized (mFocusEventDebugViewLock) {
if (mFocusEventDebugView != null) {
mFocusEventDebugView.reportKeyEvent(event);
diff --git a/services/core/java/com/android/server/om/IdmapDaemon.java b/services/core/java/com/android/server/om/IdmapDaemon.java
index d33c860343c5..9e311511c24f 100644
--- a/services/core/java/com/android/server/om/IdmapDaemon.java
+++ b/services/core/java/com/android/server/om/IdmapDaemon.java
@@ -26,6 +26,7 @@ import android.os.FabricatedOverlayInfo;
import android.os.FabricatedOverlayInternal;
import android.os.IBinder;
import android.os.IIdmap2;
+import android.os.OverlayConstraint;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.StrictMode;
@@ -135,8 +136,8 @@ class IdmapDaemon {
}
String createIdmap(@NonNull String targetPath, @NonNull String overlayPath,
- @Nullable String overlayName, int policies, boolean enforce, int userId)
- throws TimeoutException, RemoteException {
+ @Nullable String overlayName, int policies, boolean enforce, int userId,
+ @NonNull OverlayConstraint[] constraints) throws TimeoutException, RemoteException {
try (Connection c = connect()) {
final IIdmap2 idmap2 = c.getIdmap2();
if (idmap2 == null) {
@@ -147,7 +148,7 @@ class IdmapDaemon {
}
return idmap2.createIdmap(targetPath, overlayPath, TextUtils.emptyIfNull(overlayName),
- policies, enforce, userId);
+ policies, enforce, userId, constraints);
}
}
@@ -165,8 +166,8 @@ class IdmapDaemon {
}
boolean verifyIdmap(@NonNull String targetPath, @NonNull String overlayPath,
- @Nullable String overlayName, int policies, boolean enforce, int userId)
- throws Exception {
+ @Nullable String overlayName, int policies, boolean enforce, int userId,
+ @NonNull OverlayConstraint[] constraints) throws Exception {
try (Connection c = connect()) {
final IIdmap2 idmap2 = c.getIdmap2();
if (idmap2 == null) {
@@ -177,7 +178,7 @@ class IdmapDaemon {
}
return idmap2.verifyIdmap(targetPath, overlayPath, TextUtils.emptyIfNull(overlayName),
- policies, enforce, userId);
+ policies, enforce, userId, constraints);
}
}
diff --git a/services/core/java/com/android/server/om/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java
index 86d05d92c95b..4e86aa00657d 100644
--- a/services/core/java/com/android/server/om/IdmapManager.java
+++ b/services/core/java/com/android/server/om/IdmapManager.java
@@ -22,6 +22,7 @@ import static com.android.server.om.OverlayManagerService.TAG;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
+import android.content.om.OverlayConstraint;
import android.content.om.OverlayInfo;
import android.content.om.OverlayableInfo;
import android.os.Build.VERSION_CODES;
@@ -102,7 +103,8 @@ final class IdmapManager {
*/
@IdmapStatus int createIdmap(@NonNull final AndroidPackage targetPackage,
@NonNull PackageState overlayPackageState, @NonNull final AndroidPackage overlayPackage,
- String overlayBasePath, String overlayName, @UserIdInt int userId) {
+ String overlayBasePath, String overlayName, @UserIdInt int userId,
+ @NonNull final List<OverlayConstraint> constraints) {
if (DEBUG) {
Slog.d(TAG, "create idmap for " + targetPackage.getPackageName() + " and "
+ overlayPackage.getPackageName());
@@ -112,12 +114,13 @@ final class IdmapManager {
int policies = calculateFulfilledPolicies(targetPackage, overlayPackageState,
overlayPackage, userId);
boolean enforce = enforceOverlayable(overlayPackageState, overlayPackage);
+ android.os.OverlayConstraint[] idmapConstraints = toIdmapConstraints(constraints);
if (mIdmapDaemon.verifyIdmap(targetPath, overlayBasePath, overlayName, policies,
- enforce, userId)) {
+ enforce, userId, idmapConstraints)) {
return IDMAP_IS_VERIFIED;
}
final boolean idmapCreated = mIdmapDaemon.createIdmap(targetPath, overlayBasePath,
- overlayName, policies, enforce, userId) != null;
+ overlayName, policies, enforce, userId, idmapConstraints) != null;
return (idmapCreated) ? IDMAP_IS_MODIFIED | IDMAP_IS_VERIFIED : IDMAP_NOT_EXIST;
} catch (Exception e) {
Slog.w(TAG, "failed to generate idmap for " + targetPath + " and "
@@ -275,4 +278,19 @@ final class IdmapManager {
return false;
}
+
+ @NonNull
+ private static android.os.OverlayConstraint[] toIdmapConstraints(
+ @NonNull final List<OverlayConstraint> constraints) {
+ android.os.OverlayConstraint[] idmapConstraints =
+ new android.os.OverlayConstraint[constraints.size()];
+ int index = 0;
+ for (OverlayConstraint constraint : constraints) {
+ android.os.OverlayConstraint idmapConstraint = new android.os.OverlayConstraint();
+ idmapConstraint.type = constraint.getType();
+ idmapConstraint.value = constraint.getValue();
+ idmapConstraints[index++] = idmapConstraint;
+ }
+ return idmapConstraints;
+ }
}
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index d6b587b5f16d..e613700943bc 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -46,6 +46,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.om.IOverlayManager;
+import android.content.om.OverlayConstraint;
import android.content.om.OverlayIdentifier;
import android.content.om.OverlayInfo;
import android.content.om.OverlayManagerTransaction;
@@ -674,6 +675,18 @@ public final class OverlayManagerService extends SystemService {
@Override
public boolean setEnabled(@Nullable final String packageName, final boolean enable,
int userIdArg) {
+ return setEnabled(packageName, enable, userIdArg,
+ Collections.emptyList() /* constraints */);
+ }
+
+ @Override
+ public boolean enableWithConstraints(@Nullable final String packageName, int userIdArg,
+ @NonNull final List<OverlayConstraint> constraints) {
+ return setEnabled(packageName, true /* enable */, userIdArg, constraints);
+ }
+
+ private boolean setEnabled(@Nullable final String packageName, final boolean enable,
+ int userIdArg, @NonNull final List<OverlayConstraint> constraints) {
if (packageName == null) {
return false;
}
@@ -690,7 +703,7 @@ public final class OverlayManagerService extends SystemService {
synchronized (mLock) {
try {
updateTargetPackagesLocked(
- mImpl.setEnabled(overlay, enable, realUserId));
+ mImpl.setEnabled(overlay, enable, realUserId, constraints));
return true;
} catch (OperationFailedException e) {
return false;
@@ -989,13 +1002,15 @@ public final class OverlayManagerService extends SystemService {
case TYPE_SET_ENABLED:
Set<UserPackage> result = null;
result = CollectionUtils.addAll(result,
- mImpl.setEnabled(request.overlay, true, realUserId));
+ mImpl.setEnabled(request.overlay, true /* enable */, realUserId,
+ request.constraints));
result = CollectionUtils.addAll(result,
mImpl.setHighestPriority(request.overlay, realUserId));
return CollectionUtils.emptyIfNull(result);
case TYPE_SET_DISABLED:
- return mImpl.setEnabled(request.overlay, false, realUserId);
+ return mImpl.setEnabled(request.overlay, false /* enable */, realUserId,
+ request.constraints);
case TYPE_REGISTER_FABRICATED:
final FabricatedOverlayInternal fabricated =
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index 02c0190224d0..ee29849465e2 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -34,11 +34,13 @@ import static com.android.server.om.OverlayManagerService.TAG;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.om.CriticalOverlayInfo;
+import android.content.om.OverlayConstraint;
import android.content.om.OverlayIdentifier;
import android.content.om.OverlayInfo;
import android.content.pm.UserPackage;
import android.content.pm.overlay.OverlayPaths;
import android.content.pm.parsing.FrameworkParsingPackageUtils;
+import android.content.res.Flags;
import android.os.FabricatedOverlayInfo;
import android.os.FabricatedOverlayInternal;
import android.text.TextUtils;
@@ -246,7 +248,7 @@ final class OverlayManagerServiceImpl {
+ oi.targetPackageName + "' in category '" + oi.category + "' for user "
+ newUserId);
mSettings.setEnabled(overlay, newUserId, true);
- if (updateState(oi, newUserId, 0)) {
+ if (updateState(oi, newUserId, 0, oi.constraints)) {
CollectionUtils.add(updatedTargets,
UserPackage.of(oi.userId, oi.targetPackageName));
}
@@ -338,7 +340,7 @@ final class OverlayManagerServiceImpl {
for (int i = 0, n = overlays.size(); i < n; i++) {
final OverlayInfo oi = overlays.get(i);
try {
- modified |= updateState(oi, userId, flags);
+ modified |= updateState(oi, userId, flags, oi.constraints);
} catch (OverlayManagerSettings.BadKeyException e) {
Slog.e(TAG, "failed to update settings", e);
modified |= mSettings.remove(oi.getOverlayIdentifier(), userId);
@@ -386,7 +388,7 @@ final class OverlayManagerServiceImpl {
}
// Update the enabled state of the overlay.
- if (updateState(currentInfo, userId, flags)) {
+ if (updateState(currentInfo, userId, flags, currentInfo.constraints)) {
updatedTargets = CollectionUtils.add(updatedTargets,
UserPackage.of(userId, currentInfo.targetPackageName));
}
@@ -440,10 +442,22 @@ final class OverlayManagerServiceImpl {
@NonNull
Set<UserPackage> setEnabled(@NonNull final OverlayIdentifier overlay,
- final boolean enable, final int userId) throws OperationFailedException {
+ final boolean enable, final int userId,
+ @NonNull final List<OverlayConstraint> constraints)
+ throws OperationFailedException {
if (DEBUG) {
- Slog.d(TAG, String.format("setEnabled overlay=%s enable=%s userId=%d",
- overlay, enable, userId));
+ Slog.d(TAG, TextUtils.formatSimple(
+ "setEnabled overlay=%s enable=%s userId=%d constraints=%s",
+ overlay, enable, userId, OverlayConstraint.constraintsToString(constraints)));
+ }
+
+ boolean hasConstraints = constraints != null && !constraints.isEmpty();
+ if (!Flags.rroConstraints() && hasConstraints) {
+ throw new OperationFailedException("RRO constraints are not supported");
+ }
+ if (!enable && hasConstraints) {
+ throw new OperationFailedException(
+ "Constraints can only be set when enabling an overlay");
}
try {
@@ -455,7 +469,7 @@ final class OverlayManagerServiceImpl {
}
boolean modified = mSettings.setEnabled(overlay, userId, enable);
- modified |= updateState(oi, userId, 0);
+ modified |= updateState(oi, userId, 0, constraints);
if (modified) {
return Set.of(UserPackage.of(userId, oi.targetPackageName));
@@ -469,7 +483,7 @@ final class OverlayManagerServiceImpl {
Optional<UserPackage> setEnabledExclusive(@NonNull final OverlayIdentifier overlay,
boolean withinCategory, final int userId) throws OperationFailedException {
if (DEBUG) {
- Slog.d(TAG, String.format("setEnabledExclusive overlay=%s"
+ Slog.d(TAG, TextUtils.formatSimple("setEnabledExclusive overlay=%s"
+ " withinCategory=%s userId=%d", overlay, withinCategory, userId));
}
@@ -501,12 +515,16 @@ final class OverlayManagerServiceImpl {
// Disable the overlay.
modified |= mSettings.setEnabled(disabledOverlay, userId, false);
- modified |= updateState(disabledInfo, userId, 0);
+ modified |= updateState(disabledInfo, userId, 0 /* flags */,
+ Collections.emptyList() /* constraints */);
}
// Enable the selected overlay.
modified |= mSettings.setEnabled(overlay, userId, true);
- modified |= updateState(enabledInfo, userId, 0);
+ // No constraints should be applied when exclusively enabling an overlay within
+ // a category.
+ modified |= updateState(enabledInfo, userId, 0 /* flags */,
+ Collections.emptyList() /* constraints */);
if (modified) {
return Optional.of(UserPackage.of(userId, enabledInfo.targetPackageName));
@@ -569,7 +587,8 @@ final class OverlayManagerServiceImpl {
// overlay.
mSettings.setBaseCodePath(overlayIdentifier, userId, info.path);
}
- if (updateState(oi, userId, 0)) {
+ // No constraints should be applied when registering a fabricated overlay.
+ if (updateState(oi, userId, 0 /* flags */, Collections.emptyList() /* constraints */)) {
updatedTargets.add(UserPackage.of(userId, oi.targetPackageName));
}
} catch (OverlayManagerSettings.BadKeyException e) {
@@ -670,7 +689,7 @@ final class OverlayManagerServiceImpl {
Set<UserPackage> setHighestPriority(@NonNull final OverlayIdentifier overlay,
final int userId) throws OperationFailedException {
- try{
+ try {
if (DEBUG) {
Slog.d(TAG, "setHighestPriority overlay=" + overlay + " userId=" + userId);
}
@@ -693,7 +712,7 @@ final class OverlayManagerServiceImpl {
Optional<UserPackage> setLowestPriority(@NonNull final OverlayIdentifier overlay,
final int userId) throws OperationFailedException {
- try{
+ try {
if (DEBUG) {
Slog.d(TAG, "setLowestPriority packageName=" + overlay + " userId=" + userId);
}
@@ -784,7 +803,8 @@ final class OverlayManagerServiceImpl {
* Returns true if the settings/state was modified, false otherwise.
*/
private boolean updateState(@NonNull final CriticalOverlayInfo info,
- final int userId, final int flags) throws OverlayManagerSettings.BadKeyException {
+ final int userId, final int flags, @NonNull final List<OverlayConstraint> constraints)
+ throws OverlayManagerSettings.BadKeyException {
final OverlayIdentifier overlay = info.getOverlayIdentifier();
var targetPackageState =
mPackageManager.getPackageStateForUser(info.getTargetPackageName(), userId);
@@ -803,6 +823,7 @@ final class OverlayManagerServiceImpl {
}
modified |= mSettings.setCategory(overlay, userId, overlayPackage.getOverlayCategory());
+ modified |= mSettings.setConstraints(overlay, userId, constraints);
if (!info.isFabricated()) {
modified |= mSettings.setBaseCodePath(overlay, userId,
overlayPackage.getSplits().get(0).getPath());
@@ -817,7 +838,7 @@ final class OverlayManagerServiceImpl {
&& !isPackageConfiguredMutable(overlayPackage))) {
idmapStatus = mIdmapManager.createIdmap(targetPackage, overlayPackageState,
overlayPackage, updatedOverlayInfo.baseCodePath, overlay.getOverlayName(),
- userId);
+ userId, updatedOverlayInfo.constraints);
modified |= (idmapStatus & IDMAP_IS_MODIFIED) != 0;
}
@@ -826,7 +847,7 @@ final class OverlayManagerServiceImpl {
userId, flags, idmapStatus);
if (currentState != newState) {
if (DEBUG) {
- Slog.d(TAG, String.format("%s:%d: %s -> %s",
+ Slog.d(TAG, TextUtils.formatSimple("%s:%d: %s -> %s",
overlay, userId,
OverlayInfo.stateToString(currentState),
OverlayInfo.stateToString(newState)));
diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java
index e6b1c5f640f2..cb01727524da 100644
--- a/services/core/java/com/android/server/om/OverlayManagerSettings.java
+++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java
@@ -21,6 +21,7 @@ import static com.android.server.om.OverlayManagerService.TAG;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.om.OverlayConstraint;
import android.content.om.OverlayIdentifier;
import android.content.om.OverlayInfo;
import android.os.UserHandle;
@@ -44,6 +45,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -78,7 +80,8 @@ final class OverlayManagerSettings {
remove(overlay, userId);
final SettingsItem item = new SettingsItem(overlay, userId, targetPackageName,
targetOverlayableName, baseCodePath, OverlayInfo.STATE_UNKNOWN, isEnabled,
- isMutable, priority, overlayCategory, isFabricated);
+ isMutable, priority, overlayCategory, isFabricated,
+ Collections.emptyList() /* constraints */);
insert(item);
return item.getOverlayInfo();
}
@@ -155,6 +158,15 @@ final class OverlayManagerSettings {
return mItems.get(idx).setEnabled(enable);
}
+ boolean setConstraints(@NonNull final OverlayIdentifier overlay, final int userId,
+ @NonNull final List<OverlayConstraint> constraints) throws BadKeyException {
+ final int idx = select(overlay, userId);
+ if (idx < 0) {
+ throw new BadKeyException(overlay, userId);
+ }
+ return mItems.get(idx).setConstraints(constraints);
+ }
+
@OverlayInfo.State int getState(@NonNull final OverlayIdentifier overlay, final int userId)
throws BadKeyException {
final int idx = select(overlay, userId);
@@ -424,6 +436,8 @@ final class OverlayManagerSettings {
pw.println("mPriority..............: " + item.mPriority);
pw.println("mCategory..............: " + item.mCategory);
pw.println("mIsFabricated..........: " + item.mIsFabricated);
+ pw.println("mConstraints...........: "
+ + OverlayConstraint.constraintsToString(item.mConstraints));
pw.decreaseIndent();
pw.println("}");
@@ -480,6 +494,7 @@ final class OverlayManagerSettings {
static final class Serializer {
private static final String TAG_OVERLAYS = "overlays";
private static final String TAG_ITEM = "item";
+ private static final String TAG_CONSTRAINT = "constraint";
private static final String ATTR_BASE_CODE_PATH = "baseCodePath";
private static final String ATTR_IS_ENABLED = "isEnabled";
@@ -495,8 +510,11 @@ final class OverlayManagerSettings {
private static final String ATTR_VERSION = "version";
private static final String ATTR_IS_FABRICATED = "fabricated";
+ private static final String ATTR_CONSTRAINT_TYPE = "type";
+ private static final String ATTR_CONSTRAINT_VALUE = "value";
+
@VisibleForTesting
- static final int CURRENT_VERSION = 4;
+ static final int CURRENT_VERSION = 5;
public static void restore(@NonNull final ArrayList<SettingsItem> table,
@NonNull final InputStream is) throws IOException, XmlPullParserException {
@@ -526,7 +544,7 @@ final class OverlayManagerSettings {
// and overwritten.
throw new XmlPullParserException("old version " + oldVersion + "; ignoring");
case 3:
- // Upgrading from version 3 to 4 is not a breaking change so do not ignore the
+ // Upgrading from version 3 to 5 is not a breaking change so do not ignore the
// overlay file.
return;
default:
@@ -553,12 +571,23 @@ final class OverlayManagerSettings {
final boolean isFabricated = parser.getAttributeBoolean(null, ATTR_IS_FABRICATED,
false);
+ final List<OverlayConstraint> constraints = new ArrayList<>();
+ while (XmlUtils.nextElementWithin(parser, depth)) {
+ if (TAG_CONSTRAINT.equals(parser.getName())) {
+ final OverlayConstraint constraint = new OverlayConstraint(
+ parser.getAttributeInt(null, ATTR_CONSTRAINT_TYPE),
+ parser.getAttributeInt(null, ATTR_CONSTRAINT_VALUE));
+ constraints.add(constraint);
+ }
+ }
+
return new SettingsItem(overlay, userId, targetPackageName, targetOverlayableName,
- baseCodePath, state, isEnabled, !isStatic, priority, category, isFabricated);
+ baseCodePath, state, isEnabled, !isStatic, priority, category, isFabricated,
+ constraints);
}
public static void persist(@NonNull final ArrayList<SettingsItem> table,
- @NonNull final OutputStream os) throws IOException, XmlPullParserException {
+ @NonNull final OutputStream os) throws IOException {
final TypedXmlSerializer xml = Xml.resolveSerializer(os);
xml.startDocument(null, true);
xml.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
@@ -590,6 +619,14 @@ final class OverlayManagerSettings {
xml.attributeInt(null, ATTR_PRIORITY, item.mPriority);
XmlUtils.writeStringAttribute(xml, ATTR_CATEGORY, item.mCategory);
XmlUtils.writeBooleanAttribute(xml, ATTR_IS_FABRICATED, item.mIsFabricated);
+
+ for (OverlayConstraint constraint : item.mConstraints) {
+ xml.startTag(null, TAG_CONSTRAINT);
+ xml.attributeInt(null, ATTR_CONSTRAINT_TYPE, constraint.getType());
+ xml.attributeInt(null, ATTR_CONSTRAINT_VALUE, constraint.getValue());
+ xml.endTag(null, TAG_CONSTRAINT);
+ }
+
xml.endTag(null, TAG_ITEM);
}
}
@@ -603,17 +640,19 @@ final class OverlayManagerSettings {
private @OverlayInfo.State int mState;
private boolean mIsEnabled;
private OverlayInfo mCache;
- private boolean mIsMutable;
+ private final boolean mIsMutable;
private int mPriority;
private String mCategory;
- private boolean mIsFabricated;
+ private final boolean mIsFabricated;
+ @NonNull
+ private List<OverlayConstraint> mConstraints;
SettingsItem(@NonNull final OverlayIdentifier overlay, final int userId,
@NonNull final String targetPackageName,
@Nullable final String targetOverlayableName, @NonNull final String baseCodePath,
final @OverlayInfo.State int state, final boolean isEnabled,
final boolean isMutable, final int priority, @Nullable String category,
- final boolean isFabricated) {
+ final boolean isFabricated, @NonNull final List<OverlayConstraint> constraints) {
mOverlay = overlay;
mUserId = userId;
mTargetPackageName = targetPackageName;
@@ -626,6 +665,8 @@ final class OverlayManagerSettings {
mIsMutable = isMutable;
mPriority = priority;
mIsFabricated = isFabricated;
+ Objects.requireNonNull(constraints);
+ mConstraints = constraints;
}
private String getTargetPackageName() {
@@ -692,11 +733,26 @@ final class OverlayManagerSettings {
return false;
}
+ private boolean setConstraints(@NonNull List<OverlayConstraint> constraints) {
+ Objects.requireNonNull(constraints);
+
+ if (!mIsMutable) {
+ return false;
+ }
+
+ if (!Objects.equals(mConstraints, constraints)) {
+ mConstraints = constraints;
+ invalidateCache();
+ return true;
+ }
+ return false;
+ }
+
private OverlayInfo getOverlayInfo() {
if (mCache == null) {
mCache = new OverlayInfo(mOverlay.getPackageName(), mOverlay.getOverlayName(),
mTargetPackageName, mTargetOverlayableName, mCategory, mBaseCodePath,
- mState, mUserId, mPriority, mIsMutable, mIsFabricated);
+ mState, mUserId, mPriority, mIsMutable, mIsFabricated, mConstraints);
}
return mCache;
}
diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java
index cc4c2b5bf893..068d68d25017 100644
--- a/services/core/java/com/android/server/pm/AppsFilterImpl.java
+++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java
@@ -40,6 +40,7 @@ import static com.android.server.pm.AppsFilterUtils.canQueryViaUsesLibrary;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.app.ApplicationPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.SigningDetails;
@@ -173,6 +174,10 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable,
* Report a change to observers.
*/
private void onChanged() {
+ // App visibility may have changed, which means that earlier fetches from these caches may
+ // be invalid.
+ PackageManager.invalidatePackageInfoCache();
+ ApplicationPackageManager.invalidateGetPackagesForUidCache();
dispatchChange(this);
}
diff --git a/services/core/java/com/android/server/power/ThermalManagerService.java b/services/core/java/com/android/server/power/ThermalManagerService.java
index f46fa446a0ba..5ee9b7d09fdd 100644
--- a/services/core/java/com/android/server/power/ThermalManagerService.java
+++ b/services/core/java/com/android/server/power/ThermalManagerService.java
@@ -2096,11 +2096,16 @@ public class ThermalManagerService extends SystemService {
}
if (mCachedHeadrooms.contains(forecastSeconds)) {
- // TODO(b/360486877): replace with metrics
- Slog.d(TAG,
- "Headroom forecast in " + forecastSeconds + "s served from cache: "
- + mCachedHeadrooms.get(forecastSeconds));
- return mCachedHeadrooms.get(forecastSeconds);
+ float headroom = mCachedHeadrooms.get(forecastSeconds);
+ // TODO(b/360486877): add new API status enum or a new atom field to
+ // differentiate success from reading cache or not
+ FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED,
+ Binder.getCallingUid(),
+ FrameworkStatsLog.THERMAL_HEADROOM_CALLED__API_STATUS__SUCCESS,
+ headroom, forecastSeconds);
+ Slog.d(TAG, "Headroom forecast in " + forecastSeconds + "s served from cache: "
+ + headroom);
+ return headroom;
}
float maxNormalized = Float.NaN;
@@ -2121,9 +2126,15 @@ public class ThermalManagerService extends SystemService {
if (samples.size() < MINIMUM_SAMPLE_COUNT) {
if (mSamples.size() == 1 && mCachedHeadrooms.contains(0)) {
// if only one sensor name exists, then try reading the cache
- // TODO(b/360486877): replace with metrics
- Slog.d(TAG, "Headroom forecast cached: " + mCachedHeadrooms.get(0));
- return mCachedHeadrooms.get(0);
+ // TODO(b/360486877): add new API status enum or a new atom field to
+ // differentiate success from reading cache or not
+ float headroom = mCachedHeadrooms.get(0);
+ FrameworkStatsLog.write(FrameworkStatsLog.THERMAL_HEADROOM_CALLED,
+ Binder.getCallingUid(),
+ FrameworkStatsLog.THERMAL_HEADROOM_CALLED__API_STATUS__SUCCESS,
+ headroom, 0);
+ Slog.d(TAG, "Headroom forecast in 0s served from cache: " + headroom);
+ return headroom;
}
// Don't try to forecast, just use the latest one we have
float normalized = normalizeTemperature(currentTemperature, threshold);
diff --git a/services/core/java/com/android/server/power/stats/BatteryChargeCalculator.java b/services/core/java/com/android/server/power/stats/BatteryChargeCalculator.java
index cc05630d037e..f29d5b24b929 100644
--- a/services/core/java/com/android/server/power/stats/BatteryChargeCalculator.java
+++ b/services/core/java/com/android/server/power/stats/BatteryChargeCalculator.java
@@ -35,7 +35,7 @@ public class BatteryChargeCalculator extends PowerCalculator {
@Override
public void calculate(BatteryUsageStats.Builder builder, BatteryStats batteryStats,
long rawRealtimeUs, long rawUptimeUs, BatteryUsageStatsQuery query) {
- builder.setDischargePercentage(
+ builder.addDischargePercentage(
batteryStats.getDischargeAmount(BatteryStats.STATS_SINCE_CHARGED));
int batteryCapacityMah = batteryStats.getBatteryCapacity();
@@ -45,11 +45,9 @@ public class BatteryChargeCalculator extends PowerCalculator {
batteryStats.getLowDischargeAmountSinceCharge() * batteryCapacityMah / 100.0;
final double dischargedPowerUpperBoundMah =
batteryStats.getHighDischargeAmountSinceCharge() * batteryCapacityMah / 100.0;
- builder.setDischargePercentage(
- batteryStats.getDischargeAmount(BatteryStats.STATS_SINCE_CHARGED))
- .setDischargedPowerRange(dischargedPowerLowerBoundMah,
- dischargedPowerUpperBoundMah)
- .setDischargeDurationMs(batteryStats.getBatteryRealtime(rawRealtimeUs) / 1000);
+ builder
+ .addDischargedPowerRange(dischargedPowerLowerBoundMah, dischargedPowerUpperBoundMah)
+ .addDischargeDurationMs(batteryStats.getBatteryRealtime(rawRealtimeUs) / 1000);
final long batteryTimeRemainingMs = batteryStats.computeBatteryTimeRemaining(rawRealtimeUs);
if (batteryTimeRemainingMs != -1) {
diff --git a/services/core/java/com/android/server/power/stats/processor/BasePowerStatsProcessor.java b/services/core/java/com/android/server/power/stats/processor/BasePowerStatsProcessor.java
index 1459ff55aceb..d24ea83540cb 100644
--- a/services/core/java/com/android/server/power/stats/processor/BasePowerStatsProcessor.java
+++ b/services/core/java/com/android/server/power/stats/processor/BasePowerStatsProcessor.java
@@ -24,12 +24,12 @@ import static com.android.server.power.stats.processor.AggregatedPowerStatsConfi
import android.os.BatteryConsumer;
import android.os.PersistableBundle;
-import android.util.SparseLongArray;
import com.android.internal.os.PowerStats;
import com.android.server.power.stats.format.BasePowerStatsLayout;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.function.DoubleSupplier;
@@ -37,9 +37,9 @@ class BasePowerStatsProcessor extends PowerStatsProcessor {
private final DoubleSupplier mBatteryCapacitySupplier;
private PowerEstimationPlan mPlan;
private long mStartTimestamp;
- private final SparseLongArray mUidStartTimestamps = new SparseLongArray();
private static final BasePowerStatsLayout sStatsLayout = new BasePowerStatsLayout();
private final PowerStats.Descriptor mPowerStatsDescriptor;
+ private final PowerStats mPowerStats;
private final long[] mTmpUidStatsArray;
private double mBatteryCapacityUah;
private int mBatteryLevel;
@@ -59,12 +59,12 @@ class BasePowerStatsProcessor extends PowerStatsProcessor {
sStatsLayout.getDeviceStatsArrayLength(), null, 0,
sStatsLayout.getUidStatsArrayLength(), extras);
mTmpUidStatsArray = new long[sStatsLayout.getUidStatsArrayLength()];
+ mPowerStats = new PowerStats(mPowerStatsDescriptor);
}
@Override
void start(PowerComponentAggregatedPowerStats stats, long timestampMs) {
mStartTimestamp = timestampMs;
- mUidStartTimestamps.clear();
stats.setPowerStatsDescriptor(mPowerStatsDescriptor);
mBatteryCapacityUah = mBatteryCapacitySupplier.getAsDouble() * 1000;
mBatteryLevel = UNSPECIFIED;
@@ -73,6 +73,9 @@ class BasePowerStatsProcessor extends PowerStatsProcessor {
mCumulativeDischargeUah = 0;
mCumulativeDischargePct = 0;
mCumulativeDischargeDurationMs = 0;
+
+ // Establish a baseline
+ stats.addProcessedPowerStats(mPowerStats, timestampMs);
}
@Override
@@ -101,32 +104,23 @@ class BasePowerStatsProcessor extends PowerStatsProcessor {
}
@Override
- public void setUidState(PowerComponentAggregatedPowerStats stats, int uid,
- @AggregatedPowerStatsConfig.TrackedState int stateId, int state, long timestampMs) {
- super.setUidState(stats, uid, stateId, state, timestampMs);
- if (stateId == STATE_PROCESS_STATE && mUidStartTimestamps.indexOfKey(uid) < 0) {
- mUidStartTimestamps.put(uid, timestampMs);
- }
- }
-
- @Override
void finish(PowerComponentAggregatedPowerStats stats, long timestampMs) {
if (mPlan == null) {
mPlan = new PowerEstimationPlan(stats.getConfig());
}
- PowerStats powerStats = new PowerStats(mPowerStatsDescriptor);
- sStatsLayout.setUsageDuration(powerStats.stats, timestampMs - mStartTimestamp);
+ sStatsLayout.setUsageDuration(mPowerStats.stats, timestampMs - mStartTimestamp);
- sStatsLayout.addBatteryDischargePercent(powerStats.stats, mCumulativeDischargePct);
+ sStatsLayout.addBatteryDischargePercent(mPowerStats.stats, mCumulativeDischargePct);
if (mCumulativeDischargeUah != 0) {
- sStatsLayout.addBatteryDischargeUah(powerStats.stats,
+ sStatsLayout.addBatteryDischargeUah(mPowerStats.stats,
mCumulativeDischargeUah);
} else {
- sStatsLayout.addBatteryDischargeUah(powerStats.stats,
+ sStatsLayout.addBatteryDischargeUah(mPowerStats.stats,
(long) (mCumulativeDischargePct * mBatteryCapacityUah / 100.0));
}
- sStatsLayout.addBatteryDischargeDuration(powerStats.stats, mCumulativeDischargeDurationMs);
+ sStatsLayout.addBatteryDischargeDuration(mPowerStats.stats, mCumulativeDischargeDurationMs);
+
mCumulativeDischargePct = 0;
mCumulativeDischargeUah = 0;
mCumulativeDischargeDurationMs = 0;
@@ -134,19 +128,16 @@ class BasePowerStatsProcessor extends PowerStatsProcessor {
List<Integer> uids = new ArrayList<>();
stats.collectUids(uids);
+ long durationMs = timestampMs - mStartTimestamp;
if (!uids.isEmpty()) {
for (int i = uids.size() - 1; i >= 0; i--) {
- Integer uid = uids.get(i);
- long durationMs = timestampMs - mUidStartTimestamps.get(uid, mStartTimestamp);
- mUidStartTimestamps.put(uid, timestampMs);
-
long[] uidStats = new long[sStatsLayout.getUidStatsArrayLength()];
sStatsLayout.setUidUsageDuration(uidStats, durationMs);
- powerStats.uidStats.put(uid, uidStats);
+ mPowerStats.uidStats.put(uids.get(i), uidStats);
}
}
- stats.addPowerStats(powerStats, timestampMs);
+ stats.addPowerStats(mPowerStats, timestampMs);
for (int i = mPlan.uidStateEstimates.size() - 1; i >= 0; i--) {
UidStateEstimate uidStateEstimate = mPlan.uidStateEstimates.get(i);
@@ -169,5 +160,7 @@ class BasePowerStatsProcessor extends PowerStatsProcessor {
}
mStartTimestamp = timestampMs;
+ Arrays.fill(mPowerStats.stats, 0);
+ mPowerStats.uidStats.clear();
}
}
diff --git a/services/core/java/com/android/server/power/stats/processor/PowerStatsAggregator.java b/services/core/java/com/android/server/power/stats/processor/PowerStatsAggregator.java
index 1b864bbe479c..8461a5442bd0 100644
--- a/services/core/java/com/android/server/power/stats/processor/PowerStatsAggregator.java
+++ b/services/core/java/com/android/server/power/stats/processor/PowerStatsAggregator.java
@@ -110,7 +110,8 @@ public class PowerStatsAggregator {
lastTime = item.time;
- if (item.batteryLevel != lastBatteryLevel) {
+ if (item.cmd == BatteryStats.HistoryItem.CMD_UPDATE
+ && item.batteryLevel != lastBatteryLevel) {
mStats.noteBatteryLevel(item.batteryLevel, item.batteryChargeUah,
item.time);
lastBatteryLevel = item.batteryLevel;
diff --git a/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java b/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java
index ef0e63bece90..177d12988a27 100644
--- a/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java
+++ b/services/core/java/com/android/server/power/stats/processor/PowerStatsExporter.java
@@ -245,13 +245,13 @@ class PowerStatsExporter {
private void populateBatteryLevelInfo(BatteryUsageStats.Builder builder,
BatteryLevelInfo batteryLevelInfo) {
- builder.setDischargePercentage((int) Math.round(batteryLevelInfo.batteryDischargePct))
- .setDischargedPowerRange(batteryLevelInfo.batteryDischargeMah,
+ builder.addDischargePercentage((int) Math.round(batteryLevelInfo.batteryDischargePct))
+ .addDischargedPowerRange(batteryLevelInfo.batteryDischargeMah,
batteryLevelInfo.batteryDischargeMah)
- .setDischargeDurationMs(batteryLevelInfo.batteryDischargeDurationMs)
+ .addDischargeDurationMs(batteryLevelInfo.batteryDischargeDurationMs)
.getAggregateBatteryConsumerBuilder(
BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
- .setConsumedPower(batteryLevelInfo.batteryDischargeMah);
+ .addConsumedPower(batteryLevelInfo.batteryDischargeMah);
}
private void populateBatteryConsumers(
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index ef63229f55e2..69b2b9b326ba 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -777,7 +777,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
mWallpaperCompatibleDisplaysForTest.remove(displayId);
}
- private void updateFallbackConnection() {
+ private void updateFallbackConnection(int clientUid) {
if (mLastWallpaper == null || mFallbackWallpaper == null) return;
final WallpaperConnection systemConnection = mLastWallpaper.connection;
final WallpaperConnection fallbackConnection = mFallbackWallpaper.connection;
@@ -793,8 +793,12 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
if (isDeviceEligibleForDesktopExperienceWallpaper(mContext)) {
- mWallpaperDisplayHelper.forEachDisplayData(displayData -> {
- int displayId = displayData.mDisplayId;
+ Display[] displays = mWallpaperDisplayHelper.getDisplays();
+ for (int i = displays.length - 1; i >= 0; i--) {
+ int displayId = displays[i].getDisplayId();
+ if (!mWallpaperDisplayHelper.isUsableDisplay(displayId, clientUid)) {
+ continue;
+ }
// If the display is already connected to the desired wallpaper(s), either the
// same wallpaper for both lock and system, or different wallpapers for each,
// any existing fallback wallpaper connection will be removed.
@@ -802,11 +806,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
&& (lockConnection == null || lockConnection.containsDisplay(displayId))) {
DisplayConnector fallbackConnector =
mFallbackWallpaper.connection.mDisplayConnector.get(displayId);
- if (fallbackConnector != null && fallbackConnector.mEngine != null) {
- fallbackConnector.disconnectLocked(mFallbackWallpaper.connection);
+ if (fallbackConnector != null) {
+ if (fallbackConnector.mEngine != null) {
+ fallbackConnector.disconnectLocked(mFallbackWallpaper.connection);
+ }
mFallbackWallpaper.connection.mDisplayConnector.remove(displayId);
}
- return;
+ continue;
}
// Identify if the fallback wallpaper should be use for lock or system or both.
@@ -844,7 +850,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
mFallbackWallpaper);
}
}
- });
+ }
} else if (isWallpaperCompatibleForDisplay(DEFAULT_DISPLAY, systemConnection)) {
if (fallbackConnection.mDisplayConnector.size() != 0) {
fallbackConnection.forEachDisplayConnector(connector -> {
@@ -3787,7 +3793,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
wallpaper.connection = newConn;
newConn.mReply = reply;
updateCurrentWallpapers(wallpaper);
- updateFallbackConnection();
+ updateFallbackConnection(componentUid);
} catch (RemoteException e) {
String msg = "Remote exception for " + componentName + "\n" + e;
if (fromUser) {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 5cb39d44586f..1dd7c4d4adbd 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -6635,6 +6635,22 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
.getKeyguardController().isKeyguardLocked(mDisplayId);
}
+ boolean isKeyguardLockedOrAodShowing() {
+ return isKeyguardLocked() || isAodShowing();
+ }
+
+ /**
+ * @return whether aod is showing for this display
+ */
+ boolean isAodShowing() {
+ final boolean isAodShowing = mRootWindowContainer.mTaskSupervisor
+ .getKeyguardController().isAodShowing(mDisplayId);
+ if (mDisplayId == DEFAULT_DISPLAY && isAodShowing) {
+ return !isKeyguardGoingAway();
+ }
+ return isAodShowing;
+ }
+
/**
* @return whether keyguard is going away on this display
*/
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 6091b8334438..dd2f49e171a8 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -18,6 +18,7 @@ package com.android.server.wm;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION;
@@ -216,6 +217,9 @@ class KeyguardController {
} else if (keyguardShowing && !state.mKeyguardShowing) {
transition.addFlag(TRANSIT_FLAG_KEYGUARD_APPEARING);
}
+ if (mWindowManager.mFlags.mAodTransition && aodShowing && !state.mAodShowing) {
+ transition.addFlag(TRANSIT_FLAG_AOD_APPEARING);
+ }
}
}
// Update the task snapshot if the screen will not be turned off. To make sure that the
@@ -238,19 +242,27 @@ class KeyguardController {
state.mAodShowing = aodShowing;
state.writeEventLog("setKeyguardShown");
- if (keyguardChanged) {
- // Irrelevant to AOD.
- state.mKeyguardGoingAway = false;
- if (keyguardShowing) {
- state.mDismissalRequested = false;
+ if (keyguardChanged || aodChanged) {
+ if (keyguardChanged) {
+ // Irrelevant to AOD.
+ state.mKeyguardGoingAway = false;
+ if (keyguardShowing) {
+ state.mDismissalRequested = false;
+ }
}
if (goingAwayRemoved
- || (keyguardShowing && !Display.isOffState(dc.getDisplayInfo().state))) {
+ || (keyguardShowing && !Display.isOffState(dc.getDisplayInfo().state))
+ || (mWindowManager.mFlags.mAodTransition && aodShowing)) {
// Keyguard decided to show or stopped going away. Send a transition to animate back
// to the locked state before holding the sleep token again
if (!ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS) {
dc.requestTransitionAndLegacyPrepare(
TRANSIT_TO_FRONT, TRANSIT_FLAG_KEYGUARD_APPEARING);
+ if (mWindowManager.mFlags.mAodTransition && aodShowing
+ && dc.mTransitionController.isCollecting()) {
+ dc.mTransitionController.getCollectingTransition().addFlag(
+ TRANSIT_FLAG_AOD_APPEARING);
+ }
}
dc.mWallpaperController.adjustWallpaperWindows();
dc.executeAppTransition();
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 5217a759c6ae..fe653e454d6c 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -36,6 +36,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
import static android.view.WindowManager.TRANSIT_OPEN;
@@ -973,6 +974,10 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
return false;
}
+ boolean isInAodAppearTransition() {
+ return (mFlags & TRANSIT_FLAG_AOD_APPEARING) != 0;
+ }
+
/**
* Specifies configuration change explicitly for the window container, so it can be chosen as
* transition target. This is usually used with transition mode
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index ff9e5a2aad99..563bcb771212 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -525,6 +525,19 @@ class TransitionController {
return false;
}
+ boolean isInAodAppearTransition() {
+ if (mCollectingTransition != null && mCollectingTransition.isInAodAppearTransition()) {
+ return true;
+ }
+ for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) {
+ if (mWaitingTransitions.get(i).isInAodAppearTransition()) return true;
+ }
+ for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
+ if (mPlayingTransitions.get(i).isInAodAppearTransition()) return true;
+ }
+ return false;
+ }
+
/**
* @return A pair of the transition and restore-behind target for the given {@param container}.
* @param container An ancestor of a transient-launch activity
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index c1ef208d1d4d..70948e1264c4 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -166,6 +166,14 @@ class WallpaperController {
mFindResults.setWallpaperTarget(w);
return false;
}
+ } else if (mService.mFlags.mAodTransition
+ && mDisplayContent.isKeyguardLockedOrAodShowing()) {
+ if (mService.mPolicy.isKeyguardHostWindow(w.mAttrs)
+ && w.mTransitionController.isInAodAppearTransition()) {
+ if (DEBUG_WALLPAPER) Slog.v(TAG, "Found aod transition wallpaper target: " + w);
+ mFindResults.setWallpaperTarget(w);
+ return true;
+ }
}
final boolean animationWallpaper = animatingContainer != null
@@ -684,7 +692,8 @@ class WallpaperController {
private WallpaperWindowToken getTokenForTarget(WindowState target) {
if (target == null) return null;
WindowState window = mFindResults.getTopWallpaper(
- target.canShowWhenLocked() && mService.isKeyguardLocked());
+ (target.canShowWhenLocked() && mService.isKeyguardLocked())
+ || (mService.mFlags.mAodTransition && mDisplayContent.isAodShowing()));
return window == null ? null : window.mToken.asWallpaperToken();
}
@@ -727,7 +736,9 @@ class WallpaperController {
if (mFindResults.wallpaperTarget == null && mFindResults.useTopWallpaperAsTarget) {
mFindResults.setWallpaperTarget(
- mFindResults.getTopWallpaper(mDisplayContent.isKeyguardLocked()));
+ mFindResults.getTopWallpaper(mService.mFlags.mAodTransition
+ ? mDisplayContent.isKeyguardLockedOrAodShowing()
+ : mDisplayContent.isKeyguardLocked()));
}
}
@@ -899,11 +910,17 @@ class WallpaperController {
if (mDisplayContent.mWmService.mFlags.mEnsureWallpaperInTransitions) {
visibleRequested = mWallpaperTarget != null && mWallpaperTarget.isVisibleRequested();
}
- updateWallpaperTokens(visibleRequested, mDisplayContent.isKeyguardLocked());
+ updateWallpaperTokens(visibleRequested,
+ mService.mFlags.mAodTransition
+ ? mDisplayContent.isKeyguardLockedOrAodShowing()
+ : mDisplayContent.isKeyguardLocked());
ProtoLog.v(WM_DEBUG_WALLPAPER,
"Wallpaper at display %d - visibility: %b, keyguardLocked: %b",
- mDisplayContent.getDisplayId(), visible, mDisplayContent.isKeyguardLocked());
+ mDisplayContent.getDisplayId(), visible,
+ mService.mFlags.mAodTransition
+ ? mDisplayContent.isKeyguardLockedOrAodShowing()
+ : mDisplayContent.isKeyguardLocked());
if (visible && mLastFrozen != mFindResults.isWallpaperTargetForLetterbox) {
mLastFrozen = mFindResults.isWallpaperTargetForLetterbox;
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 04ddb3cacf27..d5626661725e 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -732,8 +732,14 @@ public class WindowManagerService extends IWindowManager.Stub
new WallpaperVisibilityListeners();
IDisplayChangeWindowController mDisplayChangeController = null;
- private final DeathRecipient mDisplayChangeControllerDeath =
- () -> mDisplayChangeController = null;
+ private final DeathRecipient mDisplayChangeControllerDeath = new DeathRecipient() {
+ @Override
+ public void binderDied() {
+ synchronized (mGlobalLock) {
+ mDisplayChangeController = null;
+ }
+ }
+ };
final DisplayWindowListenerController mDisplayNotificationController;
final TaskSystemBarsListenerController mTaskSystemBarsListenerController;
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
index 66aaa562b873..a01df8bf108d 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
@@ -37,6 +37,7 @@ import android.content.pm.PackageManagerInternal;
import android.content.pm.Signature;
import android.content.pm.SigningDetails;
import android.content.pm.UserInfo;
+import android.app.PropertyInvalidatedCache;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
@@ -50,6 +51,8 @@ import android.util.SparseArray;
import androidx.annotation.NonNull;
+import android.app.ApplicationPackageManager;
+import android.content.pm.PackageManager;
import com.android.internal.pm.parsing.pkg.PackageImpl;
import com.android.internal.pm.parsing.pkg.ParsedPackage;
import com.android.internal.pm.pkg.component.ParsedActivity;
@@ -64,8 +67,10 @@ import com.android.internal.pm.pkg.component.ParsedUsesPermissionImpl;
import com.android.internal.pm.pkg.parsing.ParsingPackage;
import com.android.server.om.OverlayReferenceMapper;
import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.utils.Watchable;
import com.android.server.utils.WatchableTester;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -244,6 +249,55 @@ public class AppsFilterImplTest {
(Answer<Boolean>) invocation ->
((AndroidPackage) invocation.getArgument(SYSTEM_USER)).getTargetSdkVersion()
>= Build.VERSION_CODES.R);
+ PropertyInvalidatedCache.setTestMode(true);
+ PackageManager.sApplicationInfoCache.testPropertyName();
+ ApplicationPackageManager.sGetPackagesForUidCache.testPropertyName();
+ }
+
+ @After
+ public void tearDown() {
+ PropertyInvalidatedCache.setTestMode(false);
+ }
+
+ /**
+ * A class to make it easier to verify that PM caches are properly invalidated by
+ * AppsFilterImpl operations. This extends WatchableTester to test the cache nonces along
+ * with change reporting.
+ */
+ private static class NonceTester extends WatchableTester {
+ // The nonces from caches under consideration. The no-parameter constructor fetches the
+ // values from the cacches.
+ private static record Nonces(long applicationInfo, long packageInfo) {
+ Nonces() {
+ this(ApplicationPackageManager.sGetPackagesForUidCache.getNonce(),
+ PackageManager.sApplicationInfoCache.getNonce());
+ }
+ }
+
+ // Track the latest cache nonces.
+ private Nonces mNonces;
+
+ NonceTester(Watchable w, String k) {
+ super(w, k);
+ mNonces = new Nonces();
+ }
+
+ @Override
+ public void verifyChangeReported(String msg) {
+ super.verifyChangeReported(msg);
+ Nonces update = new Nonces();
+ assertTrue(msg, update.applicationInfo != mNonces.applicationInfo);
+ assertTrue(msg, update.packageInfo != mNonces.packageInfo);
+ mNonces = update;
+ }
+
+ @Override
+ public void verifyNoChangeReported(String msg) {
+ super.verifyNoChangeReported(msg);
+ Nonces update = new Nonces();
+ assertTrue(msg, update.applicationInfo == mNonces.applicationInfo);
+ assertTrue(msg, update.packageInfo == mNonces.packageInfo);
+ }
}
@Test
@@ -1167,7 +1221,7 @@ public class AppsFilterImplTest {
final AppsFilterImpl appsFilter =
new AppsFilterImpl(mFeatureConfigMock, new String[]{}, /* systemAppsQueryable */
false, /* overlayProvider */ null, mMockHandler);
- final WatchableTester watcher = new WatchableTester(appsFilter, "onChange");
+ final WatchableTester watcher = new NonceTester(appsFilter, "onChange");
watcher.register();
simulateAddBasicAndroid(appsFilter);
watcher.verifyChangeReported("addBasicAndroid");
diff --git a/services/tests/mockingservicestests/AndroidManifest.xml b/services/tests/mockingservicestests/AndroidManifest.xml
index aa3930ac7c07..b509b0f9fd92 100644
--- a/services/tests/mockingservicestests/AndroidManifest.xml
+++ b/services/tests/mockingservicestests/AndroidManifest.xml
@@ -52,6 +52,16 @@
<uses-library android:name="android.test.runner" />
<activity
android:name="android.service.games.GameSessionTrampolineActivityTest$TestActivity" />
+ <service android:name="com.android.server.wallpaper.TestWallpaperService"
+ android:label="Test Wallpaper Service"
+ android:exported="true"
+ android:permission="android.permission.BIND_WALLPAPER">
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService"/>
+ </intent-filter>
+ <meta-data android:name="android.service.wallpaper"
+ android:resource="@xml/test_wallpaper"/>
+ </service>
</application>
<instrumentation
diff --git a/services/tests/mockingservicestests/res/xml/test_wallpaper.xml b/services/tests/mockingservicestests/res/xml/test_wallpaper.xml
new file mode 100644
index 000000000000..4eed477337b5
--- /dev/null
+++ b/services/tests/mockingservicestests/res/xml/test_wallpaper.xml
@@ -0,0 +1,19 @@
+<?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.
+ -->
+<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
+ android:label="Test Wallpaper"
+ android:supportsMultipleDisplays="true" />
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java b/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java
index d203de537b81..fa5847560782 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/CachedAppOptimizerTest.java
@@ -142,6 +142,9 @@ public final class CachedAppOptimizerTest {
}, mProcessDependencies);
LocalServices.removeServiceForTest(PackageManagerInternal.class);
LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
+
+ mCachedAppOptimizerUnderTest.init();
+ mCachedAppOptimizerUnderTest.mCompactStatsManager.reinit();
}
@After
@@ -168,7 +171,6 @@ public final class CachedAppOptimizerTest {
@Test
public void init_setsDefaults() {
- mCachedAppOptimizerUnderTest.init();
synchronized (mCachedAppOptimizerUnderTest.mPhenotypeFlagLock) {
assertThat(mCachedAppOptimizerUnderTest.useCompaction()).isEqualTo(
CachedAppOptimizer.DEFAULT_USE_COMPACTION);
@@ -304,7 +306,6 @@ public final class CachedAppOptimizerTest {
assertThat(mCachedAppOptimizerUnderTest.useCompaction()).isEqualTo(
CachedAppOptimizer.DEFAULT_USE_COMPACTION);
// When we call init and change some the flag value...
- mCachedAppOptimizerUnderTest.init();
mCountDown = new CountDownLatch(1);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
CachedAppOptimizer.KEY_USE_COMPACTION, "true", false);
@@ -331,7 +332,6 @@ public final class CachedAppOptimizerTest {
// The freezer DeviceConfig property is read at boot only
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER_NATIVE_BOOT,
CachedAppOptimizer.KEY_USE_FREEZER, "true", false);
- mCachedAppOptimizerUnderTest.init();
assertThat(mCachedAppOptimizerUnderTest.useFreezer()).isTrue();
mCountDown = new CountDownLatch(1);
@@ -363,7 +363,6 @@ public final class CachedAppOptimizerTest {
public void useCompaction_listensToDeviceConfigChangesBadValues() throws InterruptedException {
assertThat(mCachedAppOptimizerUnderTest.useCompaction()).isEqualTo(
CachedAppOptimizer.DEFAULT_USE_COMPACTION);
- mCachedAppOptimizerUnderTest.init();
// When we push an invalid flag value...
mCountDown = new CountDownLatch(1);
@@ -392,8 +391,6 @@ public final class CachedAppOptimizerTest {
@Test
public void compactThrottle_listensToDeviceConfigChanges() throws InterruptedException {
- mCachedAppOptimizerUnderTest.init();
-
// When we override new reasonable throttle values after init...
mCountDown = new CountDownLatch(8);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -440,8 +437,6 @@ public final class CachedAppOptimizerTest {
@Test
public void compactThrottle_listensToDeviceConfigChangesBadValues()
throws InterruptedException {
- mCachedAppOptimizerUnderTest.init();
-
// When one of the throttles is overridden with a bad value...
mCountDown = new CountDownLatch(1);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -526,8 +521,6 @@ public final class CachedAppOptimizerTest {
@Test
public void statsdSampleRate_listensToDeviceConfigChanges() throws InterruptedException {
- mCachedAppOptimizerUnderTest.init();
-
// When we override mCompactStatsdSampleRate with a reasonable value ...
mCountDown = new CountDownLatch(1);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -554,8 +547,6 @@ public final class CachedAppOptimizerTest {
@Test
public void statsdSampleRate_listensToDeviceConfigChangesBadValues()
throws InterruptedException {
- mCachedAppOptimizerUnderTest.init();
-
// When we override mCompactStatsdSampleRate with an unreasonable value ...
mCountDown = new CountDownLatch(1);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -580,8 +571,6 @@ public final class CachedAppOptimizerTest {
@Test
public void statsdSampleRate_listensToDeviceConfigChangesOutOfRangeValues()
throws InterruptedException {
- mCachedAppOptimizerUnderTest.init();
-
// When we override mCompactStatsdSampleRate with an value outside of [0..1]...
mCountDown = new CountDownLatch(1);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -624,8 +613,6 @@ public final class CachedAppOptimizerTest {
@Test
public void fullCompactionRssThrottleKb_listensToDeviceConfigChanges()
throws InterruptedException {
- mCachedAppOptimizerUnderTest.init();
-
// When we override mStatsdSampleRate with a reasonable value ...
mCountDown = new CountDownLatch(1);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -641,8 +628,6 @@ public final class CachedAppOptimizerTest {
@Test
public void fullCompactionRssThrottleKb_listensToDeviceConfigChangesBadValues()
throws InterruptedException {
- mCachedAppOptimizerUnderTest.init();
-
// When we override mStatsdSampleRate with an unreasonable value ...
mCountDown = new CountDownLatch(1);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -666,8 +651,6 @@ public final class CachedAppOptimizerTest {
@Test
public void fullCompactionDeltaRssThrottleKb_listensToDeviceConfigChanges()
throws InterruptedException {
- mCachedAppOptimizerUnderTest.init();
-
// When we override mStatsdSampleRate with a reasonable value ...
mCountDown = new CountDownLatch(1);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -684,8 +667,6 @@ public final class CachedAppOptimizerTest {
@Test
public void fullCompactionDeltaRssThrottleKb_listensToDeviceConfigChangesBadValues()
throws InterruptedException {
- mCachedAppOptimizerUnderTest.init();
-
// When we override mStatsdSampleRate with an unreasonable value ...
mCountDown = new CountDownLatch(1);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -709,7 +690,6 @@ public final class CachedAppOptimizerTest {
@Test
public void procStateThrottle_listensToDeviceConfigChanges()
throws InterruptedException {
- mCachedAppOptimizerUnderTest.init();
mCountDown = new CountDownLatch(1);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
CachedAppOptimizer.KEY_COMPACT_PROC_STATE_THROTTLE, "1,2,3", false);
@@ -726,7 +706,6 @@ public final class CachedAppOptimizerTest {
@Test
public void procStateThrottle_listensToDeviceConfigChangesBadValues()
throws InterruptedException {
- mCachedAppOptimizerUnderTest.init();
Set<Integer> expected = new HashSet<>();
for (String s : TextUtils.split(
@@ -774,7 +753,6 @@ public final class CachedAppOptimizerTest {
public void processWithDeltaRSSTooSmall_notFullCompacted() throws Exception {
// Initialize CachedAppOptimizer and set flags to (1) enable compaction, (2) set RSS
// throttle to 12000.
- mCachedAppOptimizerUnderTest.init();
setFlag(CachedAppOptimizer.KEY_USE_COMPACTION, "true", true);
setFlag(CachedAppOptimizer.KEY_COMPACT_FULL_DELTA_RSS_THROTTLE_KB, "12000", false);
initActivityManagerService();
@@ -810,9 +788,10 @@ public final class CachedAppOptimizerTest {
false);
waitForHandler();
// THEN process IS compacted.
- assertThat(mCachedAppOptimizerUnderTest.mLastCompactionStats.get(pid)).isNotNull();
- valuesAfter = mCachedAppOptimizerUnderTest.mLastCompactionStats.get(
- pid).getRssAfterCompaction();
+ assertThat(mCachedAppOptimizerUnderTest.mCompactStatsManager.getLastCompactionStats(pid))
+ .isNotNull();
+ valuesAfter = mCachedAppOptimizerUnderTest.mCompactStatsManager.getLastCompactionStats(pid)
+ .getRssAfterCompaction();
assertThat(valuesAfter).isEqualTo(rssAfter1);
// WHEN delta is below threshold (500).
@@ -828,9 +807,10 @@ public final class CachedAppOptimizerTest {
waitForHandler();
// THEN process IS NOT compacted - values after compaction for process 1 should remain the
// same as from the last compaction.
- assertThat(mCachedAppOptimizerUnderTest.mLastCompactionStats.get(pid)).isNotNull();
- valuesAfter = mCachedAppOptimizerUnderTest.mLastCompactionStats.get(
- pid).getRssAfterCompaction();
+ assertThat(mCachedAppOptimizerUnderTest.mCompactStatsManager.
+ getLastCompactionStats(pid)).isNotNull();
+ valuesAfter = mCachedAppOptimizerUnderTest.mCompactStatsManager.
+ getLastCompactionStats(pid).getRssAfterCompaction();
assertThat(valuesAfter).isEqualTo(rssAfter1);
// WHEN delta is above threshold (13000).
@@ -845,9 +825,10 @@ public final class CachedAppOptimizerTest {
false);
waitForHandler();
// THEN process IS compacted - values after compaction for process 1 should be updated.
- assertThat(mCachedAppOptimizerUnderTest.mLastCompactionStats.get(pid)).isNotNull();
- valuesAfter = mCachedAppOptimizerUnderTest.mLastCompactionStats.get(
- pid).getRssAfterCompaction();
+ assertThat(mCachedAppOptimizerUnderTest.
+ mCompactStatsManager.getLastCompactionStats(pid)).isNotNull();
+ valuesAfter = mCachedAppOptimizerUnderTest.
+ mCompactStatsManager.getLastCompactionStats(pid).getRssAfterCompaction();
assertThat(valuesAfter).isEqualTo(rssAfter3);
}
@@ -856,7 +837,7 @@ public final class CachedAppOptimizerTest {
public void processWithAnonRSSTooSmall_notFullCompacted() throws Exception {
// Initialize CachedAppOptimizer and set flags to (1) enable compaction, (2) set RSS
// throttle to 8000.
- mCachedAppOptimizerUnderTest.init();
+
setFlag(CachedAppOptimizer.KEY_USE_COMPACTION, "true", true);
setFlag(CachedAppOptimizer.KEY_COMPACT_FULL_RSS_THROTTLE_KB, "8000", false);
initActivityManagerService();
@@ -888,7 +869,8 @@ public final class CachedAppOptimizerTest {
false);
waitForHandler();
// THEN process IS NOT compacted.
- assertThat(mCachedAppOptimizerUnderTest.mLastCompactionStats.get(pid)).isNull();
+ assertThat(mCachedAppOptimizerUnderTest.
+ mCompactStatsManager.getLastCompactionStats(pid)).isNull();
// GIVEN we simulate RSS memory before above threshold.
mProcessDependencies.setRss(rssAboveThreshold);
@@ -899,9 +881,10 @@ public final class CachedAppOptimizerTest {
false);
waitForHandler();
// THEN process IS compacted.
- assertThat(mCachedAppOptimizerUnderTest.mLastCompactionStats.get(pid)).isNotNull();
- long[] valuesAfter = mCachedAppOptimizerUnderTest.mLastCompactionStats.get(
- pid).getRssAfterCompaction();
+ assertThat(mCachedAppOptimizerUnderTest.
+ mCompactStatsManager.getLastCompactionStats(pid)).isNotNull();
+ long[] valuesAfter = mCachedAppOptimizerUnderTest.mCompactStatsManager.
+ getLastCompactionStats(pid).getRssAfterCompaction();
assertThat(valuesAfter).isEqualTo(rssAboveThresholdAfter);
}
@@ -910,7 +893,6 @@ public final class CachedAppOptimizerTest {
public void processWithOomAdjTooSmall_notFullCompacted() throws Exception {
// Initialize CachedAppOptimizer and set flags to (1) enable compaction, (2) set Min and
// Max OOM_Adj throttles.
- mCachedAppOptimizerUnderTest.init();
setFlag(CachedAppOptimizer.KEY_USE_COMPACTION, "true", true);
setFlag(CachedAppOptimizer.KEY_COMPACT_THROTTLE_MIN_OOM_ADJ, Long.toString(920), true);
setFlag(CachedAppOptimizer.KEY_COMPACT_THROTTLE_MAX_OOM_ADJ, Long.toString(950), true);
@@ -934,9 +916,10 @@ public final class CachedAppOptimizerTest {
mCachedAppOptimizerUnderTest.onProcessFrozen(processRecord);
waitForHandler();
// THEN process IS compacted.
- assertThat(mCachedAppOptimizerUnderTest.mLastCompactionStats.get(pid)).isNotNull();
- long[] valuesAfter = mCachedAppOptimizerUnderTest.mLastCompactionStats
- .get(pid)
+ assertThat(mCachedAppOptimizerUnderTest.mCompactStatsManager
+ .getLastCompactionStats(pid)).isNotNull();
+ long[] valuesAfter = mCachedAppOptimizerUnderTest.mCompactStatsManager
+ .getLastCompactionStats(pid)
.getRssAfterCompaction();
assertThat(valuesAfter).isEqualTo(rssAfter);
}
@@ -944,7 +927,7 @@ public final class CachedAppOptimizerTest {
@SuppressWarnings("GuardedBy")
@Test
public void process_forceCompacted() throws Exception {
- mCachedAppOptimizerUnderTest.init();
+
setFlag(CachedAppOptimizer.KEY_USE_COMPACTION, "true", true);
setFlag(CachedAppOptimizer.KEY_COMPACT_THROTTLE_MIN_OOM_ADJ, Long.toString(920), true);
setFlag(CachedAppOptimizer.KEY_COMPACT_THROTTLE_MAX_OOM_ADJ, Long.toString(950), true);
@@ -970,7 +953,8 @@ public final class CachedAppOptimizerTest {
false);
waitForHandler();
// the process is not compacted
- assertThat(mCachedAppOptimizerUnderTest.mLastCompactionStats.get(pid)).isNull();
+ assertThat(mCachedAppOptimizerUnderTest.mCompactStatsManager.
+ getLastCompactionStats(pid)).isNull();
// Compact process some
mCachedAppOptimizerUnderTest.compactApp(processRecord,
@@ -978,7 +962,8 @@ public final class CachedAppOptimizerTest {
false);
waitForHandler();
// the process is not compacted
- assertThat(mCachedAppOptimizerUnderTest.mLastCompactionStats.get(pid)).isNull();
+ assertThat(mCachedAppOptimizerUnderTest.mCompactStatsManager
+ .getLastCompactionStats(pid)).isNull();
processRecord.mState.setSetAdj(100);
processRecord.mState.setCurAdj(100);
@@ -989,9 +974,10 @@ public final class CachedAppOptimizerTest {
true);
waitForHandler();
// then process is compacted.
- assertThat(mCachedAppOptimizerUnderTest.mLastCompactionStats.get(pid)).isNotNull();
+ assertThat(mCachedAppOptimizerUnderTest
+ .mCompactStatsManager.getLastCompactionStats(pid)).isNotNull();
- mCachedAppOptimizerUnderTest.mLastCompactionStats.clear();
+ mCachedAppOptimizerUnderTest.mCompactStatsManager.getLastCompactionStats().clear();
if (CachedAppOptimizer.ENABLE_SHARED_AND_CODE_COMPACT) {
// We force a some compaction
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/TestWallpaperService.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/TestWallpaperService.java
new file mode 100644
index 000000000000..85ea5a0f2c2e
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/TestWallpaperService.java
@@ -0,0 +1,26 @@
+/*
+ * 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.wallpaper;
+
+import android.service.wallpaper.WallpaperService;
+
+public final class TestWallpaperService extends WallpaperService {
+ @Override
+ public Engine onCreateEngine() {
+ return new Engine();
+ }
+}
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 a5073599b29e..bc04fd94c719 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -136,6 +136,10 @@ public class WallpaperManagerServiceTests {
private static final String TAG = "WallpaperManagerServiceTests";
private static final int DISPLAY_SIZE_DIMENSION = 100;
+
+ private static final ComponentName TEST_WALLPAPER_COMPONENT = ComponentName.createRelative(
+ "com.android.frameworks.mockingservicestests",
+ "com.android.server.wallpaper.TestWallpaperService");
private static StaticMockitoSession sMockitoSession;
@ClassRule
@@ -144,6 +148,7 @@ public class WallpaperManagerServiceTests {
private static ComponentName sImageWallpaperComponentName;
private static ComponentName sDefaultWallpaperComponent;
+ private static WallpaperDescription sDefaultWallpaperDescription;
private static ComponentName sFallbackWallpaperComponentName;
@@ -210,8 +215,11 @@ public class WallpaperManagerServiceTests {
} else {
sContext.addMockService(sDefaultWallpaperComponent, sWallpaperService);
}
+ sDefaultWallpaperDescription = new WallpaperDescription.Builder().setComponent(
+ sDefaultWallpaperComponent).build();
sContext.addMockService(sImageWallpaperComponentName, sWallpaperService);
+ sContext.addMockService(TEST_WALLPAPER_COMPONENT, sWallpaperService);
if (sFallbackWallpaperComponentName != null) {
sContext.addMockService(sFallbackWallpaperComponentName, sWallpaperService);
}
@@ -484,11 +492,12 @@ public class WallpaperManagerServiceTests {
}
@Test
- @EnableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT)
+ @EnableFlags({Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT,
+ Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING})
public void testSaveLoadSettings_withoutWallpaperDescription()
throws IOException, XmlPullParserException {
WallpaperData expectedData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0);
- expectedData.setComponent(sDefaultWallpaperComponent);
+ expectedData.setDescription(sDefaultWallpaperDescription);
expectedData.primaryColors = new WallpaperColors(Color.valueOf(Color.RED),
Color.valueOf(Color.BLUE), null);
expectedData.mWallpaperDimAmount = 0.5f;
@@ -524,11 +533,12 @@ public class WallpaperManagerServiceTests {
}
@Test
- @EnableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT)
+ @EnableFlags({Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT,
+ Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING})
public void testSaveLoadSettings_withWallpaperDescription()
throws IOException, XmlPullParserException {
WallpaperData expectedData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0);
- expectedData.setComponent(sDefaultWallpaperComponent);
+ expectedData.setDescription(sDefaultWallpaperDescription);
PersistableBundle content = new PersistableBundle();
content.putString("ckey", "cvalue");
WallpaperDescription description = new WallpaperDescription.Builder()
@@ -556,7 +566,8 @@ public class WallpaperManagerServiceTests {
}
@Test
- @DisableFlags(Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT)
+ @DisableFlags({Flags.FLAG_REMOVE_NEXT_WALLPAPER_COMPONENT,
+ Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING})
public void testSaveLoadSettings_legacyNextComponent()
throws IOException, XmlPullParserException {
WallpaperData systemWallpaperData = mService.getCurrentWallpaperData(FLAG_SYSTEM, 0);
@@ -1033,35 +1044,33 @@ public class WallpaperManagerServiceTests {
}
// Verify a secondary display removes system decorations ended
- // Test setWallpaperComponent on multiple displays.
- // GIVEN 3 displays, 0, 2, 3, the new wallpaper is only compatible for display 0 and 3 but not
- // 2.
- // WHEN the new wallpaper is set for system and lock via setWallpaperComponent.
+ // Test fallback connection is correctly established for multiple displays after reboot.
+ // GIVEN 3 displays, 0, 2, 3, the wallpaper is only compatible for display 0 and 3 but not 2.
+ // WHEN the device is booted.
// THEN there are 2 connections in mLastWallpaper and 1 connection in mFallbackWallpaper.
@Test
@EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
- public void setWallpaperComponent_multiDisplays_shouldHaveExpectedConnections() {
- // Skip if there is no pre-defined default wallpaper component.
- assumeThat(sDefaultWallpaperComponent,
- not(CoreMatchers.equalTo(sImageWallpaperComponentName)));
-
- final int testUserId = USER_SYSTEM;
- mService.switchUser(testUserId, null);
+ public void deviceBooted_multiDisplays_shouldHaveExpectedConnections() {
final int incompatibleDisplayId = 2;
final int compatibleDisplayId = 3;
setUpDisplays(List.of(DEFAULT_DISPLAY, incompatibleDisplayId, compatibleDisplayId));
mService.removeWallpaperCompatibleDisplayForTest(incompatibleDisplayId);
- mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
- FLAG_SYSTEM | FLAG_LOCK, testUserId);
+ final int testUserId = USER_SYSTEM;
+ // After reboot, a switch user triggers the wallpapers initialization.
+ mService.switchUser(testUserId, null);
verifyLastWallpaperData(testUserId, sImageWallpaperComponentName);
verifyCurrentSystemData(testUserId);
- assertThat(mService.mLastWallpaper.connection.getConnectedEngineSize()).isEqualTo(2);
assertThat(mService.mLastWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isTrue();
assertThat(mService.mLastWallpaper.connection.containsDisplay(compatibleDisplayId))
.isTrue();
- assertThat(mService.mFallbackWallpaper.connection.getConnectedEngineSize()).isEqualTo(1);
+ assertThat(mService.mLastWallpaper.connection.containsDisplay(incompatibleDisplayId))
+ .isFalse();
+ assertThat(mService.mFallbackWallpaper.connection.containsDisplay(DEFAULT_DISPLAY))
+ .isFalse();
+ assertThat(mService.mFallbackWallpaper.connection.containsDisplay(compatibleDisplayId))
+ .isFalse();
assertThat(mService.mFallbackWallpaper.connection.containsDisplay(incompatibleDisplayId))
.isTrue();
assertThat(mService.mLastLockWallpaper).isNull();
@@ -1076,30 +1085,31 @@ public class WallpaperManagerServiceTests {
@Test
@EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
public void setWallpaperComponent_multiDisplays_displayBecomeCompatible_shouldHaveExpectedConnections() {
- // Skip if there is no pre-defined default wallpaper component.
- assumeThat(sDefaultWallpaperComponent,
- not(CoreMatchers.equalTo(sImageWallpaperComponentName)));
-
- final int testUserId = USER_SYSTEM;
- mService.switchUser(testUserId, null);
final int display2 = 2;
final int display3 = 3;
setUpDisplays(List.of(DEFAULT_DISPLAY, display2, display3));
mService.removeWallpaperCompatibleDisplayForTest(display2);
- mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
+ final int testUserId = USER_SYSTEM;
+ mService.switchUser(testUserId, null);
+ // Switch to a test wallpaper and then image wallpaper later to simulate a wallpaper change.
+ mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(),
FLAG_SYSTEM | FLAG_LOCK, testUserId);
-
mService.addWallpaperCompatibleDisplayForTest(display2);
+
mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
FLAG_SYSTEM | FLAG_LOCK, testUserId);
verifyLastWallpaperData(testUserId, sImageWallpaperComponentName);
verifyCurrentSystemData(testUserId);
- assertThat(mService.mLastWallpaper.connection.getConnectedEngineSize()).isEqualTo(3);
assertThat(mService.mLastWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isTrue();
assertThat(mService.mLastWallpaper.connection.containsDisplay(display2)).isTrue();
assertThat(mService.mLastWallpaper.connection.containsDisplay(display3)).isTrue();
- assertThat(mService.mFallbackWallpaper.connection.getConnectedEngineSize()).isEqualTo(0);
+ assertThat(
+ mService.mFallbackWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isFalse();
+ assertThat(
+ mService.mFallbackWallpaper.connection.containsDisplay(display2)).isFalse();
+ assertThat(
+ mService.mFallbackWallpaper.connection.containsDisplay(display3)).isFalse();
assertThat(mService.mLastLockWallpaper).isNull();
}
@@ -1112,28 +1122,27 @@ public class WallpaperManagerServiceTests {
@Test
@EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
public void setWallpaperComponent_multiDisplays_displayBecomeIncompatible_shouldHaveExpectedConnections() {
- // Skip if there is no pre-defined default wallpaper component.
- assumeThat(sDefaultWallpaperComponent,
- not(CoreMatchers.equalTo(sImageWallpaperComponentName)));
-
- final int testUserId = USER_SYSTEM;
- mService.switchUser(testUserId, null);
final int display2 = 2;
final int display3 = 3;
setUpDisplays(List.of(DEFAULT_DISPLAY, display2, display3));
mService.removeWallpaperCompatibleDisplayForTest(display2);
- mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
+ final int testUserId = USER_SYSTEM;
+ mService.switchUser(testUserId, null);
+ // Switch to a test wallpaper and then image wallpaper later to simulate a wallpaper change.
+ mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(),
FLAG_SYSTEM | FLAG_LOCK, testUserId);
-
mService.removeWallpaperCompatibleDisplayForTest(display3);
+
mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
FLAG_SYSTEM | FLAG_LOCK, testUserId);
verifyLastWallpaperData(testUserId, sImageWallpaperComponentName);
verifyCurrentSystemData(testUserId);
- assertThat(mService.mLastWallpaper.connection.getConnectedEngineSize()).isEqualTo(1);
assertThat(mService.mLastWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isTrue();
- assertThat(mService.mFallbackWallpaper.connection.getConnectedEngineSize()).isEqualTo(2);
+ assertThat(mService.mLastWallpaper.connection.containsDisplay(display2)).isFalse();
+ assertThat(mService.mLastWallpaper.connection.containsDisplay(display3)).isFalse();
+ assertThat(
+ mService.mFallbackWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isFalse();
assertThat(mService.mFallbackWallpaper.connection.containsDisplay(display2)).isTrue();
assertThat(mService.mFallbackWallpaper.connection.containsDisplay(display3)).isTrue();
assertThat(mService.mLastLockWallpaper).isNull();
@@ -1148,35 +1157,40 @@ public class WallpaperManagerServiceTests {
@Test
@EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WALLPAPER)
public void setWallpaperComponent_systemAndLockWallpapers_multiDisplays_shouldHaveExpectedConnections() {
- // Skip if there is no pre-defined default wallpaper component.
- assumeThat(sDefaultWallpaperComponent,
- not(CoreMatchers.equalTo(sImageWallpaperComponentName)));
-
- final int testUserId = USER_SYSTEM;
- mService.switchUser(testUserId, null);
final int incompatibleDisplayId = 2;
final int compatibleDisplayId = 3;
setUpDisplays(List.of(DEFAULT_DISPLAY, incompatibleDisplayId, compatibleDisplayId));
+ final int testUserId = USER_SYSTEM;
+ mService.switchUser(testUserId, null);
+ // Switch to a test wallpaper and then image wallpaper later to simulate a wallpaper change.
+ mService.setWallpaperComponent(TEST_WALLPAPER_COMPONENT, sContext.getOpPackageName(),
+ FLAG_SYSTEM | FLAG_LOCK, testUserId);
mService.removeWallpaperCompatibleDisplayForTest(incompatibleDisplayId);
mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
FLAG_SYSTEM, testUserId);
- mService.setWallpaperComponent(sImageWallpaperComponentName, sContext.getOpPackageName(),
- FLAG_LOCK, testUserId);
verifyLastWallpaperData(testUserId, sImageWallpaperComponentName);
- verifyLastLockWallpaperData(testUserId, sImageWallpaperComponentName);
+ verifyLastLockWallpaperData(testUserId, TEST_WALLPAPER_COMPONENT);
verifyCurrentSystemData(testUserId);
- assertThat(mService.mLastWallpaper.connection.getConnectedEngineSize()).isEqualTo(2);
+
assertThat(mService.mLastWallpaper.connection.containsDisplay(DEFAULT_DISPLAY)).isTrue();
assertThat(mService.mLastWallpaper.connection.containsDisplay(compatibleDisplayId))
.isTrue();
- assertThat(mService.mLastLockWallpaper.connection.getConnectedEngineSize()).isEqualTo(2);
+ assertThat(mService.mLastWallpaper.connection.containsDisplay(incompatibleDisplayId))
+ .isFalse();
+ // mLastLockWallpaper is TEST_WALLPAPER_COMPONENT, which declares external displays support
+ // in the wallpaper metadata.
assertThat(mService.mLastLockWallpaper.connection.containsDisplay(DEFAULT_DISPLAY))
.isTrue();
assertThat(mService.mLastLockWallpaper.connection.containsDisplay(compatibleDisplayId))
.isTrue();
- assertThat(mService.mFallbackWallpaper.connection.getConnectedEngineSize()).isEqualTo(1);
+ assertThat(mService.mLastLockWallpaper.connection.containsDisplay(incompatibleDisplayId))
+ .isTrue();
+ assertThat(mService.mFallbackWallpaper.connection.containsDisplay(DEFAULT_DISPLAY))
+ .isFalse();
+ assertThat(mService.mFallbackWallpaper.connection.containsDisplay(compatibleDisplayId))
+ .isFalse();
assertThat(mService.mFallbackWallpaper.connection.containsDisplay(incompatibleDisplayId))
.isTrue();
}
@@ -1281,4 +1295,6 @@ public class WallpaperManagerServiceTests {
assertEquals(pfdContents, fileContents);
}
}
+
+
}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
index 9da89fcf2e84..0da7184c3541 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsAtomTest.java
@@ -458,9 +458,9 @@ public class BatteryUsageStatsAtomTest {
/* includeScreenStateData */ false,
/* includePowerStateData */ false,
/* minConsumedPowerThreshold */ 0)
- .setDischargePercentage(20)
- .setDischargedPowerRange(1000, 2000)
- .setDischargeDurationMs(1234)
+ .addDischargePercentage(20)
+ .addDischargedPowerRange(1000, 2000)
+ .addDischargeDurationMs(1234)
.setStatsStartTimestamp(1000)
.setStatsEndTimestamp(20000)
.setStatsDuration(10000);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index 31ff50f8ca58..93fe8d330d5b 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -580,7 +580,7 @@ public class BatteryUsageStatsProviderTest {
accumulateBatteryUsageStats(batteryStats, 10000000, 0);
// Accumulate every 200 bytes of battery history
accumulateBatteryUsageStats(batteryStats, 200, 2);
- accumulateBatteryUsageStats(batteryStats, 50, 4);
+ accumulateBatteryUsageStats(batteryStats, 50, 5);
// Accumulate on every invocation of accumulateBatteryUsageStats
accumulateBatteryUsageStats(batteryStats, 0, 7);
}
@@ -617,6 +617,9 @@ public class BatteryUsageStatsProviderTest {
assertThat(stats.getStatsStartTimestamp()).isEqualTo(5 * MINUTE_IN_MS);
assertThat(stats.getStatsEndTimestamp()).isEqualTo(115 * MINUTE_IN_MS);
+ assertThat(stats.getAggregateBatteryConsumer(
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+ .getConsumedPower()).isEqualTo(1200); // 1_200_000 uAh converted to mAh
assertBatteryConsumer(stats, 360.0, 60 * MINUTE_IN_MS);
assertBatteryConsumer(stats, APP_UID, 360.0, 60 * MINUTE_IN_MS);
@@ -655,6 +658,9 @@ public class BatteryUsageStatsProviderTest {
setTime(10 * MINUTE_IN_MS);
synchronized (batteryStats) {
+ batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+ /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0,
+ 10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
batteryStats.noteFlashlightOnLocked(APP_UID,
10 * MINUTE_IN_MS, 10 * MINUTE_IN_MS);
}
@@ -663,6 +669,9 @@ public class BatteryUsageStatsProviderTest {
setTime(20 * MINUTE_IN_MS);
synchronized (batteryStats) {
+ batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+ /* plugType */ 0, 85, 72, 3700, 3_000_000, 4_000_000, 0,
+ 20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
batteryStats.noteFlashlightOffLocked(APP_UID,
20 * MINUTE_IN_MS, 20 * MINUTE_IN_MS);
}
@@ -682,6 +691,9 @@ public class BatteryUsageStatsProviderTest {
setTime(50 * MINUTE_IN_MS);
synchronized (batteryStats) {
+ batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+ /* plugType */ 0, 80, 72, 3700, 2_400_000, 4_000_000, 0,
+ 50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS);
batteryStats.noteFlashlightOffLocked(APP_UID,
50 * MINUTE_IN_MS, 50 * MINUTE_IN_MS);
}
@@ -719,6 +731,10 @@ public class BatteryUsageStatsProviderTest {
assertThat(stats.getChargeTimeRemainingMs()).isEqualTo(777);
assertThat(stats.getBatteryCapacity()).isEqualTo(4000); // from PowerProfile
+ assertThat(stats.getAggregateBatteryConsumer(
+ BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
+ .getConsumedPower()).isEqualTo(1200); // 3_600_000-2_400_000 uAh converted to mAh
+
// Total: 10 + 20 + 30 = 60
assertBatteryConsumer(stats, 360.0, 60 * MINUTE_IN_MS);
assertBatteryConsumer(stats, APP_UID, 360.0, 60 * MINUTE_IN_MS);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
index dd50431b598e..097a60ed52c5 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
@@ -327,9 +327,9 @@ public class BatteryUsageStatsTest {
new BatteryUsageStats.Builder(new String[]{"FOO"}, true,
includeScreenState, includePowerState, 0)
.setBatteryCapacity(4000)
- .setDischargePercentage(20)
- .setDischargedPowerRange(1000, 2000)
- .setDischargeDurationMs(1234)
+ .addDischargePercentage(20)
+ .addDischargedPowerRange(1000, 2000)
+ .addDischargeDurationMs(1234)
.setStatsStartTimestamp(1000)
.setStatsEndTimestamp(3000);
@@ -371,8 +371,8 @@ public class BatteryUsageStatsTest {
final BatteryUsageStats.Builder builder =
new BatteryUsageStats.Builder(customPowerComponentNames,
includeProcessStateData, true, true, 0);
- builder.setDischargePercentage(30)
- .setDischargedPowerRange(1234, 2345)
+ builder.addDischargePercentage(30)
+ .addDischargedPowerRange(1234, 2345)
.setStatsStartTimestamp(2000)
.setStatsEndTimestamp(5000);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java
index f0334598bd30..7b8824cb0e3d 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickTypePanelTest.java
@@ -21,6 +21,7 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
+import android.graphics.drawable.GradientDrawable;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
@@ -28,6 +29,8 @@ import android.view.View;
import android.view.WindowManager;
import android.widget.LinearLayout;
+import androidx.annotation.NonNull;
+
import com.android.internal.R;
import org.junit.Before;
@@ -87,6 +90,11 @@ public class AutoclickTypePanelTest {
}
@Test
+ public void AutoclickTypePanel_initialState_correctButtonStyle() {
+ verifyButtonHasSelectedStyle(mLeftClickButton);
+ }
+
+ @Test
public void togglePanelExpansion_onClick_expandedTrue() {
// On clicking left click button, the panel is expanded and all buttons are visible.
mLeftClickButton.callOnClick();
@@ -116,4 +124,21 @@ public class AutoclickTypePanelTest {
assertThat(mDoubleClickButton.getVisibility()).isEqualTo(View.GONE);
assertThat(mDragButton.getVisibility()).isEqualTo(View.GONE);
}
+
+ @Test
+ public void togglePanelExpansion_selectButton_correctStyle() {
+ // By first click, the panel is expanded.
+ mLeftClickButton.callOnClick();
+
+ // Clicks any button in the expanded state to select a type button.
+ mScrollButton.callOnClick();
+
+ verifyButtonHasSelectedStyle(mScrollButton);
+ }
+
+ private void verifyButtonHasSelectedStyle(@NonNull LinearLayout button) {
+ GradientDrawable gradientDrawable = (GradientDrawable) button.getBackground();
+ assertThat(gradientDrawable.getColor().getDefaultColor())
+ .isEqualTo(mTestableContext.getColor(R.color.materialColorPrimary));
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 1627f683cd3e..06958b81d846 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -25,7 +25,6 @@ import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE;
import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL;
-import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.testing.DexmakerShareClassLoaderRule.runWithDexmakerShareClassLoader;
@@ -116,6 +115,7 @@ import com.android.server.pm.UserManagerInternal;
import com.android.server.pm.UserManagerService;
import com.android.server.pm.UserTypeDetails;
import com.android.server.pm.UserTypeFactory;
+import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.WindowManagerService;
import com.google.common.collect.Range;
@@ -1563,11 +1563,11 @@ public class UserControllerTest {
// and the thread is still alive
assertTrue(threadStartUser.isAlive());
- // mock the binder response for the user switch completion
- ArgumentCaptor<Bundle> captor = ArgumentCaptor.forClass(Bundle.class);
- verify(mInjector.mWindowManagerMock).lockNow(captor.capture());
- IRemoteCallback.Stub.asInterface(captor.getValue().getBinder(
- LOCK_ON_USER_SWITCH_CALLBACK)).sendResult(null);
+ // mock send the keyguard shown event
+ ArgumentCaptor<ActivityTaskManagerInternal.ScreenObserver> captor = ArgumentCaptor.forClass(
+ ActivityTaskManagerInternal.ScreenObserver.class);
+ verify(mInjector.mActivityTaskManagerInternal).registerScreenObserver(captor.capture());
+ captor.getValue().onKeyguardStateChanged(true);
// verify the switch now moves on...
Thread.sleep(1000);
@@ -1757,6 +1757,7 @@ public class UserControllerTest {
private final IStorageManager mStorageManagerMock;
private final UserManagerInternal mUserManagerInternalMock;
private final WindowManagerService mWindowManagerMock;
+ private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
private final PowerManagerInternal mPowerManagerInternal;
private final AlarmManagerInternal mAlarmManagerInternal;
private final KeyguardManager mKeyguardManagerMock;
@@ -1778,6 +1779,7 @@ public class UserControllerTest {
mUserManagerMock = mock(UserManagerService.class);
mUserManagerInternalMock = mock(UserManagerInternal.class);
mWindowManagerMock = mock(WindowManagerService.class);
+ mActivityTaskManagerInternal = mock(ActivityTaskManagerInternal.class);
mStorageManagerMock = mock(IStorageManager.class);
mPowerManagerInternal = mock(PowerManagerInternal.class);
mAlarmManagerInternal = mock(AlarmManagerInternal.class);
@@ -1841,6 +1843,11 @@ public class UserControllerTest {
}
@Override
+ ActivityTaskManagerInternal getActivityTaskManagerInternal() {
+ return mActivityTaskManagerInternal;
+ }
+
+ @Override
PowerManagerInternal getPowerManagerInternal() {
return mPowerManagerInternal;
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
index 587f4370636c..108cd67b3316 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
@@ -627,6 +627,7 @@ public class HdmiCecMessageValidatorTest {
assertMessageValidity("4F:81:13:05").isEqualTo(ERROR_PARAMETER);
assertMessageValidity("4F:86:10:14").isEqualTo(ERROR_PARAMETER);
assertMessageValidity("0F:86:10:24").isEqualTo(ERROR_PARAMETER);
+ assertMessageValidity("8F:86:FF:FF").isEqualTo(ERROR_PARAMETER);
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayConstraintsTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayConstraintsTests.java
new file mode 100644
index 000000000000..b2e296a36b93
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayConstraintsTests.java
@@ -0,0 +1,195 @@
+/*
+ * 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.om;
+
+import static android.content.Context.DEVICE_ID_DEFAULT;
+import static android.content.om.OverlayConstraint.TYPE_DEVICE_ID;
+import static android.content.om.OverlayConstraint.TYPE_DISPLAY_ID;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.om.FabricatedOverlay;
+import android.content.om.OverlayConstraint;
+import android.content.om.OverlayIdentifier;
+import android.content.om.OverlayInfo;
+import android.content.om.OverlayManager;
+import android.content.om.OverlayManagerTransaction;
+import android.content.res.Flags;
+import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.TypedValue;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(JUnitParamsRunner.class)
+public class OverlayConstraintsTests {
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ private OverlayManager mOverlayManager;
+ private UserHandle mUserHandle;
+ private OverlayIdentifier mOverlayIdentifier = null;
+
+ @Before
+ public void setUp() throws Exception {
+ mOverlayManager = getApplicationContext().getSystemService(OverlayManager.class);
+ mUserHandle = UserHandle.of(UserHandle.myUserId());
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mOverlayIdentifier != null) {
+ OverlayManagerTransaction transaction =
+ new OverlayManagerTransaction.Builder()
+ .unregisterFabricatedOverlay(mOverlayIdentifier)
+ .build();
+ mOverlayManager.commit(transaction);
+ mOverlayIdentifier = null;
+ }
+ }
+
+ @Test
+ public void createOverlayConstraint_withInvalidType_fails() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new OverlayConstraint(500 /* type */, 1 /* value */));
+ }
+
+ @Test
+ public void createOverlayConstraint_withInvalidValue_fails() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new OverlayConstraint(TYPE_DEVICE_ID, -1 /* value */));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_RRO_CONSTRAINTS)
+ public void enableOverlayWithNullConstraints_fails() {
+ FabricatedOverlay fabricatedOverlay = createFabricatedOverlay();
+ assertThrows(NullPointerException.class,
+ () -> mOverlayManager.commit(new OverlayManagerTransaction.Builder()
+ .registerFabricatedOverlay(fabricatedOverlay)
+ .setEnabled(fabricatedOverlay.getIdentifier(), true /* enable */,
+ null /* constraints */)
+ .build()));
+ }
+
+ @Test
+ @Parameters(method = "getAllConstraintLists")
+ @RequiresFlagsEnabled(Flags.FLAG_RRO_CONSTRAINTS)
+ public void enableOverlayWithConstraints_writesConstraintsIntoOverlayInfo(
+ List<OverlayConstraint> constraints) throws Exception {
+ enableOverlay(constraints);
+
+ OverlayInfo overlayInfo = mOverlayManager.getOverlayInfo(mOverlayIdentifier, mUserHandle);
+ assertNotNull(overlayInfo);
+ assertEquals(constraints, overlayInfo.getConstraints());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_RRO_CONSTRAINTS)
+ public void disableOverlayWithConstraints_fails() throws Exception {
+ FabricatedOverlay fabricatedOverlay = createFabricatedOverlay();
+ assertThrows(SecurityException.class,
+ () -> mOverlayManager.commit(new OverlayManagerTransaction.Builder()
+ .registerFabricatedOverlay(fabricatedOverlay)
+ .setEnabled(fabricatedOverlay.getIdentifier(), false /* enable */,
+ List.of(new OverlayConstraint(TYPE_DISPLAY_ID, DEFAULT_DISPLAY)))
+ .build()));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_RRO_CONSTRAINTS)
+ public void enableOverlayWithNewConstraints_updatesConstraintsIntoOverlayInfo()
+ throws Exception {
+ List<OverlayConstraint> constraints1 =
+ List.of(new OverlayConstraint(TYPE_DISPLAY_ID, 1 /* value*/));
+ enableOverlay(constraints1);
+
+ OverlayInfo overlayInfo1 = mOverlayManager.getOverlayInfo(mOverlayIdentifier, mUserHandle);
+ assertNotNull(overlayInfo1);
+ assertEquals(constraints1, overlayInfo1.getConstraints());
+
+ List<OverlayConstraint> constraints2 = List.of(
+ new OverlayConstraint(TYPE_DISPLAY_ID, 2 /* value */));
+ enableOverlay(constraints2);
+
+ OverlayInfo overlayInfo2 = mOverlayManager.getOverlayInfo(mOverlayIdentifier, mUserHandle);
+ assertNotNull(overlayInfo2);
+ assertEquals(overlayInfo1.overlayName, overlayInfo2.overlayName);
+ assertEquals(overlayInfo1.targetPackageName, overlayInfo2.targetPackageName);
+ assertEquals(overlayInfo1.packageName, overlayInfo2.packageName);
+ assertEquals(constraints2, overlayInfo2.getConstraints());
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_RRO_CONSTRAINTS)
+ public void enableOverlayWithConstraints_fails() throws Exception {
+ assertThrows(SecurityException.class, () -> enableOverlay(
+ List.of(new OverlayConstraint(TYPE_DISPLAY_ID, DEFAULT_DISPLAY))));
+ }
+
+ private FabricatedOverlay createFabricatedOverlay() {
+ String packageName = getApplicationContext().getPackageName();
+ FabricatedOverlay fabricatedOverlay = new FabricatedOverlay.Builder(
+ packageName, "testOverlay" /* name */, packageName)
+ .build();
+ fabricatedOverlay.setResourceValue("string/module_2_name" /* resourceName */,
+ TypedValue.TYPE_STRING, "hello" /* value */, null /* configuration */);
+ return fabricatedOverlay;
+ }
+
+ private void enableOverlay(List<OverlayConstraint> constraints) {
+ FabricatedOverlay fabricatedOverlay = createFabricatedOverlay();
+ OverlayManagerTransaction transaction =
+ new OverlayManagerTransaction.Builder()
+ .registerFabricatedOverlay(fabricatedOverlay)
+ .setEnabled(fabricatedOverlay.getIdentifier(), true /* enable */,
+ constraints)
+ .build();
+ mOverlayManager.commit(transaction);
+ mOverlayIdentifier = fabricatedOverlay.getIdentifier();
+ }
+
+ private static List<OverlayConstraint>[] getAllConstraintLists() {
+ return new List[]{
+ Collections.emptyList(),
+ List.of(new OverlayConstraint(TYPE_DISPLAY_ID, DEFAULT_DISPLAY)),
+ List.of(new OverlayConstraint(TYPE_DEVICE_ID, DEVICE_ID_DEFAULT)),
+ List.of(new OverlayConstraint(TYPE_DEVICE_ID, DEVICE_ID_DEFAULT),
+ new OverlayConstraint(TYPE_DEVICE_ID, DEVICE_ID_DEFAULT))
+ };
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
index ec61b877a3e4..0818db1db3bc 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplRebootTests.java
@@ -30,6 +30,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Collections;
import java.util.Set;
import java.util.function.Consumer;
@@ -203,7 +204,7 @@ public class OverlayManagerServiceImplRebootTests extends OverlayManagerServiceI
// Overlay priority changing between reboots should not affect enable state of mutable
// overlays.
- impl.setEnabled(IDENTIFIER, true, USER);
+ impl.setEnabled(IDENTIFIER, true, USER, Collections.emptyList() /* constraints */);
// Reorder the overlays
configureSystemOverlay(OVERLAY, ConfigState.MUTABLE_DISABLED, 1 /* priority */);
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
index 578b888a6496..e46a806727c0 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
@@ -16,10 +16,14 @@
package com.android.server.om;
+import static android.content.Context.DEVICE_ID_DEFAULT;
+import static android.content.om.OverlayConstraint.TYPE_DEVICE_ID;
+import static android.content.om.OverlayConstraint.TYPE_DISPLAY_ID;
import static android.content.om.OverlayInfo.STATE_DISABLED;
import static android.content.om.OverlayInfo.STATE_ENABLED;
import static android.content.om.OverlayInfo.STATE_MISSING_TARGET;
import static android.os.OverlayablePolicy.CONFIG_SIGNATURE;
+import static android.view.Display.DEFAULT_DISPLAY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -27,21 +31,30 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.testng.Assert.assertThrows;
+import android.content.om.OverlayConstraint;
import android.content.om.OverlayIdentifier;
import android.content.om.OverlayInfo;
import android.content.pm.UserPackage;
+import android.content.res.Flags;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
-import androidx.test.runner.AndroidJUnit4;
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
-@RunWith(AndroidJUnit4.class)
+@RunWith(JUnitParamsRunner.class)
public class OverlayManagerServiceImplTests extends OverlayManagerServiceImplTestsBase {
private static final String OVERLAY = "com.test.overlay";
@@ -62,6 +75,9 @@ public class OverlayManagerServiceImplTests extends OverlayManagerServiceImplTes
private static final String CERT_CONFIG_OK = "config_certificate_ok";
private static final String CERT_CONFIG_NOK = "config_certificate_nok";
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Test
public void testGetOverlayInfo() throws Exception {
installAndAssert(overlay(OVERLAY, TARGET), USER,
@@ -176,7 +192,8 @@ public class OverlayManagerServiceImplTests extends OverlayManagerServiceImplTes
Set.of(UserPackage.of(USER, TARGET)));
assertState(STATE_DISABLED, IDENTIFIER, USER);
- assertEquals(impl.setEnabled(IDENTIFIER, true, USER),
+ assertEquals(impl.setEnabled(IDENTIFIER, true /* enable */, USER,
+ Collections.emptyList() /* constraints */),
Set.of(UserPackage.of(USER, TARGET)));
assertState(STATE_ENABLED, IDENTIFIER, USER);
@@ -213,22 +230,17 @@ public class OverlayManagerServiceImplTests extends OverlayManagerServiceImplTes
}
@Test
+ @RequiresFlagsDisabled(Flags.FLAG_RRO_CONSTRAINTS)
public void testSetEnabledAtVariousConditions() throws Exception {
- final OverlayManagerServiceImpl impl = getImpl();
- assertThrows(OverlayManagerServiceImpl.OperationFailedException.class,
- () -> impl.setEnabled(IDENTIFIER, true, USER));
-
- // request succeeded, and there was a change that needs to be
- // propagated to the rest of the system
- installAndAssert(target(TARGET), USER,
- Set.of(UserPackage.of(USER, TARGET)));
- installAndAssert(overlay(OVERLAY, TARGET), USER,
- Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
- assertEquals(Set.of(UserPackage.of(USER, TARGET)),
- impl.setEnabled(IDENTIFIER, true, USER));
+ testSetEnabledAtVariousConditions(Collections.emptyList());
+ }
- // request succeeded, but nothing changed
- assertEquals(Set.of(), impl.setEnabled(IDENTIFIER, true, USER));
+ @Test
+ @Parameters(method = "getConstraintLists")
+ @RequiresFlagsEnabled(Flags.FLAG_RRO_CONSTRAINTS)
+ public void testSetEnabledAtVariousConditionsWithConstraints(
+ List<OverlayConstraint> constraints) throws Exception {
+ testSetEnabledAtVariousConditions(constraints);
}
@Test
@@ -338,4 +350,33 @@ public class OverlayManagerServiceImplTests extends OverlayManagerServiceImplTes
Set.of(UserPackage.of(USER, TARGET)),
Set.of(UserPackage.of(USER, TARGET)));
}
+
+ private void testSetEnabledAtVariousConditions(final List<OverlayConstraint> constraints)
+ throws Exception {
+ final OverlayManagerServiceImpl impl = getImpl();
+ assertThrows(OverlayManagerServiceImpl.OperationFailedException.class,
+ () -> impl.setEnabled(IDENTIFIER, true /* enable */, USER, constraints));
+
+ // request succeeded, and there was a change that needs to be
+ // propagated to the rest of the system
+ installAndAssert(target(TARGET), USER,
+ Set.of(UserPackage.of(USER, TARGET)));
+ installAndAssert(overlay(OVERLAY, TARGET), USER,
+ Set.of(UserPackage.of(USER, OVERLAY), UserPackage.of(USER, TARGET)));
+ assertEquals(Set.of(UserPackage.of(USER, TARGET)),
+ impl.setEnabled(IDENTIFIER, true /* enable */, USER, constraints));
+
+ // request succeeded, but nothing changed
+ assertEquals(Set.of(), impl.setEnabled(IDENTIFIER, true /* enable */, USER, constraints));
+ }
+
+ private static List<OverlayConstraint>[] getConstraintLists() {
+ return new List[]{
+ Collections.emptyList(),
+ List.of(new OverlayConstraint(TYPE_DISPLAY_ID, DEFAULT_DISPLAY)),
+ List.of(new OverlayConstraint(TYPE_DEVICE_ID, DEVICE_ID_DEFAULT)),
+ List.of(new OverlayConstraint(TYPE_DEVICE_ID, DEVICE_ID_DEFAULT),
+ new OverlayConstraint(TYPE_DEVICE_ID, DEVICE_ID_DEFAULT))
+ };
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
index 3e82d45595d7..c69de8d2dbc4 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
@@ -32,6 +32,7 @@ import android.content.om.OverlayableInfo;
import android.content.pm.UserPackage;
import android.os.FabricatedOverlayInfo;
import android.os.FabricatedOverlayInternal;
+import android.os.OverlayConstraint;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -480,7 +481,7 @@ class OverlayManagerServiceImplTestsBase {
@Override
String createIdmap(String targetPath, String overlayPath, String overlayName,
- int policies, boolean enforce, int userId) {
+ int policies, boolean enforce, int userId, OverlayConstraint[] constraints) {
mIdmapFiles.put(overlayPath, new IdmapHeader(getCrc(targetPath),
getCrc(overlayPath), targetPath, overlayName, policies, enforce));
return overlayPath;
@@ -493,7 +494,7 @@ class OverlayManagerServiceImplTestsBase {
@Override
boolean verifyIdmap(String targetPath, String overlayPath, String overlayName, int policies,
- boolean enforce, int userId) {
+ boolean enforce, int userId, OverlayConstraint[] constraints) {
final IdmapHeader idmap = mIdmapFiles.get(overlayPath);
if (idmap == null) {
return false;
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java
index 3f7eac798ccc..ad3855f4c28f 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java
@@ -16,6 +16,8 @@
package com.android.server.om;
+import static android.content.om.OverlayConstraint.TYPE_DEVICE_ID;
+import static android.content.om.OverlayConstraint.TYPE_DISPLAY_ID;
import static android.content.om.OverlayInfo.STATE_DISABLED;
import static android.content.om.OverlayInfo.STATE_ENABLED;
@@ -26,6 +28,9 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.om.OverlayConstraint;
import android.content.om.OverlayIdentifier;
import android.content.om.OverlayInfo;
import android.text.TextUtils;
@@ -44,25 +49,32 @@ import org.xmlpull.v1.XmlPullParser;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
-import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.stream.IntStream;
import javax.annotation.Nullable;
@RunWith(AndroidJUnit4.class)
public class OverlayManagerSettingsTests {
private OverlayManagerSettings mSettings;
- private static int USER_0 = 0;
- private static int USER_1 = 1;
+ private static final int USER_0 = 0;
+ private static final int USER_1 = 1;
+
+ private static final int DISPLAY_ID = 1;
+ private static final int DEVICE_ID = 2;
+
+ private static final OverlayConstraint CONSTRAINT_0 =
+ new OverlayConstraint(TYPE_DISPLAY_ID, DISPLAY_ID);
+ private static final OverlayConstraint CONSTRAINT_1 =
+ new OverlayConstraint(TYPE_DEVICE_ID, DEVICE_ID);
- private static OverlayIdentifier OVERLAY_A = new OverlayIdentifier("com.test.overlay_a",
+ private static final OverlayIdentifier OVERLAY_A = new OverlayIdentifier("com.test.overlay_a",
null /* overlayName */);
- private static OverlayIdentifier OVERLAY_B = new OverlayIdentifier("com.test.overlay_b",
+ private static final OverlayIdentifier OVERLAY_B = new OverlayIdentifier("com.test.overlay_b",
null /* overlayName */);
- private static OverlayIdentifier OVERLAY_C = new OverlayIdentifier("com.test.overlay_c",
+ private static final OverlayIdentifier OVERLAY_C = new OverlayIdentifier("com.test.overlay_c",
null /* overlayName */);
private static final OverlayInfo OVERLAY_A_USER0 = createInfo(OVERLAY_A, USER_0);
@@ -72,6 +84,13 @@ public class OverlayManagerSettingsTests {
private static final OverlayInfo OVERLAY_A_USER1 = createInfo(OVERLAY_A, USER_1);
private static final OverlayInfo OVERLAY_B_USER1 = createInfo(OVERLAY_B, USER_1);
+ private static final OverlayInfo OVERLAY_A_USER0_WITH_CONSTRAINTS =
+ createInfo(OVERLAY_A, USER_0, List.of(CONSTRAINT_0, CONSTRAINT_1));
+ private static final OverlayInfo OVERLAY_B_USER0_WITH_CONSTRAINTS =
+ createInfo(OVERLAY_B, USER_0, List.of(CONSTRAINT_1));
+ private static final OverlayInfo OVERLAY_B_USER1_WITH_CONSTRAINTS =
+ createInfo(OVERLAY_B, USER_1, List.of(CONSTRAINT_1));
+
private static final String TARGET_PACKAGE = "com.test.target";
@Before
@@ -228,6 +247,22 @@ public class OverlayManagerSettingsTests {
mSettings.getOverlaysForTarget(OVERLAY_A_USER0.targetPackageName, USER_0));
}
+ @Test
+ public void testSetConstraints() throws Exception {
+ insertSetting(OVERLAY_A_USER0);
+ insertSetting(OVERLAY_B_USER0);
+ assertListsAreEqual(List.of(OVERLAY_A_USER0, OVERLAY_B_USER0),
+ mSettings.getOverlaysForTarget(TARGET_PACKAGE, USER_0));
+
+ assertTrue(mSettings.setConstraints(OVERLAY_A, USER_0,
+ List.of(CONSTRAINT_0, CONSTRAINT_1)));
+ assertTrue(mSettings.setConstraints(OVERLAY_B, USER_0, List.of(CONSTRAINT_1)));
+
+ assertListsAreEqual(
+ List.of(OVERLAY_A_USER0_WITH_CONSTRAINTS, OVERLAY_B_USER0_WITH_CONSTRAINTS),
+ mSettings.getOverlaysForTarget(TARGET_PACKAGE, USER_0));
+ }
+
// tests: persist and restore
@Test
@@ -291,12 +326,42 @@ public class OverlayManagerSettingsTests {
}
@Test
+ public void testPersistWithConstraints() throws Exception {
+ insertSetting(OVERLAY_A_USER0_WITH_CONSTRAINTS);
+ insertSetting(OVERLAY_B_USER1_WITH_CONSTRAINTS);
+
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ mSettings.persist(os);
+ ByteArrayInputStream xml = new ByteArrayInputStream(os.toByteArray());
+
+ assertEquals(1, countXmlTags(xml, "overlays"));
+ assertEquals(2, countXmlTags(xml, "item"));
+ assertEquals(3, countXmlTags(xml, "constraint"));
+ assertEquals(1, countXmlAttributesWhere(xml, "item", "packageName",
+ OVERLAY_A.getPackageName()));
+ assertEquals(1, countXmlAttributesWhere(xml, "item", "packageName",
+ OVERLAY_B.getPackageName()));
+ assertEquals(1, countXmlAttributesWhere(xml, "item", "userId",
+ Integer.toString(USER_0)));
+ assertEquals(1, countXmlAttributesWhere(xml, "item", "userId",
+ Integer.toString(USER_1)));
+ assertEquals(1, countXmlAttributesWhere(xml, "constraint", "type",
+ TYPE_DISPLAY_ID));
+ assertEquals(2, countXmlAttributesWhere(xml, "constraint", "type",
+ TYPE_DEVICE_ID));
+ assertEquals(1, countXmlAttributesWhere(xml, "constraint", "value",
+ DISPLAY_ID));
+ assertEquals(2, countXmlAttributesWhere(xml, "constraint", "value",
+ DEVICE_ID));
+ }
+
+ @Test
public void testRestoreEmpty() throws Exception {
final int version = OverlayManagerSettings.Serializer.CURRENT_VERSION;
final String xml =
"<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+ "<overlays version=\"" + version + "\" />\n";
- ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes("utf-8"));
+ ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes(UTF_8));
mSettings.restore(is);
assertDoesNotContain(mSettings, new OverlayIdentifier("com.test.overlay"), 0);
@@ -319,7 +384,45 @@ public class OverlayManagerSettingsTests {
+ " isStatic='false'\n"
+ " priority='0' />\n"
+ "</overlays>\n";
- ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes("utf-8"));
+ ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes(UTF_8));
+
+ mSettings.restore(is);
+ final OverlayIdentifier identifier = new OverlayIdentifier("com.test.overlay", "test");
+ OverlayInfo oi = mSettings.getOverlayInfo(identifier, 1234);
+ assertNotNull(oi);
+ assertEquals("com.test.overlay", oi.packageName);
+ assertEquals("test", oi.overlayName);
+ assertEquals("com.test.target", oi.targetPackageName);
+ assertEquals("/data/app/com.test.overlay-1/base.apk", oi.baseCodePath);
+ assertEquals(1234, oi.userId);
+ assertEquals(STATE_DISABLED, oi.state);
+ assertFalse(mSettings.getEnabled(identifier, 1234));
+ assertTrue(oi.constraints.isEmpty());
+ }
+
+ @Test
+ public void testRestoreSingleUserSingleOverlayWithConstraints() throws Exception {
+ final int version = OverlayManagerSettings.Serializer.CURRENT_VERSION;
+ final String xml =
+ "<?xml version='1.0' encoding='utf-8' standalone='yes'?>\n"
+ + "<overlays version='" + version + "'>\n"
+ + "<item packageName='com.test.overlay'\n"
+ + " overlayName='test'\n"
+ + " userId='1234'\n"
+ + " targetPackageName='com.test.target'\n"
+ + " baseCodePath='/data/app/com.test.overlay-1/base.apk'\n"
+ + " state='" + STATE_DISABLED + "'\n"
+ + " isEnabled='false'\n"
+ + " category='test-category'\n"
+ + " isStatic='false'\n"
+ + " priority='0' >\n"
+ + "<constraint type='" + TYPE_DISPLAY_ID + "'\n"
+ + " value = '" + DISPLAY_ID + "' />\n"
+ + "<constraint type='" + TYPE_DEVICE_ID + "'\n"
+ + " value = '" + DEVICE_ID + "' />\n"
+ + "</item>\n"
+ + "</overlays>\n";
+ ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes(UTF_8));
mSettings.restore(is);
final OverlayIdentifier identifier = new OverlayIdentifier("com.test.overlay", "test");
@@ -332,6 +435,7 @@ public class OverlayManagerSettingsTests {
assertEquals(1234, oi.userId);
assertEquals(STATE_DISABLED, oi.state);
assertFalse(mSettings.getEnabled(identifier, 1234));
+ assertListsAreEqual(List.of(CONSTRAINT_0, CONSTRAINT_1), oi.constraints);
}
@Test
@@ -352,6 +456,24 @@ public class OverlayManagerSettingsTests {
assertEquals(OVERLAY_B_USER1, b);
}
+ @Test
+ public void testPersistAndRestoreWithConstraints() throws Exception {
+ insertSetting(OVERLAY_A_USER0_WITH_CONSTRAINTS);
+ insertSetting(OVERLAY_B_USER1_WITH_CONSTRAINTS);
+
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ mSettings.persist(os);
+ ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
+ OverlayManagerSettings newSettings = new OverlayManagerSettings();
+ newSettings.restore(is);
+
+ OverlayInfo a = newSettings.getOverlayInfo(OVERLAY_A, USER_0);
+ assertEquals(OVERLAY_A_USER0_WITH_CONSTRAINTS, a);
+
+ OverlayInfo b = newSettings.getOverlayInfo(OVERLAY_B, USER_1);
+ assertEquals(OVERLAY_B_USER1_WITH_CONSTRAINTS, b);
+ }
+
private int countXmlTags(InputStream in, String tagToLookFor) throws Exception {
in.reset();
int count = 0;
@@ -384,11 +506,30 @@ public class OverlayManagerSettingsTests {
return count;
}
+ private int countXmlAttributesWhere(InputStream in, String tag, String attr, int value)
+ throws Exception {
+ in.reset();
+ int count = 0;
+ TypedXmlPullParser parser = Xml.resolvePullParser(in);
+ int event = parser.getEventType();
+ while (event != XmlPullParser.END_DOCUMENT) {
+ if (event == XmlPullParser.START_TAG && tag.equals(parser.getName())) {
+ int v = parser.getAttributeInt(null, attr);
+ if (value == v) {
+ count++;
+ }
+ }
+ event = parser.next();
+ }
+ return count;
+ }
+
private void insertSetting(OverlayInfo oi) throws Exception {
mSettings.init(oi.getOverlayIdentifier(), oi.userId, oi.targetPackageName, null,
oi.baseCodePath, true, false,0, oi.category, oi.isFabricated);
mSettings.setState(oi.getOverlayIdentifier(), oi.userId, oi.state);
mSettings.setEnabled(oi.getOverlayIdentifier(), oi.userId, false);
+ mSettings.setConstraints(oi.getOverlayIdentifier(), oi.userId, oi.constraints);
}
private static void assertContains(final OverlayManagerSettings settings,
@@ -417,42 +558,28 @@ public class OverlayManagerSettingsTests {
}
private static OverlayInfo createInfo(@NonNull OverlayIdentifier identifier, int userId) {
+ return createInfo(identifier, userId, Collections.emptyList());
+ }
+
+ private static OverlayInfo createInfo(@NonNull OverlayIdentifier identifier, int userId,
+ @NonNull List<OverlayConstraint> constraints) {
return new OverlayInfo(
identifier.getPackageName(),
identifier.getOverlayName(),
"com.test.target",
- null,
- "some-category",
- "/data/app/" + identifier + "/base.apk",
+ null /* targetOverlayableName */,
+ "some-category" /* category */,
+ "/data/app/" + identifier + "/base.apk" /* baseCodePath */,
STATE_DISABLED,
userId,
- 0,
- true,
- false);
- }
-
- private static void assertContains(int[] haystack, int needle) {
- List<Integer> list = IntStream.of(haystack)
- .boxed()
- .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
- if (!list.contains(needle)) {
- fail(String.format("integer array [%s] does not contain value %s",
- TextUtils.join(",", list), needle));
- }
- }
-
- private static void assertDoesNotContain(int[] haystack, int needle) {
- List<Integer> list = IntStream.of(haystack)
- .boxed()
- .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
- if (list.contains(needle)) {
- fail(String.format("integer array [%s] contains value %s",
- TextUtils.join(",", list), needle));
- }
+ 0 /* priority */,
+ true /* isMutable */,
+ false /* isFabricated */,
+ constraints);
}
- private static void assertListsAreEqual(
- @NonNull List<OverlayInfo> expected, @Nullable List<OverlayInfo> actual) {
+ private static <T> void assertListsAreEqual(
+ @NonNull List<T> expected, @Nullable List<T> actual) {
if (!expected.equals(actual)) {
fail(String.format("lists [%s] and [%s] differ",
TextUtils.join(",", expected), TextUtils.join(",", actual)));
diff --git a/telephony/java/android/telephony/data/ApnSetting.java b/telephony/java/android/telephony/data/ApnSetting.java
index 5daa29b940bf..22624e22d534 100644
--- a/telephony/java/android/telephony/data/ApnSetting.java
+++ b/telephony/java/android/telephony/data/ApnSetting.java
@@ -994,7 +994,6 @@ public class ApnSetting implements Parcelable {
*
* @return True if the PDU session for this APN should always be on and false otherwise
*/
- @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
public boolean isAlwaysOn() {
return mAlwaysOn;
}
@@ -2349,7 +2348,6 @@ public class ApnSetting implements Parcelable {
*
* @param alwaysOn the always on status to set for this APN
*/
- @FlaggedApi(Flags.FLAG_APN_SETTING_FIELD_SUPPORT_FLAG)
public @NonNull Builder setAlwaysOn(boolean alwaysOn) {
this.mAlwaysOn = alwaysOn;
return this;
diff --git a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java
index 6d818d7287b0..779676e4f979 100644
--- a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java
+++ b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java
@@ -161,8 +161,8 @@ public class BatteryUsageStatsPerfTest {
final BatteryUsageStats.Builder builder =
new BatteryUsageStats.Builder(new String[]{"FOO"}, false, false, false, 0)
.setBatteryCapacity(4000)
- .setDischargePercentage(20)
- .setDischargedPowerRange(1000, 2000)
+ .addDischargePercentage(20)
+ .addDischargedPowerRange(1000, 2000)
.setStatsStartTimestamp(1000)
.setStatsEndTimestamp(3000);
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 40f4f1ab0791..5259455cf33c 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -31,9 +31,12 @@ import android.hardware.input.InputManagerGlobal
import android.hardware.input.InputSettings
import android.hardware.input.KeyGestureEvent
import android.os.InputEventInjectionSync
+import android.os.PermissionEnforcer
import android.os.SystemClock
+import android.os.test.FakePermissionEnforcer
import android.os.test.TestLooper
import android.platform.test.annotations.Presubmit
+import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import android.provider.Settings
import android.view.View.OnKeyListener
@@ -66,11 +69,13 @@ import org.mockito.ArgumentMatchers.anyFloat
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
+import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.Mockito.`when`
import org.mockito.stubbing.OngoingStubbing
@@ -136,10 +141,19 @@ class InputManagerServiceTests {
private lateinit var testLooper: TestLooper
private lateinit var contentResolver: MockContentResolver
private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession
+ private lateinit var fakePermissionEnforcer: FakePermissionEnforcer
@Before
fun setup() {
context = spy(ContextWrapper(InstrumentationRegistry.getInstrumentation().getContext()))
+ fakePermissionEnforcer = FakePermissionEnforcer()
+ doReturn(Context.PERMISSION_ENFORCER_SERVICE).`when`(context).getSystemServiceName(
+ eq(PermissionEnforcer::class.java)
+ )
+ doReturn(fakePermissionEnforcer).`when`(context).getSystemService(
+ eq(Context.PERMISSION_ENFORCER_SERVICE)
+ )
+
contentResolver = MockContentResolver(context)
contentResolver.addProvider(Settings.AUTHORITY, FakeSettingsProvider())
whenever(context.contentResolver).thenReturn(contentResolver)
@@ -162,7 +176,7 @@ class InputManagerServiceTests {
): InputManagerService.KeyboardBacklightControllerInterface {
return kbdController
}
- })
+ }, fakePermissionEnforcer)
inputManagerGlobalSession = InputManagerGlobal.createTestSession(service)
val inputManager = InputManager(context)
whenever(context.getSystemService(InputManager::class.java)).thenReturn(inputManager)
@@ -314,6 +328,36 @@ class InputManagerServiceTests {
}
}
+ @Test
+ @EnableFlags(com.android.hardware.input.Flags.FLAG_KEY_EVENT_ACTIVITY_DETECTION)
+ fun testKeyActivenessNotifyEventsLifecycle() {
+ service.systemRunning()
+
+ fakePermissionEnforcer.grant(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY);
+
+ val inputManager = context.getSystemService(InputManager::class.java)
+
+ /* register for key event activeness */
+ var listener = mock(InputManager.KeyEventActivityListener::class.java)
+ assertEquals(true, inputManager.registerKeyEventActivityListener(listener))
+
+ /* mimic key event pressed */
+ val event = createKeycodeAEvent(createInputDevice(), KeyEvent.ACTION_DOWN)
+ service.interceptKeyBeforeQueueing(event, 0)
+
+ /* verify onKeyEventActivity callback called */
+ verify(listener, times(1)).onKeyEventActivity()
+
+ /* unregister for key event activeness */
+ assertEquals(true, inputManager.unregisterKeyEventActivityListener(listener))
+
+ /* mimic key event pressed */
+ service.interceptKeyBeforeQueueing(event, /* policyFlags */ 0)
+
+ /* verify onKeyEventActivity callback not called */
+ verifyNoMoreInteractions(listener)
+ }
+
private class AutoClosingVirtualDisplays(val displays: List<VirtualDisplay>) : AutoCloseable {
operator fun get(i: Int): VirtualDisplay = displays[i]
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index eb71189ffc46..4718fbf085f8 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -674,7 +674,8 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv
}
FeatureFlagsFilterOptions flags_filter_options;
- flags_filter_options.flags_must_be_readonly = true;
+ flags_filter_options.fail_on_unrecognized_flags = false;
+ flags_filter_options.flags_must_have_value = false;
FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options);
if (!flags_filter.Consume(context_, doc.get())) {
return 1;
diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp
index 6cc42f17c0a1..a2dc8f8ce0fd 100644
--- a/tools/aapt2/cmd/Link_test.cpp
+++ b/tools/aapt2/cmd/Link_test.cpp
@@ -1026,7 +1026,7 @@ TEST_F(LinkTest, FeatureFlagDisabled_SdkAtMostUDC) {
.SetManifestFile(app_manifest)
.AddParameter("-I", android_apk)
.AddParameter("--java", app_java)
- .AddParameter("--feature-flags", "flag=false");
+ .AddParameter("--feature-flags", "flag:ro=false");
const std::string app_apk = GetTestPath("app.apk");
BuildApk({}, app_apk, std::move(app_link_args), this, &diag);
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout1.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout1.xml
index 8b9ce134a9de..c595cdcff482 100644
--- a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout1.xml
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/layout/layout1.xml
@@ -6,12 +6,14 @@
<TextView android:id="@+id/text1"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
+ android:layout_height="wrap_content"
+ android:featureFlag="test.package.readWriteFlag"/>
<TextView android:id="@+id/disabled_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:featureFlag="test.package.falseFlag" />
<TextView android:id="@+id/text2"
+ android:text="FIND_ME"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:featureFlag="test.package.trueFlag" />
diff --git a/tools/aapt2/link/FeatureFlagsFilter.cpp b/tools/aapt2/link/FeatureFlagsFilter.cpp
index 4e7c1b4d8e54..23f78388b930 100644
--- a/tools/aapt2/link/FeatureFlagsFilter.cpp
+++ b/tools/aapt2/link/FeatureFlagsFilter.cpp
@@ -50,7 +50,7 @@ class FlagsVisitor : public xml::Visitor {
private:
bool ShouldRemove(std::unique_ptr<xml::Node>& node) {
- if (const auto* el = NodeCast<Element>(node.get())) {
+ if (auto* el = NodeCast<Element>(node.get())) {
auto* attr = el->FindAttribute(xml::kSchemaAndroid, "featureFlag");
if (attr == nullptr) {
return false;
@@ -72,9 +72,13 @@ class FlagsVisitor : public xml::Visitor {
has_error_ = true;
return false;
}
- if (options_.remove_disabled_elements) {
+ if (options_.remove_disabled_elements && it->second.read_only) {
// Remove if flag==true && attr=="!flag" (negated) OR flag==false && attr=="flag"
- return *it->second.enabled == negated;
+ bool remove = *it->second.enabled == negated;
+ if (!remove) {
+ el->RemoveAttribute(xml::kSchemaAndroid, "featureFlag");
+ }
+ return remove;
}
} else if (options_.flags_must_have_value) {
diagnostics_->Error(android::DiagMessage(node->line_number)
diff --git a/tools/aapt2/link/FeatureFlagsFilter_test.cpp b/tools/aapt2/link/FeatureFlagsFilter_test.cpp
index 2db2899e716c..744045588506 100644
--- a/tools/aapt2/link/FeatureFlagsFilter_test.cpp
+++ b/tools/aapt2/link/FeatureFlagsFilter_test.cpp
@@ -48,7 +48,7 @@ TEST(FeatureFlagsFilterTest, NoFeatureFlagAttributes) {
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
<permission android:name="FOO" />
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, false}}});
+ {{"flag", FeatureFlagProperties{true, false}}});
ASSERT_THAT(doc, NotNull());
auto root = doc->root.get();
ASSERT_THAT(root, NotNull());
@@ -60,7 +60,7 @@ TEST(FeatureFlagsFilterTest, RemoveElementWithDisabledFlag) {
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
<permission android:name="FOO" android:featureFlag="flag" />
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, false}}});
+ {{"flag", FeatureFlagProperties{true, false}}});
ASSERT_THAT(doc, NotNull());
auto root = doc->root.get();
ASSERT_THAT(root, NotNull());
@@ -73,7 +73,7 @@ TEST(FeatureFlagsFilterTest, RemoveElementWithNegatedEnabledFlag) {
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
<permission android:name="FOO" android:featureFlag="!flag" />
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, true}}});
+ {{"flag", FeatureFlagProperties{true, true}}});
ASSERT_THAT(doc, NotNull());
auto root = doc->root.get();
ASSERT_THAT(root, NotNull());
@@ -86,7 +86,7 @@ TEST(FeatureFlagsFilterTest, KeepElementWithEnabledFlag) {
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
<permission android:name="FOO" android:featureFlag="flag" />
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, true}}});
+ {{"flag", FeatureFlagProperties{true, true}}});
ASSERT_THAT(doc, NotNull());
auto root = doc->root.get();
ASSERT_THAT(root, NotNull());
@@ -102,7 +102,7 @@ TEST(FeatureFlagsFilterTest, SideBySideEnabledAndDisabled) {
<permission android:name="FOO" android:featureFlag="flag"
android:protectionLevel="dangerous" />
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, true}}});
+ {{"flag", FeatureFlagProperties{true, true}}});
ASSERT_THAT(doc, NotNull());
auto root = doc->root.get();
ASSERT_THAT(root, NotNull());
@@ -123,7 +123,7 @@ TEST(FeatureFlagsFilterTest, RemoveDeeplyNestedElement) {
</activity>
</application>
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, true}}});
+ {{"flag", FeatureFlagProperties{true, true}}});
ASSERT_THAT(doc, NotNull());
auto root = doc->root.get();
ASSERT_THAT(root, NotNull());
@@ -145,7 +145,7 @@ TEST(FeatureFlagsFilterTest, KeepDeeplyNestedElement) {
</activity>
</application>
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, true}}});
+ {{"flag", FeatureFlagProperties{true, true}}});
ASSERT_THAT(doc, NotNull());
auto root = doc->root.get();
ASSERT_THAT(root, NotNull());
@@ -162,7 +162,7 @@ TEST(FeatureFlagsFilterTest, FailOnEmptyFeatureFlagAttribute) {
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
<permission android:name="FOO" android:featureFlag=" " />
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, false}}});
+ {{"flag", FeatureFlagProperties{true, false}}});
ASSERT_THAT(doc, IsNull());
}
@@ -171,7 +171,7 @@ TEST(FeatureFlagsFilterTest, FailOnFlagWithNoGivenValue) {
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
<permission android:name="FOO" android:featureFlag="flag" />
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, std::nullopt}}});
+ {{"flag", FeatureFlagProperties{true, std::nullopt}}});
ASSERT_THAT(doc, IsNull());
}
@@ -180,7 +180,7 @@ TEST(FeatureFlagsFilterTest, FailOnUnrecognizedFlag) {
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
<permission android:name="FOO" android:featureFlag="unrecognized" />
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, true}}});
+ {{"flag", FeatureFlagProperties{true, true}}});
ASSERT_THAT(doc, IsNull());
}
@@ -190,7 +190,7 @@ TEST(FeatureFlagsFilterTest, FailOnMultipleValidationErrors) {
<permission android:name="FOO" android:featureFlag="bar" />
<permission android:name="FOO" android:featureFlag="unrecognized" />
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, std::nullopt}}});
+ {{"flag", FeatureFlagProperties{true, std::nullopt}}});
ASSERT_THAT(doc, IsNull());
}
@@ -199,7 +199,7 @@ TEST(FeatureFlagsFilterTest, OptionRemoveDisabledElementsIsFalse) {
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
<permission android:name="FOO" android:featureFlag="flag" />
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, false}}},
+ {{"flag", FeatureFlagProperties{true, false}}},
{.remove_disabled_elements = false});
ASSERT_THAT(doc, NotNull());
auto root = doc->root.get();
@@ -213,7 +213,7 @@ TEST(FeatureFlagsFilterTest, OptionFlagsMustHaveValueIsFalse) {
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
<permission android:name="FOO" android:featureFlag="flag" />
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, std::nullopt}}},
+ {{"flag", FeatureFlagProperties{true, std::nullopt}}},
{.flags_must_have_value = false});
ASSERT_THAT(doc, NotNull());
auto root = doc->root.get();
@@ -227,7 +227,7 @@ TEST(FeatureFlagsFilterTest, OptionFailOnUnrecognizedFlagsIsFalse) {
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android">
<permission android:name="FOO" android:featureFlag="unrecognized" />
</manifest>)EOF",
- {{"flag", FeatureFlagProperties{false, true}}},
+ {{"flag", FeatureFlagProperties{true, true}}},
{.fail_on_unrecognized_flags = false});
ASSERT_THAT(doc, NotNull());
auto root = doc->root.get();
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
index 629300838bbe..adf711ecfcbb 100644
--- a/tools/aapt2/link/FlaggedResources_test.cpp
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -59,6 +59,16 @@ void DumpChunksToString(LoadedApk* loaded_apk, std::string* output) {
output_stream.Flush();
}
+void DumpXmlTreeToString(LoadedApk* loaded_apk, std::string file, std::string* output) {
+ StringOutputStream output_stream(output);
+ Printer printer(&output_stream);
+
+ auto xml = loaded_apk->LoadXml(file, &noop_diag);
+ ASSERT_NE(xml, nullptr);
+ Debug::DumpXml(*xml, &printer);
+ output_stream.Flush();
+}
+
TEST_F(FlaggedResourcesTest, DisabledStringRemovedFromPool) {
auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"});
auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
@@ -148,4 +158,15 @@ TEST_F(FlaggedResourcesTest, TwoValuesSameDisabledFlagDifferentFiles) {
ASSERT_TRUE(diag.GetLog().contains("duplicate value for resource 'bool1'"));
}
+TEST_F(FlaggedResourcesTest, EnabledXmlELementAttributeRemoved) {
+ auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"});
+ auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
+
+ std::string output;
+ DumpXmlTreeToString(loaded_apk.get(), "res/layout-v22/layout1.xml", &output);
+ ASSERT_FALSE(output.contains("test.package.trueFlag"));
+ ASSERT_TRUE(output.contains("FIND_ME"));
+ ASSERT_TRUE(output.contains("test.package.readWriteFlag"));
+}
+
} // namespace aapt