summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AconfigFlags.bp7
-rw-r--r--boot/boot-image-profile-extra.txt21
-rw-r--r--core/api/current.txt1
-rw-r--r--core/java/android/app/INotificationManager.aidl1
-rw-r--r--core/java/android/app/IUiModeManager.aidl54
-rw-r--r--core/java/android/app/IUiModeManagerCallback.aidl1
-rw-r--r--core/java/android/app/KeyguardManager.java9
-rw-r--r--core/java/android/app/Notification.java28
-rw-r--r--core/java/android/app/UiModeManager.java137
-rw-r--r--core/java/android/app/contextualsearch/flags.aconfig7
-rw-r--r--core/java/android/content/pm/parsing/ApkLite.java23
-rw-r--r--core/java/android/content/pm/parsing/ApkLiteParseUtils.java8
-rw-r--r--core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java30
-rw-r--r--core/java/android/content/pm/parsing/PackageLite.java18
-rw-r--r--core/java/android/hardware/biometrics/BiometricConstants.java20
-rw-r--r--core/java/android/hardware/camera2/impl/CameraDeviceImpl.java8
-rw-r--r--core/java/android/hardware/input/input_framework.aconfig8
-rw-r--r--core/java/android/os/ChildZygoteProcess.java3
-rw-r--r--core/java/android/os/CombinedMessageQueue/MessageQueue.java62
-rw-r--r--core/java/android/os/ITradeInMode.aidl33
-rw-r--r--core/java/android/os/Looper.java8
-rw-r--r--core/java/android/os/Message.java16
-rw-r--r--core/java/android/os/PerfettoTrace.java41
-rw-r--r--core/java/android/os/PerfettoTrackEventExtra.java20
-rw-r--r--core/java/android/os/PowerManager.java11
-rw-r--r--core/java/android/os/TestLooperManager.java31
-rw-r--r--core/java/android/os/Trace.java1
-rw-r--r--core/java/android/os/UserManager.java46
-rw-r--r--core/java/android/view/ViewRootImpl.java69
-rw-r--r--core/java/android/view/inputmethod/ImeTracker.java1
-rw-r--r--core/java/android/widget/RemoteViews.java10
-rw-r--r--core/java/android/window/DesktopExperienceFlags.java12
-rw-r--r--core/java/android/window/DesktopModeFlags.java94
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig31
-rw-r--r--core/java/android/window/flags/windowing_sdk.aconfig11
-rw-r--r--core/java/com/android/internal/content/NativeLibraryHelper.java25
-rw-r--r--core/java/com/android/internal/notification/NotificationChannelGroupsHelper.java186
-rw-r--r--core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java16
-rw-r--r--core/jni/com_android_internal_content_NativeLibraryHelper.cpp24
-rw-r--r--core/proto/android/server/windowmanagerservice.proto1
-rw-r--r--core/proto/android/widget/remoteviews.proto1
-rw-r--r--core/res/res/drawable/accessibility_autoclick_button_rounded_background.xml20
-rw-r--r--core/res/res/drawable/accessibility_autoclick_left_click.xml25
-rw-r--r--core/res/res/drawable/accessibility_autoclick_pause.xml26
-rw-r--r--core/res/res/drawable/accessibility_autoclick_position.xml26
-rw-r--r--core/res/res/drawable/accessibility_autoclick_type_panel_rounded_background.xml20
-rw-r--r--core/res/res/layout/accessibility_autoclick_type_panel.xml78
-rw-r--r--core/res/res/layout/notification_2025_template_compact_heads_up_base.xml89
-rw-r--r--core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml116
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_messaging.xml3
-rw-r--r--core/res/res/values/attrs_manifest.xml4
-rw-r--r--core/res/res/values/config.xml3
-rw-r--r--core/res/res/values/dimens.xml12
-rw-r--r--core/res/res/values/public-final.xml5
-rw-r--r--core/res/res/values/strings.xml10
-rw-r--r--core/res/res/values/styles.xml16
-rw-r--r--core/res/res/values/symbols.xml21
-rw-r--r--core/tests/coretests/src/android/os/PerfettoTraceTest.java87
-rw-r--r--core/tests/coretests/src/android/view/ViewRootImplTest.java115
-rw-r--r--core/tests/coretests/src/com/android/internal/notification/NotificationChannelGroupsHelperTest.java268
-rw-r--r--core/tests/coretests/src/com/android/internal/notification/OWNERS1
-rw-r--r--core/tests/overlaytests/host/src/com/android/server/om/hosttest/InstallOverlayTests.java11
-rw-r--r--data/etc/privapp-permissions-platform.xml1
-rw-r--r--libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml9
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt4
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerTest.kt231
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt9
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt3
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleExpandedViewManager.kt2
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/TestActivity.kt (renamed from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java)18
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt2
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt2
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt4
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt2
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/common/TestSyncExecutor.kt34
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml2
-rw-r--r--libs/WindowManager/Shell/shared/Android.bp1
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DeviceConfig.kt)2
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java45
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java194
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleResizabilityChecker.kt58
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ResizabilityChecker.kt35
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt39
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.kt35
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt35
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java159
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/OWNERS3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java26
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt125
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt283
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md49
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java73
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java78
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java57
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitMultiDisplayProvider.java29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt2
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml2
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt47
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppWithExternalDisplayConnected.kt49
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/OpenAppWithExternalDisplayConnectedTest.kt27
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt2
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt2
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt103
-rw-r--r--libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/ExtendedDisplaySettingsSession.kt34
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java204
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt56
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt68
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt627
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt56
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt60
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt29
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java21
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java19
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt66
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt30
-rw-r--r--libs/androidfw/Android.bp1
-rw-r--r--libs/androidfw/LocaleDataLookup.cpp7
-rw-r--r--libs/androidfw/TypeWrappers.cpp17
-rw-r--r--libs/androidfw/include/androidfw/TypeWrappers.h13
-rw-r--r--libs/androidfw/tests/LocaleDataLookup_bench.cpp57
-rw-r--r--libs/androidfw/tests/TypeWrappers_test.cpp12
-rw-r--r--media/java/android/media/AudioManager.java17
-rw-r--r--media/java/android/media/IAudioService.aidl4
-rw-r--r--media/java/android/media/MediaCodec.java4
-rw-r--r--media/java/android/media/flags/media_better_together.aconfig10
-rw-r--r--nfc-non-updatable/flags/flags.aconfig10
-rw-r--r--nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java6
-rw-r--r--packages/CrashRecovery/framework/java/android/service/watchdog/ExplicitHealthCheckService.java359
-rw-r--r--packages/CrashRecovery/framework/java/android/service/watchdog/OWNERS3
-rw-r--r--packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java447
-rw-r--r--packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java2253
-rw-r--r--packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java861
-rw-r--r--packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java58
-rw-r--r--packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java85
-rw-r--r--packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java785
-rw-r--r--packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java255
-rw-r--r--packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java42
-rw-r--r--packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java117
-rw-r--r--packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java188
-rw-r--r--packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java66
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt6
-rw-r--r--packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt6
-rw-r--r--packages/SettingsLib/res/values/strings.xml2
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java15
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java24
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java11
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java6
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java11
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java6
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java38
-rw-r--r--packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java6
-rw-r--r--packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InfoMediaManagerTest.java25
-rw-r--r--packages/Shell/AndroidManifest.xml3
-rw-r--r--packages/SystemUI/Android.bp2
-rw-r--r--packages/SystemUI/aconfig/accessibility.aconfig10
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig7
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt118
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt2
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt14
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt3
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt38
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt26
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt66
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt48
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/ribbon/ui/composable/Ribbon.kt26
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt1
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt51
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt213
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt47
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt20
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt17
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt32
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt (renamed from packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt)60
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt63
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt35
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamClockTimeComplicationTest.java7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt58
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/dump/DumpHandlerTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt46
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java127
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt6
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt18
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModelTest.kt12
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt82
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelTest.kt18
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt28
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt113
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt39
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt39
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt37
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt56
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt42
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt27
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt34
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt46
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt17
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt35
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt36
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java6
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java7
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt7
-rw-r--r--packages/SystemUI/pods/com/android/systemui/util/settings/UserSettingsProxy.kt50
-rw-r--r--packages/SystemUI/res/drawable/notif_footer_btn_settings.xml2
-rw-r--r--packages/SystemUI/res/layout/notification_stack_scroll_layout.xml1
-rw-r--r--packages/SystemUI/res/values-sw600dp/config.xml6
-rw-r--r--packages/SystemUI/res/values/dimens.xml3
-rw-r--r--packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt26
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/BootCompleteCacheImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java50
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialPasswordViewBinder.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContent.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt442
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt433
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt143
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt275
-rw-r--r--packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt61
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt44
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/complication/DreamClockTimeComplication.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/complication/dagger/DreamClockTimeComplicationComponent.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt105
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java233
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt96
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardJankViewModel.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/resume/MediaResumeListener.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java58
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl2
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyConfig.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt59
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsContent.kt (renamed from packages/CrashRecovery/framework/java/android/service/watchdog/PackageConfig.aidl)16
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsContent.kt56
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt38
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt26
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt17
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt62
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt50
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java63
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/MediaProjectionStopDialogModel.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt155
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt114
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt45
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt107
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt42
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java18
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt30
-rw-r--r--packages/SystemUI/tests/goldens/brightnessSlider_iconAlphaChanges.json168
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java32
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java79
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplForDeviceTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt269
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt461
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt348
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt88
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/brightness/ui/compose/BrightnessSliderMotionTest.kt136
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderContentProviderTest.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderTest.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java166
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaSwitchingControllerTest.java7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt16
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt199
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt17
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt17
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelImplTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfigTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryImplTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java159
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt7
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsContent.kt (renamed from packages/CrashRecovery/framework/java/android/service/watchdog/IExplicitHealthCheckService.aidl)27
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt13
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt10
-rw-r--r--ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java35
-rw-r--r--ravenwood/texts/ravenwood-annotation-allowed-classes.txt4
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java28
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java84
-rw-r--r--services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java38
-rw-r--r--services/core/java/com/android/server/BootReceiver.java7
-rw-r--r--services/core/java/com/android/server/StorageManagerService.java1
-rw-r--r--services/core/java/com/android/server/TradeInModeService.java115
-rw-r--r--services/core/java/com/android/server/UiModeManagerService.java131
-rw-r--r--services/core/java/com/android/server/ZramMaintenance.java100
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerShellCommand.java2
-rw-r--r--services/core/java/com/android/server/am/SettingsToPropertiesMapper.java3
-rw-r--r--services/core/java/com/android/server/am/UserController.java38
-rw-r--r--services/core/java/com/android/server/appop/AppOpsService.java22
-rw-r--r--services/core/java/com/android/server/audio/AudioManagerShellCommand.java18
-rw-r--r--services/core/java/com/android/server/audio/AudioPolicyFacade.java1
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java32
-rw-r--r--services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java10
-rw-r--r--services/core/java/com/android/server/biometrics/PreAuthInfo.java1
-rw-r--r--services/core/java/com/android/server/input/KeyGestureController.java2
-rw-r--r--services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java60
-rw-r--r--services/core/java/com/android/server/notification/GroupHelper.java101
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java123
-rw-r--r--services/core/java/com/android/server/notification/PreferencesHelper.java58
-rw-r--r--services/core/java/com/android/server/om/OverlayManagerService.java156
-rw-r--r--services/core/java/com/android/server/om/OverlayManagerServiceImpl.java11
-rw-r--r--services/core/java/com/android/server/om/OverlayManagerSettings.java26
-rw-r--r--services/core/java/com/android/server/om/OverlayManagerShellCommand.java22
-rw-r--r--services/core/java/com/android/server/pm/UserManagerInternal.java18
-rw-r--r--services/core/java/com/android/server/pm/UserManagerService.java68
-rw-r--r--services/core/java/com/android/server/pm/UserRestrictionsUtils.java3
-rw-r--r--services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java5
-rw-r--r--services/core/java/com/android/server/pm/permission/PermissionManagerService.java3
-rw-r--r--services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java3
-rw-r--r--services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java2
-rw-r--r--services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java8
-rw-r--r--services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java6
-rw-r--r--services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java6
-rw-r--r--services/core/java/com/android/server/power/PowerManagerService.java17
-rw-r--r--services/core/java/com/android/server/power/feature/PowerManagerFlags.java12
-rw-r--r--services/core/java/com/android/server/power/feature/power_flags.aconfig11
-rw-r--r--services/core/java/com/android/server/power/stats/WakelockStatsFrameworkEvents.java12
-rw-r--r--services/core/java/com/android/server/slice/SlicePermissionManager.java22
-rw-r--r--services/core/java/com/android/server/vibrator/VibratorManagerService.java6
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java112
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java4
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskSupervisor.java87
-rw-r--r--services/core/java/com/android/server/wm/AppTransitionController.java12
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeHelper.java3
-rw-r--r--services/core/java/com/android/server/wm/DisplayContent.java17
-rw-r--r--services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java16
-rw-r--r--services/core/java/com/android/server/wm/RootWindowContainer.java9
-rw-r--r--services/core/java/com/android/server/wm/SurfaceAnimator.java28
-rw-r--r--services/core/java/com/android/server/wm/SurfaceFreezer.java303
-rw-r--r--services/core/java/com/android/server/wm/Task.java30
-rw-r--r--services/core/java/com/android/server/wm/TaskFragment.java28
-rw-r--r--services/core/java/com/android/server/wm/Transition.java2
-rw-r--r--services/core/java/com/android/server/wm/WindowContainer.java102
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerInternal.java13
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java31
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java34
-rw-r--r--services/permission/java/com/android/server/permission/access/permission/PermissionService.kt16
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java2
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java148
-rw-r--r--services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java41
-rw-r--r--services/tests/powerstatstests/src/com/android/server/power/stats/WakelockStatsFrameworkEventsTest.java18
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java121
-rw-r--r--services/tests/servicestests/src/com/android/server/am/UserControllerTest.java19
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java52
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTvTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java7
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java7
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java8
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java7
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java5
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTvTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java5
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java7
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionPlaybackTest.java7
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionTvTest.java7
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java7
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java5
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java6
-rw-r--r--services/tests/servicestests/src/com/android/server/zram/ZramMaintenanceTest.kt155
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java379
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java35
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java2
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java47
-rw-r--r--services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java6
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java98
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java175
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java68
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java40
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java166
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java17
-rw-r--r--services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java3
-rw-r--r--tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt6
-rw-r--r--tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt4
-rw-r--r--tests/utils/testutils/java/android/os/test/TestLooper.java170
-rwxr-xr-xtools/localedata/extract_icu_data.py9
552 files changed, 12765 insertions, 10851 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 834398e5c2c2..ac756ea1d624 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -1573,6 +1573,13 @@ java_aconfig_library {
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+java_aconfig_library {
+ name: "power_flags_lib_host",
+ aconfig_declarations: "power_flags",
+ host_supported: true,
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// Content
aconfig_declarations {
name: "android.content.flags-aconfig",
diff --git a/boot/boot-image-profile-extra.txt b/boot/boot-image-profile-extra.txt
index fd51f9cbcee1..ced0d176f174 100644
--- a/boot/boot-image-profile-extra.txt
+++ b/boot/boot-image-profile-extra.txt
@@ -45,3 +45,24 @@ HSPLandroid/os/MessageQueue$OnFileDescriptorEventListener;->*
HSPLandroid/os/MessageQueue$StackNodeType;->*
HSPLandroid/os/MessageQueue$StateNode;->*
HSPLandroid/os/MessageQueue$TimedParkStateNode;->*
+HSPLandroid/os/PerfettoTrace$Category;->*
+HSPLandroid/os/PerfettoTrace;->*
+HSPLandroid/os/PerfettoTrackEventExtra;->*
+HSPLandroid/os/PerfettoTrackEventExtra$BuilderImpl;->*
+HSPLandroid/os/PerfettoTrackEventExtra$NoOpBuilder;->*
+HSPLandroid/os/PerfettoTrackEventExtra$ArgBool;->*
+HSPLandroid/os/PerfettoTrackEventExtra$ArgInt64;->*
+HSPLandroid/os/PerfettoTrackEventExtra$ArgDouble;->*
+HSPLandroid/os/PerfettoTrackEventExtra$ArgString;->*
+HSPLandroid/os/PerfettoTrackEventExtra$CounterInt64;->*
+HSPLandroid/os/PerfettoTrackEventExtra$CounterDouble;->*
+HSPLandroid/os/PerfettoTrackEventExtra$CounterTrack;->*
+HSPLandroid/os/PerfettoTrackEventExtra$NamedTrack;->*
+HSPLandroid/os/PerfettoTrackEventExtra$Flow;->*
+HSPLandroid/os/PerfettoTrackEventExtra$Proto;->*
+HSPLandroid/os/PerfettoTrackEventExtra$FieldInt64;->*
+HSPLandroid/os/PerfettoTrackEventExtra$FieldDouble;->*
+HSPLandroid/os/PerfettoTrackEventExtra$FieldString;->*
+HSPLandroid/os/PerfettoTrackEventExtra$FieldNested;->*
+HSPLandroid/os/PerfettoTrackEventExtra$Pool;->*
+HSPLandroid/os/PerfettoTrackEventExtra$RingBuffer;->*
diff --git a/core/api/current.txt b/core/api/current.txt
index 21929658cbb9..dd606774b770 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -1207,7 +1207,6 @@ package android {
field public static final int minResizeHeight = 16843670; // 0x1010396
field public static final int minResizeWidth = 16843669; // 0x1010395
field public static final int minSdkVersion = 16843276; // 0x101020c
- field @FlaggedApi("android.sdk.major_minor_versioning_scheme") public static final int minSdkVersionFull = 16844461; // 0x10106ad
field public static final int minWidth = 16843071; // 0x101013f
field public static final int minimumHorizontalAngle = 16843901; // 0x101047d
field public static final int minimumVerticalAngle = 16843902; // 0x101047e
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 3fb08224b9db..bc01f934e701 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -270,6 +270,7 @@ interface INotificationManager
int[] getAllowedAdjustmentKeyTypes();
void setAssistantAdjustmentKeyTypeState(int type, boolean enabled);
+ String[] getAdjustmentDeniedPackages(String key);
boolean isAdjustmentSupportedForPackage(String key, String pkg);
void setAdjustmentSupportedForPackage(String key, String pkg, boolean enabled);
diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl
index 3b83024d536b..6d4ad7518546 100644
--- a/core/java/android/app/IUiModeManager.aidl
+++ b/core/java/android/app/IUiModeManager.aidl
@@ -51,7 +51,7 @@ interface IUiModeManager {
* Return the current running mode.
*/
int getCurrentModeType();
-
+
/**
* Sets the night mode.
* <p>
@@ -161,61 +161,69 @@ interface IUiModeManager {
boolean setNightModeActivated(boolean active);
/**
- * Returns custom start clock time
- */
+ * Returns custom start clock time
+ */
long getCustomNightModeStart();
/**
- * Sets custom start clock time
- */
+ * Sets custom start clock time
+ */
void setCustomNightModeStart(long time);
/**
- * Returns custom end clock time
- */
+ * Returns custom end clock time
+ */
long getCustomNightModeEnd();
/**
- * Sets custom end clock time
- */
+ * Sets custom end clock time
+ */
void setCustomNightModeEnd(long time);
/**
- * Sets projection state for the caller for the given projection type.
- */
+ * Sets projection state for the caller for the given projection type.
+ */
boolean requestProjection(in IBinder binder, int projectionType, String callingPackage);
/**
- * Releases projection state for the caller for the given projection type.
- */
+ * Releases projection state for the caller for the given projection type.
+ */
boolean releaseProjection(int projectionType, String callingPackage);
/**
- * Registers a listener for changes to projection state.
- */
+ * Registers a listener for changes to projection state.
+ */
@EnforcePermission("READ_PROJECTION_STATE")
void addOnProjectionStateChangedListener(in IOnProjectionStateChangedListener listener, int projectionType);
/**
- * Unregisters a listener for changes to projection state.
- */
+ * Unregisters a listener for changes to projection state.
+ */
@EnforcePermission("READ_PROJECTION_STATE")
void removeOnProjectionStateChangedListener(in IOnProjectionStateChangedListener listener);
/**
- * Returns packages that have currently set the given projection type.
- */
+ * Returns packages that have currently set the given projection type.
+ */
@EnforcePermission("READ_PROJECTION_STATE")
List<String> getProjectingPackages(int projectionType);
/**
- * Returns currently set projection types.
- */
+ * Returns currently set projection types.
+ */
@EnforcePermission("READ_PROJECTION_STATE")
int getActiveProjectionTypes();
/**
- * Returns the contrast for the current user
- */
+ * Returns the contrast for the current user.
+ */
float getContrast();
+
+
+ /**
+ * Returns the force invert state.
+ *
+ * @hide
+ */
+ int getForceInvertState();
}
diff --git a/core/java/android/app/IUiModeManagerCallback.aidl b/core/java/android/app/IUiModeManagerCallback.aidl
index 47c18a8bd3cb..550779d14bab 100644
--- a/core/java/android/app/IUiModeManagerCallback.aidl
+++ b/core/java/android/app/IUiModeManagerCallback.aidl
@@ -24,4 +24,5 @@ package android.app;
*/
oneway interface IUiModeManagerCallback {
void notifyContrastChanged(float contrast);
+ void notifyForceInvertStateChanged(int forceInvertState);
}
diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java
index b5ac4e78c7ad..d91838c4cc2b 100644
--- a/core/java/android/app/KeyguardManager.java
+++ b/core/java/android/app/KeyguardManager.java
@@ -205,6 +205,15 @@ public class KeyguardManager {
public static final String EXTRA_DISALLOW_BIOMETRICS_IF_POLICY_EXISTS = "check_dpm";
/**
+ * When switching to a secure user, system server will expect a callback when the UI has
+ * completed the switch.
+ *
+ * @hide
+ */
+ public static final String LOCK_ON_USER_SWITCH_CALLBACK = "onSwitchCallback";
+
+
+ /**
*
* Password lock type, see {@link #setLock}
*
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index ba4914954223..a6338fbd6b77 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -5999,8 +5999,10 @@ public class Notification implements Parcelable
setHeaderlessVerticalMargins(contentView, p, hasSecondLine);
// Update margins to leave space for the top line (but not for headerless views like
- // HUNS, which use a different layout that already accounts for that).
- if (Flags.notificationsRedesignTemplates() && !p.mHeaderless) {
+ // HUNS, which use a different layout that already accounts for that). Templates that
+ // have content that will be displayed under the small icon also use a different margin.
+ if (Flags.notificationsRedesignTemplates()
+ && !p.mHeaderless && !p.mHasContentInLeftMargin) {
int margin = getContentMarginTop(mContext,
R.dimen.notification_2025_content_margin_top);
contentView.setViewLayoutMargin(R.id.notification_main_column,
@@ -7597,11 +7599,19 @@ public class Notification implements Parcelable
}
private int getCompactHeadsUpBaseLayoutResource() {
- return R.layout.notification_template_material_compact_heads_up_base;
+ if (Flags.notificationsRedesignTemplates()) {
+ return R.layout.notification_2025_template_compact_heads_up_base;
+ } else {
+ return R.layout.notification_template_material_compact_heads_up_base;
+ }
}
private int getMessagingCompactHeadsUpLayoutResource() {
- return R.layout.notification_template_material_messaging_compact_heads_up;
+ if (Flags.notificationsRedesignTemplates()) {
+ return R.layout.notification_2025_template_compact_heads_up_messaging;
+ } else {
+ return R.layout.notification_template_material_messaging_compact_heads_up;
+ }
}
private int getExpandedBaseLayoutResource() {
@@ -9502,7 +9512,8 @@ public class Notification implements Parcelable
.text(null)
.hideLeftIcon(isOneToOne)
.hideRightIcon(hideRightIcons || isOneToOne)
- .headerTextSecondary(isHeaderless ? null : conversationTitle);
+ .headerTextSecondary(isHeaderless ? null : conversationTitle)
+ .hasContentInLeftMargin(true);
RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(
isConversationLayout
? mBuilder.getConversationLayoutResource()
@@ -14673,6 +14684,7 @@ public class Notification implements Parcelable
Icon mPromotedPicture;
boolean mCallStyleActions;
boolean mAllowTextWithProgress;
+ boolean mHasContentInLeftMargin;
int mTitleViewId;
int mTextViewId;
@Nullable CharSequence mTitle;
@@ -14698,6 +14710,7 @@ public class Notification implements Parcelable
mPromotedPicture = null;
mCallStyleActions = false;
mAllowTextWithProgress = false;
+ mHasContentInLeftMargin = false;
mTitleViewId = R.id.title;
mTextViewId = R.id.text;
mTitle = null;
@@ -14764,6 +14777,11 @@ public class Notification implements Parcelable
return this;
}
+ public StandardTemplateParams hasContentInLeftMargin(boolean hasContentInLeftMargin) {
+ mHasContentInLeftMargin = hasContentInLeftMargin;
+ return this;
+ }
+
final StandardTemplateParams hideSnoozeButton(boolean hideSnoozeButton) {
this.mHideSnoozeButton = hideSnoozeButton;
return this;
diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java
index 57549847f05d..f6c789d51aee 100644
--- a/core/java/android/app/UiModeManager.java
+++ b/core/java/android/app/UiModeManager.java
@@ -121,6 +121,24 @@ public class UiModeManager {
}
/**
+ * Listener for the force invert state. To listen for changes to
+ * the force invert state on the device, implement this interface and
+ * register it with the system by calling {@link #addForceInvertStateChangeListener}.
+ *
+ * @hide
+ */
+ public interface ForceInvertStateChangeListener {
+
+ /**
+ * Called when the force invert state changes.
+ *
+ * @param forceInvertState The force invert state in {@link #getForceInvertState}
+ * @hide
+ */
+ void onForceInvertStateChanged(@ForceInvertType int forceInvertState);
+ }
+
+ /**
* Broadcast sent when the device's UI has switched to car mode, either
* by being placed in a car dock or explicit action of the user. After
* sending the broadcast, the system will start the intent
@@ -374,6 +392,36 @@ public class UiModeManager {
@SystemApi
public static final int MODE_NIGHT_CUSTOM_TYPE_BEDTIME = 1;
+ /** @hide */
+ @IntDef(prefix = {"Force_Invert_Type_"}, value = {
+ FORCE_INVERT_TYPE_OFF,
+ FORCE_INVERT_TYPE_DARK,
+ FORCE_INVERT_TYPE_LIGHT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ForceInvertType {}
+
+ /**
+ * Constant for {@link #getForceInvertState()}: Do not force invert.
+ *
+ * @hide
+ */
+ public static final int FORCE_INVERT_TYPE_OFF = 0;
+
+ /**
+ * Constant for {@link #getForceInvertState()}: Force apps to be dark.
+ *
+ * @hide
+ */
+ public static final int FORCE_INVERT_TYPE_DARK = 1;
+
+ /**
+ * Constant for {@link #getForceInvertState()}: Force apps to be light.
+ *
+ * @hide
+ */
+ public static final int FORCE_INVERT_TYPE_LIGHT = 2;
+
private static Globals sGlobals;
/**
@@ -405,6 +453,8 @@ public class UiModeManager {
private final IUiModeManager mService;
private final Object mGlobalsLock = new Object();
+ @ForceInvertType
+ private int mForceInvertState = FORCE_INVERT_TYPE_OFF;
private float mContrast = ContrastUtils.CONTRAST_DEFAULT_VALUE;
/**
@@ -414,16 +464,63 @@ public class UiModeManager {
private final ArrayMap<ContrastChangeListener, Executor>
mContrastChangeListeners = new ArrayMap<>();
+ private final ArrayMap<ForceInvertStateChangeListener, Executor>
+ mForceInvertStateChangeListeners = new ArrayMap<>();
+
Globals(IUiModeManager service) {
mService = service;
try {
mService.addCallback(this);
mContrast = mService.getContrast();
+ mForceInvertState = mService.getForceInvertState();
} catch (RemoteException e) {
Log.e(TAG, "Setup failed: UiModeManagerService is dead", e);
}
}
+ @ForceInvertType
+ private int getForceInvertState() {
+ synchronized (mGlobalsLock) {
+ return mForceInvertState;
+ }
+ }
+
+ private void addForceInvertStateChangeListener(ForceInvertStateChangeListener listener,
+ Executor executor) {
+ synchronized (mGlobalsLock) {
+ mForceInvertStateChangeListeners.put(listener, executor);
+ }
+ }
+
+ private void removeForceInvertStateChangeListener(ForceInvertStateChangeListener listener) {
+ synchronized (mGlobalsLock) {
+ mForceInvertStateChangeListeners.remove(listener);
+ }
+ }
+
+ @Override
+ public void notifyForceInvertStateChanged(@ForceInvertType int forceInvertState) {
+ final Map<ForceInvertStateChangeListener, Executor> listeners = new ArrayMap<>();
+ synchronized (mGlobalsLock) {
+ // if value changed in the settings, update the cached value and notify listeners
+ if (mForceInvertState == forceInvertState) {
+ return;
+ }
+
+ mForceInvertState = forceInvertState;
+ listeners.putAll(mForceInvertStateChangeListeners);
+ }
+
+ listeners.forEach((listener, executor) -> {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ executor.execute(() -> listener.onForceInvertStateChanged(forceInvertState));
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ });
+ }
+
private float getContrast() {
synchronized (mGlobalsLock) {
return mContrast;
@@ -1453,4 +1550,44 @@ public class UiModeManager {
Objects.requireNonNull(listener);
sGlobals.removeContrastChangeListener(listener);
}
+
+ /**
+ * Returns the force invert state for the user.
+ *
+ * @hide
+ */
+ @ForceInvertType
+ public int getForceInvertState() {
+ return sGlobals.getForceInvertState();
+ }
+
+ /**
+ * Registers a {@link ForceInvertStateChangeListener} for the current user.
+ *
+ * @param executor The executor on which the listener should be called back.
+ * @param listener The listener.
+ *
+ * @hide
+ */
+ public void addForceInvertStateChangeListener(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull ForceInvertStateChangeListener listener) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(listener);
+ sGlobals.addForceInvertStateChangeListener(listener, executor);
+ }
+
+ /**
+ * Unregisters a {@link ForceInvertStateChangeListener} for the current user.
+ * If the listener was not registered, does nothing and returns.
+ *
+ * @param listener The listener to unregister.
+ *
+ * @hide
+ */
+ public void removeForceInvertStateChangeListener(
+ @NonNull ForceInvertStateChangeListener listener) {
+ Objects.requireNonNull(listener);
+ sGlobals.removeForceInvertStateChangeListener(listener);
+ }
}
diff --git a/core/java/android/app/contextualsearch/flags.aconfig b/core/java/android/app/contextualsearch/flags.aconfig
index c19921dcdc61..d81ec1e8b883 100644
--- a/core/java/android/app/contextualsearch/flags.aconfig
+++ b/core/java/android/app/contextualsearch/flags.aconfig
@@ -27,7 +27,10 @@ flag {
name: "contextual_search_window_layer"
namespace: "sysui_integrations"
description: "Identify live contextual search UI to exclude from contextual search screenshot."
- bug: "372510690"
+ bug: "390176823"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
flag {
@@ -35,4 +38,4 @@ flag {
namespace: "sysui_integrations"
description: "Add audio playing status to the contextual search invocation intent."
bug: "372935419"
-} \ No newline at end of file
+}
diff --git a/core/java/android/content/pm/parsing/ApkLite.java b/core/java/android/content/pm/parsing/ApkLite.java
index 1d8209da6559..b8cf70960ea3 100644
--- a/core/java/android/content/pm/parsing/ApkLite.java
+++ b/core/java/android/content/pm/parsing/ApkLite.java
@@ -18,6 +18,7 @@ package android.content.pm.parsing;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.pm.ApplicationInfo;
import android.content.pm.ArchivedPackageParcel;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -184,6 +185,11 @@ public class ApkLite {
*/
private final @Nullable ArchivedPackageParcel mArchivedPackage;
+ /**
+ * pageSizeCompat info from manifest file
+ */
+ private final int mPageSizeCompat;
+
public ApkLite(String path, String packageName, String splitName, boolean isFeatureSplit,
String configForSplit, String usesSplitName, boolean isSplitRequired, int versionCode,
int versionCodeMajor, int revisionCode, int installLocation,
@@ -200,7 +206,8 @@ public class ApkLite {
List<String> usesStaticLibraries, long[] usesStaticLibrariesVersionsMajor,
String[][] usesStaticLibrariesCertDigests,
boolean updatableSystem,
- String emergencyInstaller, List<SharedLibraryInfo> declaredLibraries) {
+ String emergencyInstaller, List<SharedLibraryInfo> declaredLibraries,
+ int pageSizeCompat) {
mPath = path;
mPackageName = packageName;
mSplitName = splitName;
@@ -245,6 +252,7 @@ public class ApkLite {
mEmergencyInstaller = emergencyInstaller;
mArchivedPackage = null;
mDeclaredLibraries = declaredLibraries;
+ mPageSizeCompat = pageSizeCompat;
}
public ApkLite(String path, ArchivedPackageParcel archivedPackage) {
@@ -292,6 +300,7 @@ public class ApkLite {
mEmergencyInstaller = null;
mArchivedPackage = archivedPackage;
mDeclaredLibraries = null;
+ mPageSizeCompat = ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED;
}
/**
@@ -676,11 +685,19 @@ public class ApkLite {
return mArchivedPackage;
}
+ /**
+ * pageSizeCompat info from manifest file
+ */
+ @DataClass.Generated.Member
+ public int getPageSizeCompat() {
+ return mPageSizeCompat;
+ }
+
@DataClass.Generated(
- time = 1731589363302L,
+ time = 1738189581427L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/content/pm/parsing/ApkLite.java",
- inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mRevisionCode\nprivate final int mInstallLocation\nprivate final int mMinSdkVersion\nprivate final int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final boolean mFeatureSplit\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mProfileableByShell\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final boolean mOverlayIsStatic\nprivate final int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final int mRollbackDataPolicy\nprivate final boolean mHasDeviceAdminReceiver\nprivate final boolean mIsSdkLibrary\nprivate final boolean mIsStaticLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final boolean mUpdatableSystem\nprivate final @android.annotation.Nullable java.lang.String mEmergencyInstaller\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
+ inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.Nullable java.lang.String mSplitName\nprivate final @android.annotation.Nullable java.lang.String mUsesSplitName\nprivate final @android.annotation.Nullable java.lang.String mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mRevisionCode\nprivate final int mInstallLocation\nprivate final int mMinSdkVersion\nprivate final int mTargetSdkVersion\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final boolean mFeatureSplit\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mProfileableByShell\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mUseEmbeddedDex\nprivate final @android.annotation.Nullable java.lang.String mTargetPackageName\nprivate final boolean mOverlayIsStatic\nprivate final int mOverlayPriority\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyName\nprivate final @android.annotation.Nullable java.lang.String mRequiredSystemPropertyValue\nprivate final int mRollbackDataPolicy\nprivate final boolean mHasDeviceAdminReceiver\nprivate final boolean mIsSdkLibrary\nprivate final boolean mIsStaticLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final boolean mUpdatableSystem\nprivate final @android.annotation.Nullable java.lang.String mEmergencyInstaller\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\nprivate final int mPageSizeCompat\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass ApkLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
index 71d0a04760ac..26252a990676 100644
--- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
+++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java
@@ -22,6 +22,7 @@ import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
import android.annotation.NonNull;
import android.app.admin.DeviceAdminReceiver;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.SharedLibraryInfo;
@@ -459,6 +460,7 @@ public class ApkLiteParseUtils {
boolean overlayIsStatic = false;
int overlayPriority = 0;
int rollbackDataPolicy = 0;
+ int pageSizeCompat = ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED;
String requiredSystemPropertyName = null;
String requiredSystemPropertyValue = null;
@@ -516,6 +518,10 @@ public class ApkLiteParseUtils {
boolean hasBindDeviceAdminPermission =
android.Manifest.permission.BIND_DEVICE_ADMIN.equals(permission);
+ pageSizeCompat = parser.getAttributeIntValue(ANDROID_RES_NAMESPACE,
+ "pageSizeCompat",
+ ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED);
+
final int innerDepth = parser.getDepth();
int innerType;
while ((innerType = parser.next()) != XmlPullParser.END_DOCUMENT
@@ -817,7 +823,7 @@ public class ApkLiteParseUtils {
usesSdkLibrariesVersionsMajor, usesSdkLibrariesCertDigests, isStaticLibrary,
usesStaticLibraries, usesStaticLibrariesVersions,
usesStaticLibrariesCertDigests, updatableSystem, emergencyInstaller,
- declaredLibraries));
+ declaredLibraries, pageSizeCompat));
}
private static ParseResult<String[]> parseAdditionalCertificates(ParseInput input,
diff --git a/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java b/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java
index d2d3a6840acc..c7403c0ea98c 100644
--- a/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java
+++ b/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java
@@ -339,36 +339,6 @@ public class FrameworkParsingPackageUtils {
}
/**
- * Check if a package is compatible with this platform with regards to its
- * its minSdkVersionFull.
- *
- * @param minSdkVersionFullString A string representation of a major.minor version,
- * e.g. "12.34"
- * @param platformMinSdkVersionFull The major and minor version of the platform, i.e. the value
- * of Build.VERSION.SDK_INT_FULL
- * @param input A ParseInput object to report success or failure
- */
- public static ParseResult<Void> verifyMinSdkVersionFull(@NonNull String minSdkVersionFullString,
- int platformMinSdkVersionFull, @NonNull ParseInput input) {
- int minSdkVersionFull;
- try {
- minSdkVersionFull = Build.parseFullVersion(minSdkVersionFullString);
- } catch (IllegalStateException e) {
- return input.error(PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,
- e.getMessage());
- }
- if (minSdkVersionFull <= platformMinSdkVersionFull) {
- return input.success(null);
- }
- return input.error(PackageManager.INSTALL_FAILED_OLDER_SDK,
- "Requires newer sdk version "
- + Build.fullVersionToString(minSdkVersionFull)
- + " (current version is "
- + Build.fullVersionToString(platformMinSdkVersionFull)
- + ")");
- }
-
- /**
* Computes the targetSdkVersion to use at runtime. If the package is not compatible with this
* platform, populates {@code outError[0]} with an error message.
* <p>
diff --git a/core/java/android/content/pm/parsing/PackageLite.java b/core/java/android/content/pm/parsing/PackageLite.java
index 0e11eecfc7ec..43a3645f34cd 100644
--- a/core/java/android/content/pm/parsing/PackageLite.java
+++ b/core/java/android/content/pm/parsing/PackageLite.java
@@ -138,6 +138,11 @@ public class PackageLite {
*/
private final @Nullable ArchivedPackageParcel mArchivedPackage;
+ /**
+ * pageSizeCompat info from manifest file
+ */
+ private final int mPageSizeCompat;
+
public PackageLite(String path, String baseApkPath, ApkLite baseApk,
String[] splitNames, boolean[] isFeatureSplits, String[] usesSplitNames,
String[] configForSplit, String[] splitApkPaths, int[] splitRevisionCodes,
@@ -182,6 +187,7 @@ public class PackageLite {
mTargetSdk = targetSdk;
mDeclaredLibraries = baseApk.getDeclaredLibraries();
mArchivedPackage = baseApk.getArchivedPackage();
+ mPageSizeCompat = baseApk.getPageSizeCompat();
}
/**
@@ -511,11 +517,19 @@ public class PackageLite {
return mArchivedPackage;
}
+ /**
+ * pageSizeCompat info from manifest file
+ */
+ @DataClass.Generated.Member
+ public int getPageSizeCompat() {
+ return mPageSizeCompat;
+ }
+
@DataClass.Generated(
- time = 1731591578587L,
+ time = 1738193799106L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/content/pm/parsing/PackageLite.java",
- inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.NonNull java.lang.String mBaseApkPath\nprivate final @android.annotation.Nullable java.lang.String[] mSplitApkPaths\nprivate final @android.annotation.Nullable java.lang.String[] mSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mUsesSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mBaseRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mTargetSdk\nprivate final int mBaseRevisionCode\nprivate final @android.annotation.Nullable int[] mSplitRevisionCodes\nprivate final int mInstallLocation\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final @android.annotation.Nullable boolean[] mIsFeatureSplits\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mProfileableByShell\nprivate final boolean mUseEmbeddedDex\nprivate final boolean mIsSdkLibrary\nprivate final boolean mIsStaticLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\npublic java.util.List<java.lang.String> getAllApkPaths()\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass PackageLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
+ inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.lang.String mPath\nprivate final @android.annotation.NonNull java.lang.String mBaseApkPath\nprivate final @android.annotation.Nullable java.lang.String[] mSplitApkPaths\nprivate final @android.annotation.Nullable java.lang.String[] mSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mUsesSplitNames\nprivate final @android.annotation.Nullable java.lang.String[] mConfigForSplit\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String> mBaseRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mRequiredSplitTypes\nprivate final @android.annotation.Nullable java.util.Set<java.lang.String>[] mSplitTypes\nprivate final int mVersionCodeMajor\nprivate final int mVersionCode\nprivate final int mTargetSdk\nprivate final int mBaseRevisionCode\nprivate final @android.annotation.Nullable int[] mSplitRevisionCodes\nprivate final int mInstallLocation\nprivate final @android.annotation.NonNull android.content.pm.VerifierInfo[] mVerifiers\nprivate final @android.annotation.NonNull android.content.pm.SigningDetails mSigningDetails\nprivate final @android.annotation.Nullable boolean[] mIsFeatureSplits\nprivate final boolean mIsolatedSplits\nprivate final boolean mSplitRequired\nprivate final boolean mCoreApp\nprivate final boolean mDebuggable\nprivate final boolean mMultiArch\nprivate final boolean mUse32bitAbi\nprivate final boolean mExtractNativeLibs\nprivate final boolean mProfileableByShell\nprivate final boolean mUseEmbeddedDex\nprivate final boolean mIsSdkLibrary\nprivate final boolean mIsStaticLibrary\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesSdkLibraries\nprivate final @android.annotation.Nullable long[] mUsesSdkLibrariesVersionsMajor\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesSdkLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<java.lang.String> mUsesStaticLibraries\nprivate final @android.annotation.Nullable long[] mUsesStaticLibrariesVersions\nprivate final @android.annotation.Nullable java.lang.String[][] mUsesStaticLibrariesCertDigests\nprivate final @android.annotation.NonNull java.util.List<android.content.pm.SharedLibraryInfo> mDeclaredLibraries\nprivate final @android.annotation.Nullable android.content.pm.ArchivedPackageParcel mArchivedPackage\nprivate final int mPageSizeCompat\npublic java.util.List<java.lang.String> getAllApkPaths()\npublic long getLongVersionCode()\nprivate boolean hasAnyRequiredSplitTypes()\nclass PackageLite extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genConstDefs=false)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/hardware/biometrics/BiometricConstants.java b/core/java/android/hardware/biometrics/BiometricConstants.java
index 7dc6afba3f1c..a7fbce51e9df 100644
--- a/core/java/android/hardware/biometrics/BiometricConstants.java
+++ b/core/java/android/hardware/biometrics/BiometricConstants.java
@@ -188,6 +188,24 @@ public interface BiometricConstants {
int BIOMETRIC_ERROR_CONTENT_VIEW_MORE_OPTIONS_BUTTON = 22;
/**
+ * The error code returned after lock out error happens, the error dialog shows, and the users
+ * dismisses the dialog. This is a placeholder that is currently only used by the support
+ * library.
+ *
+ * @hide
+ */
+ int BIOMETRIC_ERROR_LOCKOUT_ERROR_DIALOG_DISMISSED = 23;
+
+ /**
+ * The error code returned after biometric hardware error happens, the error dialog shows, and
+ * the users dismisses the dialog.This is a placeholder that is currently only used by the
+ * support library.
+ *
+ * @hide
+ */
+ int BIOMETRIC_ERROR_BIOMETRIC_HARDWARE_ERROR_DIALOG_DISMISSED = 24;
+
+ /**
* This constant is only used by SystemUI. It notifies SystemUI that authentication was paused
* because the authentication attempt was unsuccessful.
* @hide
@@ -219,6 +237,8 @@ public interface BiometricConstants {
BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE,
BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS,
BIOMETRIC_ERROR_CONTENT_VIEW_MORE_OPTIONS_BUTTON,
+ BIOMETRIC_ERROR_LOCKOUT_ERROR_DIALOG_DISMISSED,
+ BIOMETRIC_ERROR_BIOMETRIC_HARDWARE_ERROR_DIALOG_DISMISSED,
BIOMETRIC_PAUSED_REJECTED})
@Retention(RetentionPolicy.SOURCE)
@interface Errors {}
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index 89a6b02b56c4..56d272768a66 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -1616,7 +1616,13 @@ public class CameraDeviceImpl extends CameraDevice
// request if no repeating request is active. A default capture request is created here
// for initial use. The capture callback will provide capture results that include the
// actual capture parameters used for the streaming.
- CaptureRequest.Builder builder = createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
+ CameraMetadataNative templatedRequest = mRemoteDevice.createDefaultRequest(
+ CameraDevice.TEMPLATE_PREVIEW);
+
+ CaptureRequest.Builder builder = new CaptureRequest.Builder(
+ templatedRequest, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE,
+ getId(), /*physicalCameraIdSet*/ null);
+
for (Surface surface : surfaces) {
builder.addTarget(surface);
}
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index ae017e80966f..23722ed5bb0d 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -225,3 +225,11 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "key_event_activity_detection"
+ namespace: "input"
+ is_exported: true
+ description: "Key Event Activity Detection"
+ bug: "356412905"
+}
diff --git a/core/java/android/os/ChildZygoteProcess.java b/core/java/android/os/ChildZygoteProcess.java
index d8f825a2ee60..84fd0ca4cce9 100644
--- a/core/java/android/os/ChildZygoteProcess.java
+++ b/core/java/android/os/ChildZygoteProcess.java
@@ -67,12 +67,15 @@ public class ChildZygoteProcess extends ZygoteProcess {
if (mDead.get()) {
return true;
}
+ StrictMode.ThreadPolicy oldStrictModeThreadPolicy = StrictMode.allowThreadDiskReads();
try {
if (Os.stat("/proc/" + mPid).st_uid == mUid) {
return false;
}
} catch (ErrnoException e) {
// Do nothing, it's dead.
+ } finally {
+ StrictMode.setThreadPolicy(oldStrictModeThreadPolicy);
}
mDead.set(true);
return true;
diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
index 877f130a8b5a..349a2f0a181d 100644
--- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java
+++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java
@@ -24,8 +24,6 @@ import android.annotation.TestApi;
import android.app.ActivityThread;
import android.app.Instrumentation;
import android.compat.annotation.UnsupportedAppUsage;
-import android.os.Process;
-import android.os.UserHandle;
import android.ravenwood.annotation.RavenwoodKeepWholeClass;
import android.ravenwood.annotation.RavenwoodRedirect;
import android.ravenwood.annotation.RavenwoodRedirectionClass;
@@ -95,6 +93,13 @@ public final class MessageQueue {
private int mAsyncMessageCount;
/**
+ * @hide
+ */
+ private final AtomicLong mMessageCount = new AtomicLong();
+ private final Thread mThread;
+ private final long mTid;
+
+ /**
* Select between two implementations of message queue. The legacy implementation is used
* by default as it provides maximum compatibility with applications and tests that
* reach into MessageQueue via the mMessages field. The concurrent implemmentation is used for
@@ -128,6 +133,8 @@ public final class MessageQueue {
mUseConcurrent = sIsProcessAllowedToUseConcurrent && !isInstrumenting();
mQuitAllowed = quitAllowed;
mPtr = nativeInit();
+ mThread = Thread.currentThread();
+ mTid = Process.myTid();
}
private static void initIsProcessAllowedToUseConcurrent() {
@@ -218,6 +225,32 @@ public final class MessageQueue {
}
}
+ private void decAndTraceMessageCount() {
+ mMessageCount.decrementAndGet();
+ traceMessageCount();
+ }
+
+ private void incAndTraceMessageCount(Message msg, long when) {
+ mMessageCount.incrementAndGet();
+ msg.mSendingThreadName = Thread.currentThread().getName();
+ msg.mEventId.set(PerfettoTrace.getFlowId());
+
+ traceMessageCount();
+ PerfettoTrace.instant(PerfettoTrace.MQ_CATEGORY, "message_queue_send")
+ .addFlow(msg.mEventId.get())
+ .addArg("receiving_thread", mThread.getName())
+ .addArg("delay", when - SystemClock.uptimeMillis())
+ .addArg("what", msg.what)
+ .emit();
+ }
+
+ /** @hide */
+ private void traceMessageCount() {
+ PerfettoTrace.counter(PerfettoTrace.MQ_CATEGORY, mMessageCount.get())
+ .usingThreadCounterTrack(mTid, mThread.getName())
+ .emit();
+ }
+
// Disposes of the underlying message queue.
// Must only be called on the looper thread or the finalizer.
private void dispose() {
@@ -800,6 +833,7 @@ public final class MessageQueue {
Message msg = nextMessage(false, false);
if (msg != null) {
msg.markInUse();
+ decAndTraceMessageCount();
return msg;
}
@@ -909,6 +943,7 @@ public final class MessageQueue {
if (msg.isAsynchronous()) {
mAsyncMessageCount--;
}
+ decAndTraceMessageCount();
if (TRACE) {
Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet());
}
@@ -1075,6 +1110,7 @@ public final class MessageQueue {
msg.markInUse();
msg.arg1 = token;
+ incAndTraceMessageCount(msg, when);
if (!enqueueMessageUnchecked(msg, when)) {
Log.wtf(TAG_C, "Unexpected error while adding sync barrier!");
@@ -1090,6 +1126,7 @@ public final class MessageQueue {
msg.markInUse();
msg.when = when;
msg.arg1 = token;
+ incAndTraceMessageCount(msg, when);
if (Flags.messageQueueTailTracking() && mLast != null && mLast.when <= when) {
/* Message goes to tail of list */
@@ -1196,6 +1233,7 @@ public final class MessageQueue {
needWake = mMessages == null || mMessages.target != null;
}
p.recycleUnchecked();
+ decAndTraceMessageCount();
// If the loop is quitting then it is already awake.
// We can assume mPtr != 0 when mQuitting is false.
@@ -1252,6 +1290,8 @@ public final class MessageQueue {
msg.markInUse();
msg.when = when;
+ incAndTraceMessageCount(msg, when);
+
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
@@ -1391,6 +1431,7 @@ public final class MessageQueue {
if (msg.isAsynchronous()) {
mAsyncMessageCount--;
}
+ decAndTraceMessageCount();
if (TRACE) {
Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet());
}
@@ -1642,6 +1683,7 @@ public final class MessageQueue {
mAsyncMessageCount--;
}
p.recycleUnchecked();
+ decAndTraceMessageCount();
p = n;
}
@@ -1660,6 +1702,7 @@ public final class MessageQueue {
mAsyncMessageCount--;
}
n.recycleUnchecked();
+ decAndTraceMessageCount();
p.next = nn;
if (p.next == null) {
mLast = p;
@@ -1718,6 +1761,7 @@ public final class MessageQueue {
mAsyncMessageCount--;
}
n.recycleUnchecked();
+ decAndTraceMessageCount();
p.next = nn;
if (p.next == null) {
mLast = p;
@@ -1759,6 +1803,7 @@ public final class MessageQueue {
mAsyncMessageCount--;
}
p.recycleUnchecked();
+ decAndTraceMessageCount();
p = n;
}
@@ -1777,6 +1822,7 @@ public final class MessageQueue {
mAsyncMessageCount--;
}
n.recycleUnchecked();
+ decAndTraceMessageCount();
p.next = nn;
if (p.next == null) {
mLast = p;
@@ -1832,6 +1878,7 @@ public final class MessageQueue {
mAsyncMessageCount--;
}
p.recycleUnchecked();
+ decAndTraceMessageCount();
p = n;
}
@@ -1850,6 +1897,7 @@ public final class MessageQueue {
mAsyncMessageCount--;
}
n.recycleUnchecked();
+ decAndTraceMessageCount();
p.next = nn;
if (p.next == null) {
mLast = p;
@@ -1904,6 +1952,7 @@ public final class MessageQueue {
mAsyncMessageCount--;
}
p.recycleUnchecked();
+ decAndTraceMessageCount();
p = n;
}
@@ -1921,6 +1970,7 @@ public final class MessageQueue {
mAsyncMessageCount--;
}
n.recycleUnchecked();
+ decAndTraceMessageCount();
p.next = nn;
if (p.next == null) {
mLast = p;
@@ -1976,6 +2026,7 @@ public final class MessageQueue {
mAsyncMessageCount--;
}
p.recycleUnchecked();
+ decAndTraceMessageCount();
p = n;
}
@@ -1993,6 +2044,7 @@ public final class MessageQueue {
mAsyncMessageCount--;
}
n.recycleUnchecked();
+ decAndTraceMessageCount();
p.next = nn;
if (p.next == null) {
mLast = p;
@@ -2027,6 +2079,8 @@ public final class MessageQueue {
mMessages = null;
mLast = null;
mAsyncMessageCount = 0;
+ mMessageCount.set(0);
+ traceMessageCount();
}
private void removeAllFutureMessagesLocked() {
@@ -2057,6 +2111,7 @@ public final class MessageQueue {
mAsyncMessageCount--;
}
p.recycleUnchecked();
+ decAndTraceMessageCount();
} while (n != null);
}
}
@@ -2701,6 +2756,7 @@ public final class MessageQueue {
MessageNode node = new MessageNode(msg, seq);
msg.when = when;
msg.markInUse();
+ incAndTraceMessageCount(msg, when);
if (DEBUG) {
Log.d(TAG_C, "Insert message what: " + msg.what + " when: " + msg.when + " seq: "
@@ -2828,6 +2884,7 @@ public final class MessageQueue {
if (removeMatches) {
if (p.removeFromStack()) {
p.mMessage.recycleUnchecked();
+ decAndTraceMessageCount();
if (mMessageCounts.incrementCancelled()) {
nativeWake(mPtr);
}
@@ -2870,6 +2927,7 @@ public final class MessageQueue {
found = true;
if (queue.remove(msg)) {
msg.mMessage.recycleUnchecked();
+ decAndTraceMessageCount();
}
} else {
return true;
diff --git a/core/java/android/os/ITradeInMode.aidl b/core/java/android/os/ITradeInMode.aidl
index f15954d14d0e..d05f52cf6a90 100644
--- a/core/java/android/os/ITradeInMode.aidl
+++ b/core/java/android/os/ITradeInMode.aidl
@@ -59,4 +59,37 @@ interface ITradeInMode {
* ENTER_TRADE_IN_MODE permission is required.
*/
boolean enterEvaluationMode();
+
+ /**
+ * Schedules a wipe to trigger SUW for trade-in mode testing. A reboot is
+ * required. After this, startTesting() can be called.
+ *
+ * ENTER_TRADE_IN_MODE permission is required and ro.debuggable must be 1.
+ */
+ void scheduleWipeForTesting();
+
+ /**
+ * Enables testing. This only takes effect after the next reboot, and is
+ * only allowed in ro.debuggable builds. On the following boot, normal
+ * adbd will be disabled and trade-in mode adbd will be enabled instead.
+ *
+ * ENTER_TRADE_IN_MODE permission is required and ro.debuggable must be 1.
+ */
+ void startTesting();
+
+ /**
+ * Disables testing. This disables trade-in mode and removes any scheduled
+ * trade-in mode wipe.
+ *
+ * ENTER_TRADE_IN_MODE permission is required, ro.debuggable must be 1, and
+ * startTesting() must have been called.
+ */
+ void stopTesting();
+
+ /**
+ * Returns whether the device is testing trade-in mode.
+ *
+ * ENTER_TRADE_IN_MODE permission is required and ro.debuggable must be 1.
+ */
+ boolean isTesting();
}
diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java
index 2fe4871e08dd..d16e4473d55f 100644
--- a/core/java/android/os/Looper.java
+++ b/core/java/android/os/Looper.java
@@ -199,7 +199,12 @@ public final class Looper {
return false;
}
- // This must be in a local variable, in case a UI event sets the logger
+ PerfettoTrace.begin(PerfettoTrace.MQ_CATEGORY, "message_queue_receive")
+ .addArg("sending_thread", msg.mSendingThreadName)
+ .addTerminatingFlow(msg.mEventId.get())
+ .emit();
+
+ // This must be in a local variabe, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " "
@@ -289,6 +294,7 @@ public final class Looper {
+ msg.callback + " what=" + msg.what);
}
+ PerfettoTrace.end(PerfettoTrace.MQ_CATEGORY).emit();
msg.recycleUnchecked();
return true;
diff --git a/core/java/android/os/Message.java b/core/java/android/os/Message.java
index 702fdc2bbaa6..b22d1774d967 100644
--- a/core/java/android/os/Message.java
+++ b/core/java/android/os/Message.java
@@ -23,6 +23,8 @@ import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
+import java.util.concurrent.atomic.AtomicInteger;
+
/**
*
* Defines a message containing a description and arbitrary data object that can be
@@ -37,6 +39,13 @@ import com.android.internal.annotations.VisibleForTesting;
@android.ravenwood.annotation.RavenwoodKeepWholeClass
public final class Message implements Parcelable {
/**
+ * For tracing
+ *
+ * @hide Only for use within the system server.
+ */
+ public final AtomicInteger mEventId = new AtomicInteger();
+
+ /**
* User-defined message code so that the recipient can identify
* what this message is about. Each {@link Handler} has its own name-space
* for message codes, so you do not need to worry about yours conflicting
@@ -101,6 +110,13 @@ public final class Message implements Parcelable {
*/
public int workSourceUid = UID_NONE;
+ /**
+ * Sending thread
+ *
+ * @hide
+ */
+ public String mSendingThreadName;
+
/** If set message is in use.
* This flag is set when the message is enqueued and remains set while it
* is delivered and afterwards when it is recycled. The flag is only cleared
diff --git a/core/java/android/os/PerfettoTrace.java b/core/java/android/os/PerfettoTrace.java
index 68f1570154ff..741d542ecb3b 100644
--- a/core/java/android/os/PerfettoTrace.java
+++ b/core/java/android/os/PerfettoTrace.java
@@ -16,6 +16,8 @@
package android.os;
+import com.android.internal.ravenwood.RavenwoodEnvironment;
+
import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
@@ -32,6 +34,7 @@ import java.util.concurrent.atomic.AtomicInteger;
*
* @hide
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public final class PerfettoTrace {
private static final String TAG = "PerfettoTrace";
@@ -48,11 +51,14 @@ public final class PerfettoTrace {
*/
private static final AtomicInteger sFlowEventId = new AtomicInteger();
+ public static final PerfettoTrace.Category MQ_CATEGORY = new PerfettoTrace.Category("mq");
+
/**
* Perfetto category a trace event belongs to.
* Registering a category is not sufficient to capture events within the category, it must
* also be enabled in the trace config.
*/
+ @android.ravenwood.annotation.RavenwoodKeepWholeClass
public static final class Category implements PerfettoTrackEventExtra.PerfettoPointer {
private static final NativeAllocationRegistry sRegistry =
NativeAllocationRegistry.createMalloced(
@@ -97,12 +103,16 @@ public final class PerfettoTrace {
mSeverity = severity;
mPtr = native_init(name, tag, severity);
mExtraPtr = native_get_extra_ptr(mPtr);
- sRegistry.registerNativeAllocation(this, mPtr);
+ if (!RavenwoodEnvironment.getInstance().isRunningOnRavenwood()) {
+ sRegistry.registerNativeAllocation(this, mPtr);
+ }
}
@FastNative
+ @android.ravenwood.annotation.RavenwoodReplace
private static native long native_init(String name, String tag, String severity);
@CriticalNative
+ @android.ravenwood.annotation.RavenwoodReplace
private static native long native_delete();
@CriticalNative
private static native void native_register(long ptr);
@@ -111,8 +121,24 @@ public final class PerfettoTrace {
@CriticalNative
private static native boolean native_is_enabled(long ptr);
@CriticalNative
+ @android.ravenwood.annotation.RavenwoodReplace
private static native long native_get_extra_ptr(long ptr);
+ private static long native_init$ravenwood(String name, String tag, String severity) {
+ // Tracing currently completely disabled under Ravenwood
+ return 0;
+ }
+
+ private static long native_delete$ravenwood() {
+ // Tracing currently completely disabled under Ravenwood
+ return 0;
+ }
+
+ private static long native_get_extra_ptr$ravenwood(long ptr) {
+ // Tracing currently completely disabled under Ravenwood
+ return 0;
+ }
+
/**
* Register the category.
*/
@@ -134,10 +160,16 @@ public final class PerfettoTrace {
/**
* Whether the category is enabled or not.
*/
+ @android.ravenwood.annotation.RavenwoodReplace
public boolean isEnabled() {
return IS_FLAG_ENABLED && native_is_enabled(mPtr);
}
+ public boolean isEnabled$ravenwood() {
+ // Tracing currently completely disabled under Ravenwood
+ return false;
+ }
+
/**
* Whether the category is registered or not.
*/
@@ -340,4 +372,11 @@ public final class PerfettoTrace {
public static void register(boolean isBackendInProcess) {
native_register(isBackendInProcess);
}
+
+ /**
+ * Registers categories with Perfetto.
+ */
+ public static void registerCategories() {
+ MQ_CATEGORY.register();
+ }
}
diff --git a/core/java/android/os/PerfettoTrackEventExtra.java b/core/java/android/os/PerfettoTrackEventExtra.java
index e034fb3726e3..72b909e64c70 100644
--- a/core/java/android/os/PerfettoTrackEventExtra.java
+++ b/core/java/android/os/PerfettoTrackEventExtra.java
@@ -16,6 +16,8 @@
package android.os;
+import com.android.internal.ravenwood.RavenwoodEnvironment;
+
import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
@@ -29,6 +31,7 @@ import java.util.function.Supplier;
*
* @hide
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public final class PerfettoTrackEventExtra {
private static final int DEFAULT_EXTRA_CACHE_SIZE = 5;
private static final Builder NO_OP_BUILDER = new NoOpBuilder();
@@ -305,6 +308,7 @@ public final class PerfettoTrackEventExtra {
Builder endNested();
}
+ @android.ravenwood.annotation.RavenwoodKeepWholeClass
public static final class NoOpBuilder implements Builder {
@Override
public void emit() {}
@@ -759,7 +763,9 @@ public final class PerfettoTrackEventExtra {
private PerfettoTrackEventExtra() {
mPtr = native_init();
- sRegistry.registerNativeAllocation(this, mPtr);
+ if (!RavenwoodEnvironment.getInstance().isRunningOnRavenwood()) {
+ sRegistry.registerNativeAllocation(this, mPtr);
+ }
}
/**
@@ -1342,8 +1348,10 @@ public final class PerfettoTrackEventExtra {
}
@CriticalNative
+ @android.ravenwood.annotation.RavenwoodReplace
private static native long native_init();
@CriticalNative
+ @android.ravenwood.annotation.RavenwoodReplace
private static native long native_delete();
@CriticalNative
private static native void native_add_arg(long ptr, long extraPtr);
@@ -1351,4 +1359,14 @@ public final class PerfettoTrackEventExtra {
private static native void native_clear_args(long ptr);
@FastNative
private static native void native_emit(int type, long tag, String name, long ptr);
+
+ private static long native_init$ravenwood() {
+ // Tracing currently completely disabled under Ravenwood
+ return 0;
+ }
+
+ private static long native_delete$ravenwood() {
+ // Tracing currently completely disabled under Ravenwood
+ return 0;
+ }
}
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index e769abec7dd9..5129af6be442 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -4213,6 +4213,17 @@ public final class PowerManager {
else mFlags &= ~UNIMPORTANT_FOR_LOGGING;
}
+ /** @hide */
+ public void updateUids(int[] uids) {
+ synchronized (mToken) {
+ try {
+ mService.updateWakeLockUids(mToken, uids);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
@Override
public String toString() {
synchronized (mToken) {
diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java
index ddfa3799706e..2d9d025b8d80 100644
--- a/core/java/android/os/TestLooperManager.java
+++ b/core/java/android/os/TestLooperManager.java
@@ -18,6 +18,7 @@ import android.annotation.FlaggedApi;
import android.annotation.Nullable;
import android.util.ArraySet;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
/**
@@ -38,10 +39,13 @@ public class TestLooperManager {
private final MessageQueue mQueue;
private final Looper mLooper;
private final LinkedBlockingQueue<MessageExecution> mExecuteQueue = new LinkedBlockingQueue<>();
+ private final boolean mLooperIsMyLooper;
+
+ // When this latch is zero, it's guaranteed that the LooperHolder Message
+ // is not in the underlying queue.
+ private final CountDownLatch mLooperHolderLatch = new CountDownLatch(1);
private boolean mReleased;
- private boolean mLooperBlocked;
- private final boolean mLooperIsMyLooper;
/**
* @hide
@@ -59,6 +63,8 @@ public class TestLooperManager {
if (!mLooperIsMyLooper) {
// Post a message that will keep the looper blocked as long as we are dispatching.
new Handler(looper).post(new LooperHolder());
+ } else {
+ mLooperHolderLatch.countDown();
}
}
@@ -168,6 +174,7 @@ public class TestLooperManager {
try {
execution.wait();
} catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
}
if (execution.response != null) {
throw new RuntimeException(execution.response);
@@ -222,23 +229,17 @@ public class TestLooperManager {
* is not in the underlying queue.
*/
private void waitForLooperHolder() {
- while (!mLooperIsMyLooper && !mLooperBlocked) {
- synchronized (this) {
- try {
- wait();
- } catch (InterruptedException e) {
- }
- }
+ try {
+ mLooperHolderLatch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
}
}
private class LooperHolder implements Runnable {
@Override
public void run() {
- synchronized (TestLooperManager.this) {
- mLooperBlocked = true;
- TestLooperManager.this.notify();
- }
+ mLooperHolderLatch.countDown();
while (!mReleased) {
try {
final MessageExecution take = mExecuteQueue.take();
@@ -246,11 +247,9 @@ public class TestLooperManager {
processMessage(take);
}
} catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
}
}
- synchronized (TestLooperManager.this) {
- mLooperBlocked = false;
- }
}
private void processMessage(MessageExecution mex) {
diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java
index 09e6a45dc294..bf4c4d16fe19 100644
--- a/core/java/android/os/Trace.java
+++ b/core/java/android/os/Trace.java
@@ -544,5 +544,6 @@ public final class Trace {
*/
public static void registerWithPerfetto() {
PerfettoTrace.register(false /* isBackendInProcess */);
+ PerfettoTrace.registerCategories();
}
}
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index aaf6489d18ca..ce93c71ac776 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -5326,7 +5326,9 @@ public class UserManager {
}
/**
- * Returns list of the profiles of userId including userId itself.
+ * Returns a list of the users that are associated with userId, including userId itself. This
+ * includes the user, its profiles, its parent, and its parent's other profiles, as applicable.
+ *
* Note that this returns both enabled and not enabled profiles. See
* {@link #getEnabledProfiles(int)} if you need only the enabled ones.
* <p>Note that this includes all profile types (not including Restricted profiles).
@@ -5334,7 +5336,7 @@ public class UserManager {
* <p>Requires {@link android.Manifest.permission#MANAGE_USERS} or
* {@link android.Manifest.permission#CREATE_USERS} or
* {@link android.Manifest.permission#QUERY_USERS} if userId is not the calling user.
- * @param userId profiles of this user will be returned.
+ * @param userId profiles associated with this user (including itself) will be returned.
* @return the list of profiles.
* @hide
*/
@@ -5358,12 +5360,13 @@ public class UserManager {
}
/**
- * Returns list of the profiles of the given user, including userId itself, as well as the
- * communal profile, if there is one.
+ * Returns a list of the users that are associated with userId, including userId itself,
+ * as well as the communal profile, if there is one.
*
* <p>Note that this returns both enabled and not enabled profiles.
* <p>Note that this includes all profile types (not including Restricted profiles).
*
+ * @see #getProfiles(int)
* @hide
*/
@FlaggedApi(android.multiuser.Flags.FLAG_SUPPORT_COMMUNAL_PROFILE)
@@ -5419,7 +5422,10 @@ public class UserManager {
}
/**
- * Returns list of the profiles of userId including userId itself.
+ * Returns a list of the enabled users that are associated with userId, including userId itself.
+ * This includes the user, its profiles, its parent, and its parent's other profiles, as
+ * applicable.
+ *
* Note that this returns only {@link UserInfo#isEnabled() enabled} profiles.
* <p>Note that this includes all profile types (not including Restricted profiles).
*
@@ -5447,8 +5453,10 @@ public class UserManager {
}
/**
- * Returns a list of UserHandles for profiles associated with the context user, including the
- * user itself.
+ * Returns a list of the users that are associated with the context user, including the user
+ * itself. This includes the user, its profiles, its parent, and its parent's other profiles,
+ * as applicable.
+ *
* <p>Note that this includes all profile types (not including Restricted profiles).
*
* @return A non-empty list of UserHandles associated with the context user.
@@ -5465,8 +5473,10 @@ public class UserManager {
}
/**
- * Returns a list of ids for enabled profiles associated with the context user including the
- * user itself.
+ * Returns a list of the enabled users that are associated with the context user, including the
+ * user itself. This includes the user, its profiles, its parent, and its parent's other
+ * profiles, as applicable.
+ *
* <p>Note that this includes all profile types (not including Restricted profiles).
*
* @return A non-empty list of UserHandles associated with the context user.
@@ -5483,8 +5493,10 @@ public class UserManager {
}
/**
- * Returns a list of ids for all profiles associated with the context user including the user
- * itself.
+ * Returns a list of all users that are associated with the context user, including the user
+ * itself. This includes the user, its profiles, its parent, and its parent's other profiles,
+ * as applicable.
+ *
* <p>Note that this includes all profile types (not including Restricted profiles).
*
* @return A non-empty list of UserHandles associated with the context user.
@@ -5501,8 +5513,10 @@ public class UserManager {
}
/**
- * Returns a list of ids for profiles associated with the context user including the user
- * itself.
+ * Returns a list of the users that are associated with the context user, including the user
+ * itself. This includes the user, its profiles, its parent, and its parent's other profiles, as
+ * applicable.
+ *
* <p>Note that this includes all profile types (not including Restricted profiles).
*
* @param enabledOnly whether to return only {@link UserInfo#isEnabled() enabled} profiles
@@ -5528,8 +5542,10 @@ public class UserManager {
}
/**
- * Returns a list of ids for profiles associated with the specified user including the user
- * itself.
+ * Returns a list of the users that are associated with the specified user, including the user
+ * itself. This includes the user, its profiles, its parent, and its parent's other profiles,
+ * as applicable.
+ *
* <p>Note that this includes all profile types (not including Restricted profiles).
*
* @param userId id of the user to return profiles for
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index bf34069f9445..23c43f56f2bc 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -149,6 +149,8 @@ import android.annotation.UiContext;
import android.app.ActivityManager;
import android.app.ActivityThread;
import android.app.ResourcesManager;
+import android.app.UiModeManager;
+import android.app.UiModeManager.ForceInvertStateChangeListener;
import android.app.WindowConfiguration;
import android.app.compat.CompatChanges;
import android.app.servertransaction.WindowStateTransactionItem;
@@ -164,7 +166,6 @@ import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
-import android.database.ContentObserver;
import android.graphics.BLASTBufferQueue;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -210,7 +211,6 @@ import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
import android.os.Vibrator;
-import android.provider.Settings;
import android.sysprop.DisplayProperties;
import android.sysprop.ViewProperties;
import android.text.TextUtils;
@@ -463,10 +463,8 @@ public final class ViewRootImpl implements ViewParent,
private CompatOnBackInvokedCallback mCompatOnBackInvokedCallback;
@Nullable
- private ContentObserver mForceInvertObserver;
+ private ForceInvertStateChangeListener mForceInvertStateChangeListener;
- private static final int INVALID_VALUE = Integer.MIN_VALUE;
- private int mForceInvertEnabled = INVALID_VALUE;
/**
* Callback for notifying about global configuration changes.
*/
@@ -543,6 +541,8 @@ public final class ViewRootImpl implements ViewParent,
@UiContext
public final Context mContext;
+ private UiModeManager mUiModeManager;
+
@UnsupportedAppUsage
final IWindowSession mWindowSession;
@NonNull Display mDisplay;
@@ -1238,6 +1238,7 @@ public final class ViewRootImpl implements ViewParent,
public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
WindowLayout windowLayout) {
mContext = context;
+ mUiModeManager = context.getSystemService(UiModeManager.class);
mWindowSession = session;
mWindowLayout = windowLayout;
mDisplay = display;
@@ -1781,23 +1782,6 @@ public final class ViewRootImpl implements ViewParent,
}
}
- private boolean isForceInvertEnabled() {
- if (mForceInvertEnabled == INVALID_VALUE) {
- reloadForceInvertEnabled();
- }
- return mForceInvertEnabled == 1;
- }
-
- private void reloadForceInvertEnabled() {
- if (forceInvertColor()) {
- mForceInvertEnabled = Settings.Secure.getIntForUser(
- mContext.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
- /* def= */ 0,
- UserHandle.myUserId());
- }
- }
-
/**
* Register any kind of listeners if setView was success.
*/
@@ -1829,21 +1813,11 @@ public final class ViewRootImpl implements ViewParent,
mBasePackageName);
if (forceInvertColor()) {
- if (mForceInvertObserver == null) {
- mForceInvertObserver = new ContentObserver(mHandler) {
- @Override
- public void onChange(boolean selfChange) {
- reloadForceInvertEnabled();
- updateForceDarkMode();
- }
- };
- mContext.getContentResolver().registerContentObserver(
- Settings.Secure.getUriFor(
- Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED
- ),
- false,
- mForceInvertObserver,
- UserHandle.myUserId());
+ if (mForceInvertStateChangeListener == null) {
+ mForceInvertStateChangeListener =
+ forceInvertState -> updateForceDarkMode();
+ mUiModeManager.addForceInvertStateChangeListener(mExecutor,
+ mForceInvertStateChangeListener);
}
}
}
@@ -1861,9 +1835,10 @@ public final class ViewRootImpl implements ViewParent,
.unregisterDisplayListener(mDisplayListener);
if (forceInvertColor()) {
- if (mForceInvertObserver != null) {
- mContext.getContentResolver().unregisterContentObserver(mForceInvertObserver);
- mForceInvertObserver = null;
+ if (mForceInvertStateChangeListener != null) {
+ mUiModeManager.removeForceInvertStateChangeListener(
+ mForceInvertStateChangeListener);
+ mForceInvertStateChangeListener = null;
}
}
@@ -2050,21 +2025,25 @@ public final class ViewRootImpl implements ViewParent,
return getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
}
- /** Returns true if force dark should be enabled according to various settings */
+ /**
+ * Determines the type of force dark to apply, considering force inversion, system night mode,
+ * and app-specific settings (including developer opt-outs).
+ *
+ * @return A {@link ForceDarkType.ForceDarkTypeDef} constant indicating the force dark type.
+ */
@VisibleForTesting
public @ForceDarkType.ForceDarkTypeDef int determineForceDarkType() {
if (forceInvertColor()) {
// Force invert ignores all developer opt-outs.
// We also ignore dark theme, since the app developer can override the user's preference
- // for dark mode in configuration.uiMode. Instead, we assume that the force invert
- // setting will be enabled at the same time dark theme is in the Settings app.
- if (isForceInvertEnabled()) {
+ // for dark mode in configuration.uiMode. Instead, we assume that both force invert and
+ // the system's dark theme are enabled.
+ if (mUiModeManager.getForceInvertState() == UiModeManager.FORCE_INVERT_TYPE_DARK) {
return ForceDarkType.FORCE_INVERT_COLOR_DARK;
}
}
boolean useAutoDark = getNightMode() == Configuration.UI_MODE_NIGHT_YES;
-
if (useAutoDark) {
boolean forceDarkAllowedDefault =
SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);
diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java
index 4d354e0655f0..aa0111a13b8e 100644
--- a/core/java/android/view/inputmethod/ImeTracker.java
+++ b/core/java/android/view/inputmethod/ImeTracker.java
@@ -134,6 +134,7 @@ public interface ImeTracker {
ORIGIN_CLIENT,
ORIGIN_SERVER,
ORIGIN_IME,
+ ORIGIN_WM_SHELL,
})
@Retention(RetentionPolicy.SOURCE)
@interface Origin {}
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index ec0d9152468e..0f5476f58f74 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -10082,6 +10082,7 @@ public class RemoteViews implements Parcelable, Filter {
if (mApplication != null) {
// mApplication may be null if this was created with DrawInstructions constructor.
out.write(RemoteViewsProto.PACKAGE_NAME, mApplication.packageName);
+ out.write(RemoteViewsProto.UID, mApplication.uid);
}
Resources appResources = getContextForResourcesEnsuringCorrectCachedApkPaths(
context).getResources();
@@ -10163,6 +10164,7 @@ public class RemoteViews implements Parcelable, Filter {
int mApplyFlags = 0;
long mProviderInstanceId = -1;
String mPackageName = null;
+ Integer mUid = null;
SizeF mIdealSize = null;
String mLayoutResName = null;
String mLightBackgroundResName = null;
@@ -10185,6 +10187,9 @@ public class RemoteViews implements Parcelable, Filter {
case (int) RemoteViewsProto.PACKAGE_NAME:
ref.mPackageName = in.readString(RemoteViewsProto.PACKAGE_NAME);
break;
+ case (int) RemoteViewsProto.UID:
+ ref.mUid = in.readInt(RemoteViewsProto.UID);
+ break;
case (int) RemoteViewsProto.IDEAL_SIZE:
final long idealSizeToken = in.start(RemoteViewsProto.IDEAL_SIZE);
ref.mIdealSize = createSizeFFromProto(in);
@@ -10286,8 +10291,9 @@ public class RemoteViews implements Parcelable, Filter {
Resources appResources = null;
if (!ref.mHasDrawInstructions) {
checkProtoResultNotNull(ref.mPackageName, "No application info");
- rv.mApplication = context.getPackageManager().getApplicationInfo(ref.mPackageName,
- /* flags= */ 0);
+ checkProtoResultNotNull(ref.mUid, "No uid");
+ rv.mApplication = context.getPackageManager().getApplicationInfoAsUser(
+ ref.mPackageName, /* flags= */ 0, UserHandle.getUserId(ref.mUid));
appContext = rv.getContextForResourcesEnsuringCorrectCachedApkPaths(context);
appResources = appContext.getResources();
diff --git a/core/java/android/window/DesktopExperienceFlags.java b/core/java/android/window/DesktopExperienceFlags.java
index 7758dea3ea8a..e0c48b03dad8 100644
--- a/core/java/android/window/DesktopExperienceFlags.java
+++ b/core/java/android/window/DesktopExperienceFlags.java
@@ -40,9 +40,7 @@ import java.util.function.BooleanSupplier;
* @hide
*/
public enum DesktopExperienceFlags {
- ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT(
- com.android.server.display.feature.flags.Flags::enableDisplayContentModeManagement,
- true),
+ // go/keep-sorted start
ACTIVITY_EMBEDDING_SUPPORT_FOR_CONNECTED_DISPLAYS(
Flags::activityEmbeddingSupportForConnectedDisplays, false),
BASE_DENSITY_FOR_EXTERNAL_DISPLAYS(
@@ -53,6 +51,9 @@ public enum DesktopExperienceFlags {
ENABLE_CONNECTED_DISPLAYS_DND(Flags::enableConnectedDisplaysDnd, false),
ENABLE_CONNECTED_DISPLAYS_PIP(Flags::enableConnectedDisplaysPip, false),
ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG(Flags::enableConnectedDisplaysWindowDrag, false),
+ ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT(
+ com.android.server.display.feature.flags.Flags::enableDisplayContentModeManagement,
+ true),
ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS(Flags::enableDisplayFocusInShellTransitions, false),
ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING(Flags::enableDisplayWindowingModeSwitching, false),
ENABLE_DRAG_TO_MAXIMIZE(Flags::enableDragToMaximize, true),
@@ -63,9 +64,12 @@ public enum DesktopExperienceFlags {
false),
ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF(
Flags::enablePerDisplayPackageContextCacheInStatusbarNotif, false),
+ ENABLE_TASKBAR_CONNECTED_DISPLAYS(Flags::enableTaskbarConnectedDisplays, false),
ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS(Flags::enterDesktopByDefaultOnFreeformDisplays,
false),
- REPARENT_WINDOW_TOKEN_API(Flags::reparentWindowTokenApi, true);
+ REPARENT_WINDOW_TOKEN_API(Flags::reparentWindowTokenApi, true)
+ // go/keep-sorted end
+ ;
/**
* Flag class, to be used in case the enum cannot be used because the flag is not accessible.
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 9468301ac996..082bf5dc5a1c 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -42,62 +42,68 @@ import java.util.function.BooleanSupplier;
*/
public enum DesktopModeFlags {
// All desktop mode related flags to be overridden by developer option toggle will be added here
- ENABLE_DESKTOP_WINDOWING_MODE(
- Flags::enableDesktopWindowingMode, /* shouldOverrideByDevOption= */ true),
- ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true),
- ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION(
- Flags::enableCaptionCompatInsetForceConsumption, true),
+ // go/keep-sorted start
+ DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE(Flags::disableNonResizableAppSnapResizing, true),
+ ENABLE_APP_HEADER_WITH_TASK_DENSITY(Flags::enableAppHeaderWithTaskDensity, true),
+ ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION(Flags::enableCaptionCompatInsetForceConsumption,
+ true),
ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS(
Flags::enableCaptionCompatInsetForceConsumptionAlways, true),
ENABLE_CASCADING_WINDOWS(Flags::enableCascadingWindows, true),
- ENABLE_TILE_RESIZING(Flags::enableTileResizing, true),
- ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(
- Flags::enableDesktopWindowingWallpaperActivity, true),
- ENABLE_DESKTOP_WINDOWING_MODALS_POLICY(Flags::enableDesktopWindowingModalsPolicy, true),
- ENABLE_THEMED_APP_HEADERS(Flags::enableThemedAppHeaders, true),
- ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true),
- ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH(Flags::enableDesktopWindowingQuickSwitch, true),
- ENABLE_APP_HEADER_WITH_TASK_DENSITY(Flags::enableAppHeaderWithTaskDensity, true),
- ENABLE_TASK_STACK_OBSERVER_IN_SHELL(Flags::enableTaskStackObserverInShell, true),
- ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS(Flags::enableDesktopWindowingSizeConstraints, true),
- DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE(Flags::disableNonResizableAppSnapResizing, true),
- ENABLE_WINDOWING_SCALED_RESIZING(Flags::enableWindowingScaledResizing, true),
- ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true),
+ ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX(
+ Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true),
+ ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX(Flags::enableDesktopAppLaunchTransitionsBugfix,
+ true),
+ ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS(Flags::enableCompatUiVisibilityStatus, true),
+ ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX(
+ Flags::enableDesktopRecentsTransitionsCornersBugfix, false),
+ ENABLE_DESKTOP_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE_BUGFIX(
+ Flags::skipCompatUiEducationInDesktopMode, true),
+ ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS(Flags::enableDesktopSystemDialogsTransitions, true),
+ ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER(
+ Flags::enableDesktopWallpaperActivityForSystemUser, true),
+ ENABLE_DESKTOP_WINDOWING_APP_TO_WEB(Flags::enableDesktopWindowingAppToWeb, true),
+ ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION(Flags::enableDesktopWindowingAppToWebEducation,
+ true),
ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION(Flags::enableDesktopWindowingBackNavigation, true),
- ENABLE_WINDOWING_EDGE_DRAG_RESIZE(Flags::enableWindowingEdgeDragResize, true),
- ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS(
- Flags::enableDesktopWindowingTaskbarRunningApps, true),
- ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS(
- Flags::enableWindowingTransitionHandlersObservers, false),
- ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, false),
- ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true),
ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS_BUGFIX(
Flags::enableDesktopWindowingEnterTransitionBugfix, true),
+ ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX(
+ Flags::enableDesktopWindowingExitByMinimizeTransitionBugfix, false),
ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX(
Flags::enableDesktopWindowingExitTransitionsBugfix, true),
- ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS_BUGFIX(
- Flags::enableDesktopAppLaunchAlttabTransitionsBugfix, true),
- ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX(
- Flags::enableDesktopAppLaunchTransitionsBugfix, true),
- ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS(
- Flags::enableCompatUiVisibilityStatus, true),
- ENABLE_DESKTOP_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE_BUGFIX(
- Flags::skipCompatUiEducationInDesktopMode, true),
- INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC(
- Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true),
ENABLE_DESKTOP_WINDOWING_HSUM(Flags::enableDesktopWindowingHsum, true),
+ ENABLE_DESKTOP_WINDOWING_MODALS_POLICY(Flags::enableDesktopWindowingModalsPolicy, true),
+ ENABLE_DESKTOP_WINDOWING_MODE(Flags::enableDesktopWindowingMode, true),
+ ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES(
+ Flags::enableDesktopWindowingMultiInstanceFeatures, true),
+ ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, true),
+ ENABLE_DESKTOP_WINDOWING_QUICK_SWITCH(Flags::enableDesktopWindowingQuickSwitch, true),
+ ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS(Flags::enableDesktopWindowingSizeConstraints, true),
+ ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS(Flags::enableDesktopWindowingTaskbarRunningApps,
+ true),
+ ENABLE_DESKTOP_WINDOWING_TASK_LIMIT(Flags::enableDesktopWindowingTaskLimit, true),
+ ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY(Flags::enableDesktopWindowingWallpaperActivity,
+ true),
+ ENABLE_HANDLE_INPUT_FIX(Flags::enableHandleInputFix, true),
+ ENABLE_HOLD_TO_DRAG_APP_HANDLE(Flags::enableHoldToDragAppHandle, true),
ENABLE_MINIMIZE_BUTTON(Flags::enableMinimizeButton, true),
ENABLE_RESIZING_METRICS(Flags::enableResizingMetrics, true),
ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS(Flags::enableTaskResizingKeyboardShortcuts, true),
- ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER(
- Flags::enableDesktopWallpaperActivityForSystemUser, true),
- ENABLE_TOP_VISIBLE_ROOT_TASK_PER_USER_TRACKING(
- Flags::enableTopVisibleRootTaskPerUserTracking, true),
- ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX(
- Flags::enableDesktopRecentsTransitionsCornersBugfix, false),
- ENABLE_DESKTOP_SYSTEM_DIALOGS_TRANSITIONS(Flags::enableDesktopSystemDialogsTransitions, true),
- ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES(
- Flags::enableDesktopWindowingMultiInstanceFeatures, true);
+ ENABLE_TASK_STACK_OBSERVER_IN_SHELL(Flags::enableTaskStackObserverInShell, true),
+ ENABLE_THEMED_APP_HEADERS(Flags::enableThemedAppHeaders, true),
+ ENABLE_TILE_RESIZING(Flags::enableTileResizing, true),
+ ENABLE_TOP_VISIBLE_ROOT_TASK_PER_USER_TRACKING(Flags::enableTopVisibleRootTaskPerUserTracking,
+ true),
+ ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS(Flags::enableWindowingDynamicInitialBounds, true),
+ ENABLE_WINDOWING_EDGE_DRAG_RESIZE(Flags::enableWindowingEdgeDragResize, true),
+ ENABLE_WINDOWING_SCALED_RESIZING(Flags::enableWindowingScaledResizing, true),
+ ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS(
+ Flags::enableWindowingTransitionHandlersObservers, false),
+ INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC(
+ Flags::includeTopTransparentFullscreenTaskInDesktopHeuristic, true)
+ // go/keep-sorted end
+ ;
/**
* Flag class, to be used in case the enum cannot be used because the flag is not accessible.
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index d20b06738f8c..6634ee0e1020 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -513,6 +513,13 @@ flag {
}
flag {
+ name: "enable_taskbar_connected_displays"
+ namespace: "lse_desktop_experience"
+ description: "Enables connected displays in taskbar."
+ bug: "393398093"
+}
+
+flag {
name: "enable_bug_fixes_for_secondary_display"
namespace: "lse_desktop_experience"
description: "Bugfixes / papercuts to bring Desktop Windowing to secondary displays."
@@ -610,3 +617,27 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "enable_desktop_taskbar_on_freeform_displays"
+ namespace: "lse_desktop_experience"
+ description: "Forces pinned taskbar with desktop tasks on freeform displays"
+ bug: "390665752"
+}
+
+flag {
+ name: "enable_presentation_for_connected_displays"
+ namespace: "lse_desktop_experience"
+ description: "Enables full support of presentation API for connected displays."
+ bug: "378503083"
+}
+
+flag {
+ name: "enable_full_screen_window_on_removing_split_screen_stage_bugfix"
+ namespace: "lse_desktop_experience"
+ description: "Enables clearing the windowing mode of a freeform window when removing the task from the split screen stage."
+ bug: "372791604"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 1b946afd506c..6f8852daae5f 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -185,3 +185,14 @@ flag {
description: "Enables letterboxing for a safe region"
bug: "380132497"
}
+
+flag {
+ namespace: "windowing_sdk"
+ name: "fix_layout_existing_task"
+ description: "Layout the existing task to ensure the bounds are updated."
+ bug: "390291971"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+} \ No newline at end of file
diff --git a/core/java/com/android/internal/content/NativeLibraryHelper.java b/core/java/com/android/internal/content/NativeLibraryHelper.java
index e170d6652863..8c64750b66e4 100644
--- a/core/java/com/android/internal/content/NativeLibraryHelper.java
+++ b/core/java/com/android/internal/content/NativeLibraryHelper.java
@@ -85,6 +85,8 @@ public class NativeLibraryHelper {
final boolean extractNativeLibs;
final boolean debuggable;
+ final boolean pageSizeCompatDisabled;
+
public static Handle create(File packageFile) throws IOException {
final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
final ParseResult<PackageLite> ret = ApkLiteParseUtils.parsePackageLite(input.reset(),
@@ -97,12 +99,15 @@ public class NativeLibraryHelper {
}
public static Handle create(PackageLite lite) throws IOException {
+ boolean isPageSizeCompatDisabled = lite.getPageSizeCompat()
+ == ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_DISABLED;
return create(lite.getAllApkPaths(), lite.isMultiArch(), lite.isExtractNativeLibs(),
- lite.isDebuggable());
+ lite.isDebuggable(), isPageSizeCompatDisabled);
}
public static Handle create(List<String> codePaths, boolean multiArch,
- boolean extractNativeLibs, boolean debuggable) throws IOException {
+ boolean extractNativeLibs, boolean debuggable, boolean isPageSizeCompatDisabled)
+ throws IOException {
final int size = codePaths.size();
final String[] apkPaths = new String[size];
final long[] apkHandles = new long[size];
@@ -119,7 +124,8 @@ public class NativeLibraryHelper {
}
}
- return new Handle(apkPaths, apkHandles, multiArch, extractNativeLibs, debuggable);
+ return new Handle(apkPaths, apkHandles, multiArch, extractNativeLibs, debuggable,
+ isPageSizeCompatDisabled);
}
public static Handle createFd(PackageLite lite, FileDescriptor fd) throws IOException {
@@ -130,17 +136,21 @@ public class NativeLibraryHelper {
throw new IOException("Unable to open APK " + path + " from fd " + fd);
}
+ boolean isPageSizeCompatDisabled = lite.getPageSizeCompat()
+ == ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_DISABLED;
+
return new Handle(new String[]{path}, apkHandles, lite.isMultiArch(),
- lite.isExtractNativeLibs(), lite.isDebuggable());
+ lite.isExtractNativeLibs(), lite.isDebuggable(), isPageSizeCompatDisabled);
}
Handle(String[] apkPaths, long[] apkHandles, boolean multiArch,
- boolean extractNativeLibs, boolean debuggable) {
+ boolean extractNativeLibs, boolean debuggable, boolean isPageSizeCompatDisabled) {
this.apkPaths = apkPaths;
this.apkHandles = apkHandles;
this.multiArch = multiArch;
this.extractNativeLibs = extractNativeLibs;
this.debuggable = debuggable;
+ this.pageSizeCompatDisabled = isPageSizeCompatDisabled;
mGuard.open("close");
}
@@ -175,7 +185,8 @@ public class NativeLibraryHelper {
private static native long nativeSumNativeBinaries(long handle, String cpuAbi);
private native static int nativeCopyNativeBinaries(long handle, String sharedLibraryPath,
- String abiToCopy, boolean extractNativeLibs, boolean debuggable);
+ String abiToCopy, boolean extractNativeLibs, boolean debuggable,
+ boolean pageSizeCompatDisabled);
private static native int nativeCheckAlignment(
long handle,
@@ -203,7 +214,7 @@ public class NativeLibraryHelper {
public static int copyNativeBinaries(Handle handle, File sharedLibraryDir, String abi) {
for (long apkHandle : handle.apkHandles) {
int res = nativeCopyNativeBinaries(apkHandle, sharedLibraryDir.getPath(), abi,
- handle.extractNativeLibs, handle.debuggable);
+ handle.extractNativeLibs, handle.debuggable, handle.pageSizeCompatDisabled);
if (res != INSTALL_SUCCEEDED) {
return res;
}
diff --git a/core/java/com/android/internal/notification/NotificationChannelGroupsHelper.java b/core/java/com/android/internal/notification/NotificationChannelGroupsHelper.java
new file mode 100644
index 000000000000..2bfb19f10807
--- /dev/null
+++ b/core/java/com/android/internal/notification/NotificationChannelGroupsHelper.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.notification;
+
+import static android.app.NotificationChannel.SYSTEM_RESERVED_IDS;
+import static android.app.NotificationManager.IMPORTANCE_NONE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.INotificationManager;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.service.notification.Flags;
+import android.util.ArrayMap;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * NotificationChannelGroupHelper contains helper methods for associating channels with the groups
+ * they belong to, matching by ID.
+ */
+public class NotificationChannelGroupsHelper {
+ /**
+ * Set of parameters passed into
+ * {@link NotificationChannelGroupsHelper#getGroupsWithChannels(Collection, Map, Params)}.
+ *
+ * @param includeDeleted Whether to include deleted channels.
+ * @param includeNonGrouped Whether to include channels that are not associated with a group.
+ * @param includeEmpty Whether to include groups containing no channels.
+ * @param includeAllBlockedWithFilter Whether to include channels that are blocked from
+ * sending notifications along with channels specified by
+ * the filter. This setting only takes effect when
+ * channelFilter is not {@code null}, and if true will
+ * include all blocked channels in the output (regardless
+ * of whether they are included in the filter).
+ * @param channelFilter If non-null, a specific set of channels to include. If a channel
+ * matching this filter is blocked, it will still be included even
+ * if includeAllBlockedWithFilter=false.
+ */
+ public record Params(
+ boolean includeDeleted,
+ boolean includeNonGrouped,
+ boolean includeEmpty,
+ boolean includeAllBlockedWithFilter,
+ Set<String> channelFilter
+ ) {
+ /**
+ * Default set of parameters used to specify the behavior of
+ * {@link INotificationManager#getNotificationChannelGroups(String)}. This will include
+ * output for all groups, including those without channels, but not any ungrouped channels.
+ */
+ public static Params forAllGroups() {
+ return new Params(
+ /* includeDeleted= */ false,
+ /* includeNonGrouped= */ false,
+ /* includeEmpty= */ true,
+ /* includeAllBlockedWithFilter= */ true,
+ /* channelFilter= */ null);
+ }
+
+ /**
+ * Parameters to get groups for all channels, including those not associated with any groups
+ * and optionally including deleted channels as well. Channels not associated with a group
+ * are returned inside a group with id {@code null}.
+ *
+ * @param includeDeleted Whether to include deleted channels.
+ */
+ public static Params forAllChannels(boolean includeDeleted) {
+ return new Params(
+ includeDeleted,
+ /* includeNonGrouped= */ true,
+ /* includeEmpty= */ false,
+ /* includeAllBlockedWithFilter= */ true,
+ /* channelFilter= */ null);
+ }
+
+ /**
+ * Parameters to collect groups only for channels specified by the channel filter, as well
+ * as any blocked channels (independent of whether they exist in the filter).
+ * @param channelFilter Specific set of channels to return.
+ */
+ public static Params onlySpecifiedOrBlockedChannels(Set<String> channelFilter) {
+ return new Params(
+ /* includeDeleted= */ false,
+ /* includeNonGrouped= */ true,
+ /* includeEmpty= */ false,
+ /* includeAllBlockedWithFilter= */ true,
+ channelFilter);
+ }
+ }
+
+ /**
+ * Retrieve the {@link NotificationChannelGroup} object specified by the given groupId, if it
+ * exists, with the list of channels filled in from the provided available channels.
+ *
+ * @param groupId The ID of the group to return.
+ * @param allChannels A list of all channels associated with the package.
+ * @param allGroups A map of group ID -> NotificationChannelGroup objects.
+ */
+ public static @Nullable NotificationChannelGroup getGroupWithChannels(@NonNull String groupId,
+ @NonNull Collection<NotificationChannel> allChannels,
+ @NonNull Map<String, NotificationChannelGroup> allGroups,
+ boolean includeDeleted) {
+ NotificationChannelGroup group = null;
+ if (allGroups.containsKey(groupId)) {
+ group = allGroups.get(groupId).clone();
+ group.setChannels(new ArrayList<>());
+ for (NotificationChannel nc : allChannels) {
+ if (includeDeleted || !nc.isDeleted()) {
+ if (groupId.equals(nc.getGroup())) {
+ group.addChannel(nc);
+ }
+ }
+ }
+ }
+ return group;
+ }
+
+ /**
+ * Returns a list of groups with their associated channels filled in.
+ *
+ * @param allChannels All available channels that may be associated with these groups.
+ * @param allGroups Map of group ID -> {@link NotificationChannelGroup} objects.
+ * @param params Params indicating which channels and which groups to include.
+ */
+ public static @NonNull List<NotificationChannelGroup> getGroupsWithChannels(
+ @NonNull Collection<NotificationChannel> allChannels,
+ @NonNull Map<String, NotificationChannelGroup> allGroups,
+ Params params) {
+ Map<String, NotificationChannelGroup> outputGroups = new ArrayMap<>();
+ NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
+ for (NotificationChannel nc : allChannels) {
+ boolean includeChannel = (params.includeDeleted || !nc.isDeleted())
+ && (params.channelFilter == null
+ || (params.includeAllBlockedWithFilter
+ && nc.getImportance() == IMPORTANCE_NONE)
+ || params.channelFilter.contains(nc.getId()))
+ && (!Flags.notificationClassification()
+ || !SYSTEM_RESERVED_IDS.contains(nc.getId()));
+ if (includeChannel) {
+ if (nc.getGroup() != null) {
+ if (allGroups.get(nc.getGroup()) != null) {
+ NotificationChannelGroup ncg = outputGroups.get(nc.getGroup());
+ if (ncg == null) {
+ ncg = allGroups.get(nc.getGroup()).clone();
+ ncg.setChannels(new ArrayList<>());
+ outputGroups.put(nc.getGroup(), ncg);
+ }
+ ncg.addChannel(nc);
+ }
+ } else {
+ nonGrouped.addChannel(nc);
+ }
+ }
+ }
+ if (params.includeNonGrouped && nonGrouped.getChannels().size() > 0) {
+ outputGroups.put(null, nonGrouped);
+ }
+ if (params.includeEmpty) {
+ for (NotificationChannelGroup group : allGroups.values()) {
+ if (!outputGroups.containsKey(group.getId())) {
+ outputGroups.put(group.getId(), group);
+ }
+ }
+ }
+ return new ArrayList<>(outputGroups.values());
+ }
+}
diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
index 5c08dc6be1a0..db60e12e50b1 100644
--- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
@@ -29,7 +29,6 @@ import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_
import static android.os.Build.VERSION_CODES.DONUT;
import static android.os.Build.VERSION_CODES.O;
import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
-import static android.sdk.Flags.majorMinorVersioningScheme;
import static com.android.internal.pm.pkg.parsing.ParsingUtils.parseKnownActivityEmbeddingCerts;
@@ -1690,21 +1689,6 @@ public class ParsingPackageUtils {
targetCode = minCode;
}
- if (majorMinorVersioningScheme()) {
- val = sa.peekValue(R.styleable.AndroidManifestUsesSdk_minSdkVersionFull);
- if (val != null) {
- if (val.type == TypedValue.TYPE_STRING && val.string != null) {
- String minSdkVersionFullString = val.string.toString();
- ParseResult<Void> minSdkVersionFullResult =
- FrameworkParsingPackageUtils.verifyMinSdkVersionFull(
- minSdkVersionFullString, Build.VERSION.SDK_INT_FULL, input);
- if (minSdkVersionFullResult.isError()) {
- return input.error(minSdkVersionFullResult);
- }
- }
- }
- }
-
if (isApkInApex) {
val = sa.peekValue(R.styleable.AndroidManifestUsesSdk_maxSdkVersion);
if (val != null) {
diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
index e78c5247d8a7..06fd80e37669 100644
--- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
+++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
@@ -273,6 +273,7 @@ static install_status_t copyFileIfChanged(JNIEnv* env, void* arg, ZipFileRO* zip
jboolean extractNativeLibs = *(jboolean*)args[1];
jboolean debuggable = *(jboolean*)args[2];
jboolean app_compat_16kb = *(jboolean*)args[3];
+ jboolean pageSizeCompatDisabled = *(jboolean*)args[4];
install_status_t ret = INSTALL_SUCCEEDED;
ScopedUtfChars nativeLibPath(env, *javaNativeLibPath);
@@ -304,6 +305,16 @@ static install_status_t copyFileIfChanged(JNIEnv* env, void* arg, ZipFileRO* zip
}
if (offset % kPageSize != 0) {
+ // If page size app compat was disabled explicitly in manifest, don't extract libs on
+ // 16 KB page size device.
+ if (kPageSize == 0x4000 && pageSizeCompatDisabled) {
+ ALOGE("pageSizeCompat=disabled library '%s' is not PAGE(%zu)-"
+ "aligned within apk (APK alignment, not ELF alignment) -"
+ "and will not be extracted.\n",
+ fileName, kPageSize);
+ return INSTALL_FAILED_INVALID_APK;
+ }
+
// If the library is zip-aligned correctly for 4kb devices and app compat is
// enabled, on 16kb devices fallback to extraction
if (offset % 0x1000 == 0 && app_compat_16kb) {
@@ -537,13 +548,12 @@ static inline bool app_compat_16kb_enabled() {
return !android::base::GetBoolProperty("pm.16kb.app_compat.disabled", false);
}
-static jint
-com_android_internal_content_NativeLibraryHelper_copyNativeBinaries(JNIEnv *env, jclass clazz,
- jlong apkHandle, jstring javaNativeLibPath, jstring javaCpuAbi,
- jboolean extractNativeLibs, jboolean debuggable)
-{
+static jint com_android_internal_content_NativeLibraryHelper_copyNativeBinaries(
+ JNIEnv* env, jclass clazz, jlong apkHandle, jstring javaNativeLibPath, jstring javaCpuAbi,
+ jboolean extractNativeLibs, jboolean debuggable, jboolean pageSizeCompatDisabled) {
jboolean app_compat_16kb = app_compat_16kb_enabled();
- void* args[] = { &javaNativeLibPath, &extractNativeLibs, &debuggable, &app_compat_16kb };
+ void* args[] = {&javaNativeLibPath, &extractNativeLibs, &debuggable, &app_compat_16kb,
+ &pageSizeCompatDisabled};
return (jint) iterateOverNativeFiles(env, apkHandle, javaCpuAbi,
copyFileIfChanged, reinterpret_cast<void*>(args));
}
@@ -804,7 +814,7 @@ static const JNINativeMethod gMethods[] = {
{"nativeOpenApkFd", "(Ljava/io/FileDescriptor;Ljava/lang/String;)J",
(void*)com_android_internal_content_NativeLibraryHelper_openApkFd},
{"nativeClose", "(J)V", (void*)com_android_internal_content_NativeLibraryHelper_close},
- {"nativeCopyNativeBinaries", "(JLjava/lang/String;Ljava/lang/String;ZZ)I",
+ {"nativeCopyNativeBinaries", "(JLjava/lang/String;Ljava/lang/String;ZZZ)I",
(void*)com_android_internal_content_NativeLibraryHelper_copyNativeBinaries},
{"nativeSumNativeBinaries", "(JLjava/lang/String;)J",
(void*)com_android_internal_content_NativeLibraryHelper_sumNativeBinaries},
diff --git a/core/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto
index 407790c89202..a673ad7dfb34 100644
--- a/core/proto/android/server/windowmanagerservice.proto
+++ b/core/proto/android/server/windowmanagerservice.proto
@@ -399,6 +399,7 @@ message ActivityRecordProto {
optional bool should_override_force_resize_app = 44;
optional bool should_enable_user_aspect_ratio_settings = 45;
optional bool is_user_fullscreen_override_enabled = 46;
+ optional int64 request_open_in_browser_education_timestamp = 47;
}
/* represents WindowToken */
diff --git a/core/proto/android/widget/remoteviews.proto b/core/proto/android/widget/remoteviews.proto
index 6a987a475711..91dbf7b54534 100644
--- a/core/proto/android/widget/remoteviews.proto
+++ b/core/proto/android/widget/remoteviews.proto
@@ -57,6 +57,7 @@ message RemoteViewsProto {
repeated bytes bitmap_cache = 14;
optional RemoteCollectionCache remote_collection_cache = 15;
repeated Action actions = 16;
+ optional int32 uid = 17;
message RemoteCollectionCache {
message Entry {
diff --git a/core/res/res/drawable/accessibility_autoclick_button_rounded_background.xml b/core/res/res/drawable/accessibility_autoclick_button_rounded_background.xml
new file mode 100644
index 000000000000..6d0c2653e5f2
--- /dev/null
+++ b/core/res/res/drawable/accessibility_autoclick_button_rounded_background.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/materialColorSurfaceContainer" />
+ <corners android:radius="24dp" />
+</shape>
diff --git a/core/res/res/drawable/accessibility_autoclick_left_click.xml b/core/res/res/drawable/accessibility_autoclick_left_click.xml
new file mode 100644
index 000000000000..64c8efbe24b3
--- /dev/null
+++ b/core/res/res/drawable/accessibility_autoclick_left_click.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="18dp"
+ android:height="18dp"
+ android:viewportWidth="18"
+ android:viewportHeight="18">
+ <path
+ android:pathData="M5.7 12C4.1 11.9167 2.75 11.3 1.65 10.15C0.55 9 0 7.61667 0 6C0 4.33333 0.583333 2.91667 1.75 1.75C2.91667 0.583332 4.33333 -0.00000143051 6 -0.00000143051C7.61667 -0.00000143051 9 0.549999 10.15 1.65C11.3 2.75 11.9167 4.1 12 5.7L9.9 5.075C9.68333 4.175 9.21667 3.44167 8.5 2.875C7.78333 2.29167 6.95 2 6 2C4.9 2 3.95833 2.39167 3.175 3.175C2.39167 3.95833 2 4.9 2 6C2 6.95 2.28333 7.78333 2.85 8.5C3.43333 9.21667 4.175 9.68333 5.075 9.9L5.7 12ZM14.525 16.5L10.25 12.225L9 16L6 6L16 9L12.225 10.25L16.5 14.525L14.525 16.5Z"
+ android:fillColor="@color/materialColorPrimary" />
+</vector>
diff --git a/core/res/res/drawable/accessibility_autoclick_pause.xml b/core/res/res/drawable/accessibility_autoclick_pause.xml
new file mode 100644
index 000000000000..5251b2afed0d
--- /dev/null
+++ b/core/res/res/drawable/accessibility_autoclick_pause.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="@color/materialColorPrimary"
+ android:pathData="M6,19h4V5H6v14zm8,-14v14h4V5h-4z" />
+</vector>
diff --git a/core/res/res/drawable/accessibility_autoclick_position.xml b/core/res/res/drawable/accessibility_autoclick_position.xml
new file mode 100644
index 000000000000..8c98235e7d99
--- /dev/null
+++ b/core/res/res/drawable/accessibility_autoclick_position.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="21dp"
+ android:height="17dp"
+ android:viewportWidth="21"
+ android:viewportHeight="17">
+ <path
+ android:pathData="M0.400024 2.99961C0.400024 1.67413 1.47454 0.599609 2.80002 0.599609H17.2C18.5255 0.599609 19.6 1.67413 19.6 2.99961V14.9996C19.6 16.3251 18.5255 17.3996 17.2 17.3996H2.80002C1.47454 17.3996 0.400024 16.3251 0.400024 14.9996V2.99961ZM2.80002 2.99961H17.2V14.9996H2.80002V2.99961ZM10.6 10.1996C9.60591 10.1996 8.80002 11.0055 8.80002 11.9996C8.80002 12.9937 9.60591 13.7996 10.6 13.7996H14.2C15.1941 13.7996 16 12.9937 16 11.9996C16 11.0055 15.1941 10.1996 14.2 10.1996H10.6Z"
+ android:fillType="evenOdd"
+ android:fillColor="@color/materialColorPrimary" />
+</vector>
diff --git a/core/res/res/drawable/accessibility_autoclick_type_panel_rounded_background.xml b/core/res/res/drawable/accessibility_autoclick_type_panel_rounded_background.xml
new file mode 100644
index 000000000000..e367ba55ae7d
--- /dev/null
+++ b/core/res/res/drawable/accessibility_autoclick_type_panel_rounded_background.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="@color/materialColorSurface" />
+ <corners android:radius="40dp" />
+</shape>
diff --git a/core/res/res/layout/accessibility_autoclick_type_panel.xml b/core/res/res/layout/accessibility_autoclick_type_panel.xml
new file mode 100644
index 000000000000..9aa47cc8d68b
--- /dev/null
+++ b/core/res/res/layout/accessibility_autoclick_type_panel.xml
@@ -0,0 +1,78 @@
+<?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.
+*/
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/accessibility_autoclick_type_panel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:background="@drawable/accessibility_autoclick_type_panel_rounded_background"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:id="@+id/accessibility_autoclick_left_click_layout"
+ style="@style/AccessibilityAutoclickPanelButtonLayoutStyle"
+ android:layout_marginEnd="@dimen/accessibility_autoclick_type_panel_button_spacing">
+
+ <ImageButton
+ android:id="@+id/accessibility_autoclick_left_click_button"
+ style="@style/AccessibilityAutoclickPanelImageButtonStyle"
+ android:contentDescription="@string/accessibility_autoclick_left_click"
+ android:src="@drawable/accessibility_autoclick_left_click" />
+ </LinearLayout>
+
+ <View
+ android:layout_width="@dimen/accessibility_autoclick_type_panel_divider_width"
+ android:layout_height="@dimen/accessibility_autoclick_type_panel_divider_height"
+ android:layout_marginEnd="@dimen/accessibility_autoclick_type_panel_button_spacing"
+ android:background="@color/materialColorSurfaceContainer" />
+
+ <LinearLayout
+ android:id="@+id/accessibility_autoclick_pause_layout"
+ style="@style/AccessibilityAutoclickPanelButtonLayoutStyle"
+ android:layout_marginEnd="@dimen/accessibility_autoclick_type_panel_button_spacing">
+
+ <ImageButton
+ android:id="@+id/accessibility_autoclick_pause_button"
+ style="@style/AccessibilityAutoclickPanelImageButtonStyle"
+ android:contentDescription="@string/accessibility_autoclick_pause"
+ android:src="@drawable/accessibility_autoclick_pause" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/accessibility_autoclick_position_layout"
+ style="@style/AccessibilityAutoclickPanelButtonLayoutStyle">
+
+ <ImageButton
+ android:id="@+id/accessibility_autoclick_position_button"
+ style="@style/AccessibilityAutoclickPanelImageButtonStyle"
+ android:contentDescription="@string/accessibility_autoclick_position"
+ android:src="@drawable/accessibility_autoclick_position" />
+ </LinearLayout>
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml b/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml
new file mode 100644
index 000000000000..11fc48668ad7
--- /dev/null
+++ b/core/res/res/layout/notification_2025_template_compact_heads_up_base.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/status_bar_latest_event_content"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/notification_header_height"
+ android:clipChildren="false"
+ android:tag="compactHUN"
+ android:gravity="center_vertical"
+ android:theme="@style/Theme.DeviceDefault.Notification"
+ android:importantForAccessibility="no">
+ <com.android.internal.widget.NotificationRowIconView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/notification_icon_circle_size"
+ android:layout_height="@dimen/notification_icon_circle_size"
+ android:layout_gravity="center_vertical|start"
+ android:layout_marginStart="@dimen/notification_icon_circle_start"
+ android:background="@drawable/notification_icon_circle"
+ android:padding="@dimen/notification_icon_circle_padding"
+ android:maxDrawableWidth="@dimen/notification_icon_circle_size"
+ android:maxDrawableHeight="@dimen/notification_icon_circle_size"
+ />
+ <FrameLayout
+ android:id="@+id/alternate_expand_target"
+ android:layout_width="@dimen/notification_content_margin_start"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ android:importantForAccessibility="no"
+ android:focusable="false"
+ />
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginStart="@dimen/notification_content_margin_start"
+ android:orientation="horizontal"
+ >
+ <NotificationTopLineView
+ android:id="@+id/notification_top_line"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:layout_weight="1"
+ android:clipChildren="false"
+ android:gravity="center_vertical"
+ android:theme="@style/Theme.DeviceDefault.Notification"
+ >
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/notification_header_separating_margin"
+ android:ellipsize="end"
+ android:fadingEdge="horizontal"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
+ android:textSize="@dimen/notification_2025_title_text_size"
+ />
+ <include layout="@layout/notification_2025_top_line_views" />
+ </NotificationTopLineView>
+ <FrameLayout
+ android:id="@+id/expand_button_touch_container"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:minWidth="@dimen/notification_content_margin_end"
+ >
+ <include layout="@layout/notification_2025_expand_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|end"
+ />
+ </FrameLayout>
+ </LinearLayout>
+</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml
new file mode 100644
index 000000000000..dd70087f7785
--- /dev/null
+++ b/core/res/res/layout/notification_2025_template_compact_heads_up_messaging.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<com.android.internal.widget.CompactMessagingLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/status_bar_latest_event_content"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/notification_header_height"
+ android:clipChildren="false"
+ android:tag="compactMessagingHUN"
+ android:gravity="center_vertical"
+ android:theme="@style/Theme.DeviceDefault.Notification"
+ android:importantForAccessibility="no">
+ <com.android.internal.widget.NotificationRowIconView
+ android:id="@+id/icon"
+ android:layout_width="@dimen/notification_icon_circle_size"
+ android:layout_height="@dimen/notification_icon_circle_size"
+ android:layout_gravity="center_vertical|start"
+ android:layout_marginStart="@dimen/notification_icon_circle_start"
+ android:background="@drawable/notification_icon_circle"
+ android:padding="@dimen/notification_icon_circle_padding"
+ android:maxDrawableWidth="@dimen/notification_icon_circle_size"
+ android:maxDrawableHeight="@dimen/notification_icon_circle_size"
+ />
+ <com.android.internal.widget.CachingIconView
+ android:id="@+id/conversation_icon"
+ android:layout_width="@dimen/notification_icon_circle_size"
+ android:layout_height="@dimen/notification_icon_circle_size"
+ android:layout_gravity="center_vertical|start"
+ android:layout_marginStart="@dimen/notification_icon_circle_start"
+ android:background="@drawable/notification_icon_circle"
+ android:clipToOutline="true"
+ android:maxDrawableWidth="@dimen/notification_icon_circle_size"
+ android:maxDrawableHeight="@dimen/notification_icon_circle_size"
+ android:scaleType="centerCrop"
+ android:importantForAccessibility="no"
+ />
+ <ViewStub
+ android:layout="@layout/conversation_face_pile_layout"
+ android:layout_gravity="center_vertical|start"
+ android:layout_width="@dimen/conversation_compact_face_pile_size"
+ android:layout_height="@dimen/conversation_compact_face_pile_size"
+ android:layout_marginStart="@dimen/notification_icon_circle_start"
+ android:id="@+id/conversation_face_pile"
+ />
+ <FrameLayout
+ android:id="@+id/alternate_expand_target"
+ android:layout_width="@dimen/notification_content_margin_start"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ android:importantForAccessibility="no"
+ android:focusable="false"
+ />
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginStart="@dimen/notification_content_margin_start"
+ android:orientation="horizontal"
+ >
+ <NotificationTopLineView
+ android:id="@+id/notification_top_line"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_centerVertical="true"
+ android:layout_weight="1"
+ android:clipChildren="false"
+ android:gravity="center_vertical"
+ android:theme="@style/Theme.DeviceDefault.Notification"
+ >
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/notification_header_separating_margin"
+ android:ellipsize="end"
+ android:fadingEdge="horizontal"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title"
+ android:textSize="@dimen/notification_2025_title_text_size"
+ />
+ <include layout="@layout/notification_2025_top_line_views" />
+ </NotificationTopLineView>
+ <FrameLayout
+ android:id="@+id/reply_action_container"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/notification_action_list_height"
+ android:gravity="center_vertical"
+ android:orientation="horizontal" />
+ <FrameLayout
+ android:id="@+id/expand_button_touch_container"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:minWidth="@dimen/notification_content_margin_end"
+ >
+ <include layout="@layout/notification_2025_expand_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|end"
+ />
+ </FrameLayout>
+ </LinearLayout>
+</com.android.internal.widget.CompactMessagingLayout>
diff --git a/core/res/res/layout/notification_2025_template_expanded_messaging.xml b/core/res/res/layout/notification_2025_template_expanded_messaging.xml
index 7f5a36b5f865..177706c6d58d 100644
--- a/core/res/res/layout/notification_2025_template_expanded_messaging.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_messaging.xml
@@ -36,14 +36,13 @@
android:clipChildren="false"
android:orientation="vertical">
- <!-- Note: the top margin is being set in code based on the estimated space needed for
- the header text. -->
<com.android.internal.widget.RemeasuringLinearLayout
android:id="@+id/notification_main_column"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:layout_weight="1"
+ android:layout_marginTop="@dimen/notification_2025_header_height"
android:layout_marginEnd="@dimen/notification_content_margin_end"
android:orientation="vertical"
android:clipChildren="false"
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 8c6fd1dfc47e..3edc5c108083 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -2572,10 +2572,6 @@
against a development branch, in which case it will only work against
the development builds. -->
<attr name="minSdkVersion" format="integer|string" />
- <!-- This is the minimum SDK major and minor version (e.g. "36.1") that
- the application requires. Verified independently of minSdkVersion.
- @FlaggedApi(android.sdk.Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME) -->
- <attr name="minSdkVersionFull" format="string" />
<!-- This is the SDK version number that the application is targeting.
It is able to run on older versions (down to minSdkVersion), but
was explicitly tested to work with the version specified here.
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index a49e03484192..9f731fe04472 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5958,6 +5958,9 @@
<!-- <item>com.google</item> -->
</string-array>
+ <!-- Whether to restrict the accounts that raw contacts can be created in. -->
+ <bool name = "config_rawContactsAccountRestrictionEnabled">true</bool>
+
<!-- Whether or not to use assistant stream volume separately from music volume -->
<bool name="config_useAssistantVolume">false</bool>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 5644cf9dd61d..484e8ef1e049 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -714,6 +714,18 @@
<!-- The minimum window size of the accessibility window magnifier -->
<dimen name="accessibility_window_magnifier_min_size">122dp</dimen>
+ <!-- The accessibility autoclick panel button spacing -->
+ <dimen name="accessibility_autoclick_type_panel_button_spacing">12dp</dimen>
+
+ <!-- The accessibility autoclick panel button width and height -->
+ <dimen name="accessibility_autoclick_type_panel_button_size">36dp</dimen>
+
+ <!-- The accessibility autoclick panel divider width -->
+ <dimen name="accessibility_autoclick_type_panel_divider_width">1dp</dimen>
+
+ <!-- The accessibility autoclick panel divider height -->
+ <dimen name="accessibility_autoclick_type_panel_divider_height">24dp</dimen>
+
<!-- Margin around the various security views -->
<dimen name="keyguard_muliuser_selector_margin">8dp</dimen>
diff --git a/core/res/res/values/public-final.xml b/core/res/res/values/public-final.xml
index d8e89318a134..af1e5123096d 100644
--- a/core/res/res/values/public-final.xml
+++ b/core/res/res/values/public-final.xml
@@ -3953,8 +3953,7 @@
<public name="pageSizeCompat" />
<!-- @FlaggedApi(android.nfc.Flags.FLAG_NFC_ASSOCIATED_ROLE_SERVICES) -->
<public name="wantsRoleHolderPriority"/>
- <!-- @FlaggedApi(android.sdk.Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME) -->
- <public name="minSdkVersionFull"/>
+ <public name="removed_"/>
<public name="removed_" />
<public name="removed_" />
<public name="removed_" />
@@ -3980,8 +3979,6 @@
<public type="attr" name="pageSizeCompat" id="0x010106ab" />
<!-- @FlaggedApi(android.nfc.Flags.FLAG_NFC_ASSOCIATED_ROLE_SERVICES) -->
<public type="attr" name="wantsRoleHolderPriority" id="0x010106ac" />
- <!-- @FlaggedApi(android.sdk.Flags.FLAG_MAJOR_MINOR_VERSIONING_SCHEME) -->
- <public type="attr" name="minSdkVersionFull" id="0x010106ad" />
<staging-public-group-final type="string" first-id="0x01b40000">
<!-- @FlaggedApi(android.content.pm.Flags.FLAG_SDK_DEPENDENCY_INSTALLER)
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index fa4c21de682e..9399e2b9824e 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6149,6 +6149,16 @@
<!-- Label for Dpad center action [CHAR LIMIT=NONE] -->
<string name="accessibility_system_action_dpad_center_label">Dpad Center</string>
+ <!-- Accessibility autoclick related strings -->
+ <!-- Label for autoclick type settings panel [CHAR LIMIT=NONE] -->
+ <string name="accessibility_autoclick_type_settings_panel_title">Autoclick type settings panel</string>
+ <!-- Label for autoclick left click button [CHAR LIMIT=NONE] -->
+ <string name="accessibility_autoclick_left_click">Left click</string>
+ <!-- Label for autoclick pause button [CHAR LIMIT=NONE] -->
+ <string name="accessibility_autoclick_pause">Pause</string>
+ <!-- Label for autoclick position button [CHAR LIMIT=NONE] -->
+ <string name="accessibility_autoclick_position">Position</string>
+
<!-- Text to tell the user that a package has been forced by themselves in the RESTRICTED bucket. [CHAR LIMIT=NONE] -->
<string name="as_app_forced_to_restricted_bucket">
<xliff:g id="package_name" example="com.android.example">%1$s</xliff:g> has been put into the RESTRICTED bucket</string>
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index 579dc91d2ca1..ee1edda838fd 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1745,6 +1745,22 @@ please see styles_device_defaults.xml.
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style>
+ <style name="AccessibilityAutoclickPanelButtonLayoutStyle">
+ <item name="android:gravity">center</item>
+ <item name="android:background">@drawable/accessibility_autoclick_button_rounded_background</item>
+ <item name="android:layout_width">@dimen/accessibility_autoclick_type_panel_button_size</item>
+ <item name="android:layout_height">@dimen/accessibility_autoclick_type_panel_button_size</item>
+ </style>
+
+ <style name="AccessibilityAutoclickPanelImageButtonStyle">
+ <item name="android:gravity">center</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:scaleType">center</item>
+ <item name="android:background">@android:color/transparent</item>
+ <item name="android:tint">@color/materialColorPrimary</item>
+ </style>
+
<!--
TODO(b/309578419): Make activities go edge-to-edge properly and then remove this.
-->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 8bb3c995cf9f..f4004fa70623 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2412,6 +2412,8 @@
<java-symbol type="layout" name="notification_2025_template_collapsed_base" />
<java-symbol type="layout" name="notification_2025_template_expanded_base" />
<java-symbol type="layout" name="notification_2025_template_heads_up_base" />
+ <java-symbol type="layout" name="notification_2025_template_compact_heads_up_base" />
+ <java-symbol type="layout" name="notification_2025_template_compact_heads_up_messaging" />
<java-symbol type="layout" name="notification_2025_template_header" />
<java-symbol type="layout" name="notification_2025_template_collapsed_messaging" />
<java-symbol type="layout" name="notification_2025_template_collapsed_media" />
@@ -4615,6 +4617,7 @@
<java-symbol type="string" name="config_rawContactsLocalAccountName" />
<java-symbol type="string" name="config_rawContactsLocalAccountType" />
<java-symbol type="array" name="config_rawContactsEligibleDefaultAccountTypes" />
+ <java-symbol type="bool" name="config_rawContactsAccountRestrictionEnabled" />
<!-- For App Standby -->
<java-symbol type="string" name="as_app_forced_to_restricted_bucket" />
@@ -5605,6 +5608,24 @@
<java-symbol type="bool" name="config_enable_a11y_fullscreen_magnification_overscroll_handler" />
<java-symbol type="dimen" name="accessibility_fullscreen_magnification_gesture_edge_slop" />
+ <!-- Accessibility autoclick related -->
+ <java-symbol type="layout" name="accessibility_autoclick_type_panel" />
+ <java-symbol type="string" name="accessibility_autoclick_type_settings_panel_title" />
+ <java-symbol type="string" name="accessibility_autoclick_left_click" />
+ <java-symbol type="string" name="accessibility_autoclick_pause" />
+ <java-symbol type="string" name="accessibility_autoclick_position" />
+ <java-symbol type="dimen" name="accessibility_autoclick_type_panel_button_spacing" />
+ <java-symbol type="dimen" name="accessibility_autoclick_type_panel_button_size" />
+ <java-symbol type="dimen" name="accessibility_autoclick_type_panel_divider_height" />
+ <java-symbol type="dimen" name="accessibility_autoclick_type_panel_divider_width" />
+ <java-symbol type="id" name="accessibility_autoclick_type_panel" />
+ <java-symbol type="id" name="accessibility_autoclick_left_click_layout" />
+ <java-symbol type="id" name="accessibility_autoclick_left_click_button" />
+ <java-symbol type="id" name="accessibility_autoclick_pause_layout" />
+ <java-symbol type="id" name="accessibility_autoclick_pause_button" />
+ <java-symbol type="id" name="accessibility_autoclick_position_layout" />
+ <java-symbol type="id" name="accessibility_autoclick_position_button" />
+
<!-- For HapticFeedbackConstants configurability defined at HapticFeedbackCustomization -->
<java-symbol type="string" name="config_hapticFeedbackCustomizationFile" />
<java-symbol type="xml" name="haptic_feedback_customization" />
diff --git a/core/tests/coretests/src/android/os/PerfettoTraceTest.java b/core/tests/coretests/src/android/os/PerfettoTraceTest.java
index 0b5a44665d2b..91efacf0dcc3 100644
--- a/core/tests/coretests/src/android/os/PerfettoTraceTest.java
+++ b/core/tests/coretests/src/android/os/PerfettoTraceTest.java
@@ -59,6 +59,8 @@ import perfetto.protos.TrackEventOuterClass.TrackEvent;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
/**
* This class is used to test the native tracing support. Run this test
@@ -77,6 +79,8 @@ public class PerfettoTraceTest {
private static final String BAR = "bar";
private static final Category FOO_CATEGORY = new Category(FOO);
+ private static final int MESSAGE = 1234567;
+ private static final int MESSAGE_COUNT = 3;
private final Set<String> mCategoryNames = new ArraySet<>();
private final Set<String> mEventNames = new ArraySet<>();
@@ -592,6 +596,89 @@ public class PerfettoTraceTest {
assertThat(mDebugAnnotationNames).doesNotContain("before");
}
+ @Test
+ @RequiresFlagsEnabled(android.os.Flags.FLAG_PERFETTO_SDK_TRACING_V2)
+ public void testMessageQueue() throws Exception {
+ TraceConfig traceConfig = getTraceConfig("mq");
+
+ PerfettoTrace.MQ_CATEGORY.register();
+ final HandlerThread thread = new HandlerThread("test");
+ thread.start();
+ final Handler handler = thread.getThreadHandler();
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ PerfettoTrace.Session session = new PerfettoTrace.Session(true,
+ getTraceConfig("mq").toByteArray());
+
+ handler.sendEmptyMessage(MESSAGE);
+ handler.sendEmptyMessageDelayed(MESSAGE, 10);
+ handler.sendEmptyMessage(MESSAGE);
+ handler.postDelayed(() -> {
+ latch.countDown();
+ }, 10);
+ assertThat(latch.await(100, TimeUnit.MILLISECONDS)).isTrue();
+
+ byte[] traceBytes = session.close();
+
+ Trace trace = Trace.parseFrom(traceBytes);
+
+ boolean hasTrackEvent = false;
+ int instantCount = 0;
+ int counterCount = 0;
+ int beginCount = 0;
+ int endCount = 0;
+
+ Set<Long> flowIds = new ArraySet<>();
+ for (TracePacket packet: trace.getPacketList()) {
+ TrackEvent event;
+ if (packet.hasTrackEvent()) {
+ hasTrackEvent = true;
+ event = packet.getTrackEvent();
+ } else {
+ continue;
+ }
+
+ List<DebugAnnotation> annotations = event.getDebugAnnotationsList();
+ switch (event.getType()) {
+ case TrackEvent.Type.TYPE_INSTANT:
+ if (annotations.get(2).getIntValue() == MESSAGE) {
+ instantCount++;
+ assertThat(annotations.get(0).getStringValue()).isEqualTo("test");
+ assertThat(event.getFlowIdsCount()).isEqualTo(1);
+ flowIds.addAll(event.getFlowIdsList());
+ }
+ break;
+ case TrackEvent.Type.TYPE_COUNTER:
+ counterCount++;
+ break;
+ case TrackEvent.Type.TYPE_SLICE_BEGIN:
+ annotations = event.getDebugAnnotationsList();
+ if (flowIds.containsAll(event.getTerminatingFlowIdsList())) {
+ beginCount++;
+ assertThat(event.getTerminatingFlowIdsCount()).isEqualTo(1);
+ }
+ break;
+ case TrackEvent.Type.TYPE_SLICE_END:
+ endCount++;
+ break;
+ default:
+ break;
+ }
+ collectInternedData(packet);
+ }
+
+ assertThat(hasTrackEvent).isTrue();
+ assertThat(mCategoryNames).contains("mq");
+ assertThat(mEventNames).contains("message_queue_send");
+ assertThat(mEventNames).contains("message_queue_receive");
+ assertThat(mDebugAnnotationNames).contains("what");
+ assertThat(mDebugAnnotationNames).contains("delay");
+ assertThat(instantCount).isEqualTo(MESSAGE_COUNT);
+ assertThat(beginCount).isEqualTo(MESSAGE_COUNT);
+ assertThat(endCount).isAtLeast(MESSAGE_COUNT);
+ assertThat(counterCount).isAtLeast(MESSAGE_COUNT);
+ }
+
private TrackEvent getTrackEvent(Trace trace, int idx) {
int curIdx = 0;
for (TracePacket packet: trace.getPacketList()) {
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index c40137f1bd34..a289df0441e5 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -16,6 +16,8 @@
package android.view;
+import static android.app.UiModeManager.MODE_NIGHT_NO;
+import static android.app.UiModeManager.MODE_NIGHT_YES;
import static android.util.SequenceUtils.getInitSeq;
import static android.view.HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING;
import static android.view.InputDevice.SOURCE_ROTARY_ENCODER;
@@ -67,8 +69,10 @@ import static org.junit.Assume.assumeTrue;
import android.annotation.NonNull;
import android.app.Instrumentation;
import android.app.UiModeManager;
+import android.app.UiModeManager.ForceInvertType;
import android.content.Context;
import android.graphics.ForceDarkType;
+import android.graphics.ForceDarkType.ForceDarkTypeDef;
import android.graphics.Rect;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Binder;
@@ -93,9 +97,12 @@ import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.compatibility.common.util.ShellIdentityUtils;
+import com.android.compatibility.common.util.TestUtils;
import com.android.cts.input.BlockingQueueEventVerifier;
import com.android.window.flags.Flags;
+import com.google.common.truth.Expect;
+
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.AfterClass;
@@ -124,6 +131,8 @@ public class ViewRootImplTest {
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ @Rule
+ public final Expect mExpect = Expect.create();
private ViewRootImpl mViewRootImpl;
private View mView;
@@ -1507,49 +1516,34 @@ public class ViewRootImplTest {
}
@Test
- public void forceInvertOffDarkThemeOff_forceDarkModeDisabled() {
- mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR);
- ShellIdentityUtils.invokeWithShellPermissions(() -> {
- Settings.Secure.putInt(
- sContext.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
- /* value= */ 0
- );
- var uiModeManager = sContext.getSystemService(UiModeManager.class);
- uiModeManager.setNightMode(UiModeManager.MODE_NIGHT_NO);
- });
-
- sInstrumentation.runOnMainSync(() ->
- mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())
- );
-
- assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.NONE);
- }
-
- @Test
- public void forceInvertOnDarkThemeOff_forceDarkModeEnabled() {
- mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR);
- ShellIdentityUtils.invokeWithShellPermissions(() -> {
- Settings.Secure.putInt(
- sContext.getContentResolver(),
- Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
- /* value= */ 1
- );
- var uiModeManager = sContext.getSystemService(UiModeManager.class);
- uiModeManager.setNightMode(UiModeManager.MODE_NIGHT_NO);
- });
-
- sInstrumentation.runOnMainSync(() ->
- mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())
- );
-
- assertThat(mViewRootImpl.determineForceDarkType())
- .isEqualTo(ForceDarkType.FORCE_INVERT_COLOR_DARK);
+ @RequiresFlagsEnabled(FLAG_FORCE_INVERT_COLOR)
+ public void updateConfiguration_returnsExpectedForceDarkMode() {
+ waitForSystemNightModeActivated(true);
+
+ verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ true,
+ UiModeManager.FORCE_INVERT_TYPE_DARK, ForceDarkType.FORCE_INVERT_COLOR_DARK);
+ verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ false,
+ UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
+ verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ true,
+ UiModeManager.FORCE_INVERT_TYPE_DARK, ForceDarkType.FORCE_INVERT_COLOR_DARK);
+ verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ false,
+ UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
+
+ waitForSystemNightModeActivated(false);
+
+ verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ true,
+ UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
+ verifyForceDarkType(/* isAppInNightMode= */ true, /* isForceInvertEnabled= */ false,
+ UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
+ verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ true,
+ UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
+ verifyForceDarkType(/* isAppInNightMode= */ false, /* isForceInvertEnabled= */ false,
+ UiModeManager.FORCE_INVERT_TYPE_OFF, ForceDarkType.NONE);
}
@Test
+ @EnableFlags(FLAG_FORCE_INVERT_COLOR)
public void forceInvertOffForceDarkOff_forceDarkModeDisabled() {
- mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR);
ShellIdentityUtils.invokeWithShellPermissions(() -> {
Settings.Secure.putInt(
sContext.getContentResolver(),
@@ -1562,15 +1556,14 @@ public class ViewRootImplTest {
});
sInstrumentation.runOnMainSync(() ->
- mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())
- );
+ mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()));
assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.NONE);
}
@Test
+ @EnableFlags(FLAG_FORCE_INVERT_COLOR)
public void forceInvertOffForceDarkOn_forceDarkModeEnabled() {
- mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR);
ShellIdentityUtils.invokeWithShellPermissions(() -> {
Settings.Secure.putInt(
sContext.getContentResolver(),
@@ -1582,8 +1575,7 @@ public class ViewRootImplTest {
});
sInstrumentation.runOnMainSync(() ->
- mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId())
- );
+ mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()));
assertThat(mViewRootImpl.determineForceDarkType()).isEqualTo(ForceDarkType.FORCE_DARK);
}
@@ -1790,4 +1782,39 @@ public class ViewRootImplTest {
() -> view.getViewTreeObserver().removeOnDrawListener(listener));
}
}
+
+ private void waitForSystemNightModeActivated(boolean active) {
+ ShellIdentityUtils.invokeWithShellPermissions(() ->
+ sInstrumentation.runOnMainSync(() -> {
+ var uiModeManager = sContext.getSystemService(UiModeManager.class);
+ uiModeManager.setNightModeActivated(active);
+ }));
+ sInstrumentation.waitForIdleSync();
+ }
+
+ private void verifyForceDarkType(boolean isAppInNightMode, boolean isForceInvertEnabled,
+ @ForceInvertType int expectedForceInvertType,
+ @ForceDarkTypeDef int expectedForceDarkType) {
+ var uiModeManager = sContext.getSystemService(UiModeManager.class);
+ ShellIdentityUtils.invokeWithShellPermissions(() -> {
+ uiModeManager.setApplicationNightMode(
+ isAppInNightMode ? MODE_NIGHT_YES : MODE_NIGHT_NO);
+ Settings.Secure.putInt(
+ sContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
+ isForceInvertEnabled ? 1 : 0);
+ });
+
+ sInstrumentation.runOnMainSync(() ->
+ mViewRootImpl.updateConfiguration(sContext.getDisplayNoVerify().getDisplayId()));
+ try {
+ TestUtils.waitUntil("Waiting for force invert state changed",
+ () -> (uiModeManager.getForceInvertState() == expectedForceInvertType));
+ } catch (Exception e) {
+ Log.e(TAG, "Unexpected error trying to apply force invert state. " + e);
+ e.printStackTrace();
+ }
+
+ mExpect.that(mViewRootImpl.determineForceDarkType()).isEqualTo(expectedForceDarkType);
+ }
}
diff --git a/core/tests/coretests/src/com/android/internal/notification/NotificationChannelGroupsHelperTest.java b/core/tests/coretests/src/com/android/internal/notification/NotificationChannelGroupsHelperTest.java
new file mode 100644
index 000000000000..26e96ea79e4b
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/notification/NotificationChannelGroupsHelperTest.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.notification;
+
+import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
+import static android.app.NotificationManager.IMPORTANCE_NONE;
+
+import static com.android.internal.notification.NotificationChannelGroupsHelper.getGroupWithChannels;
+import static com.android.internal.notification.NotificationChannelGroupsHelper.getGroupsWithChannels;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.notification.NotificationChannelGroupsHelper.Params;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationChannelGroupsHelperTest {
+ private Collection<NotificationChannel> mChannels;
+ private Map<String, NotificationChannelGroup> mGroups;
+
+ @Before
+ public void setUp() {
+ // Test data setup.
+ // Channels and their corresponding groups:
+ // * "regular": a channel that is not deleted or blocked. In group A.
+ // * "blocked": blocked channel. In group A.
+ // * "deleted": deleted channel. In group A.
+ // * "adrift": regular channel. No group.
+ // * "gone": deleted channel. No group.
+ // * "alternate": regular channel. In group B.
+ // * "another blocked": blocked channel. In group B.
+ // * "another deleted": deleted channel. In group C.
+ // * Additionally, there is an empty group D.
+ mChannels = List.of(makeChannel("regular", "a", false, false),
+ makeChannel("blocked", "a", true, false),
+ makeChannel("deleted", "a", false, true),
+ makeChannel("adrift", null, false, false),
+ makeChannel("gone", null, false, true),
+ makeChannel("alternate", "b", false, false),
+ makeChannel("anotherBlocked", "b", true, false),
+ makeChannel("anotherDeleted", "c", false, true));
+
+ mGroups = Map.of("a", new NotificationChannelGroup("a", "a"),
+ "b", new NotificationChannelGroup("b", "b"),
+ "c", new NotificationChannelGroup("c", "c"),
+ "d", new NotificationChannelGroup("d", "d"));
+ }
+
+ @Test
+ public void testGetGroup_noDeleted() {
+ NotificationChannelGroup res = getGroupWithChannels("a", mChannels, mGroups, false);
+ assertThat(res).isNotNull();
+ assertThat(res.getChannels()).hasSize(2); // "regular" & "blocked"
+ assertThat(res.getChannels()).containsExactlyElementsIn(List.of(
+ makeChannel("regular", "a", false, false),
+ makeChannel("blocked", "a", true, false)));
+ }
+
+ @Test
+ public void testGetGroup_includeDeleted() {
+ NotificationChannelGroup res = getGroupWithChannels("c", mChannels, mGroups, true);
+ assertThat(res).isNotNull();
+ assertThat(res.getChannels()).hasSize(1);
+ assertThat(res.getChannels().getFirst()).isEqualTo(
+ makeChannel("anotherDeleted", "c", false, true));
+ }
+
+ @Test
+ public void testGetGroup_empty() {
+ NotificationChannelGroup res = getGroupWithChannels("d", mChannels, mGroups, true);
+ assertThat(res).isNotNull();
+ assertThat(res.getChannels()).isEmpty();
+ }
+
+ @Test
+ public void testGetGroup_emptyBecauseNoChannelMatch() {
+ NotificationChannelGroup res = getGroupWithChannels("c", mChannels, mGroups, false);
+ assertThat(res).isNotNull();
+ assertThat(res.getChannels()).isEmpty();
+ }
+
+ @Test
+ public void testGetGroup_nonexistent() {
+ NotificationChannelGroup res = getGroupWithChannels("e", mChannels, mGroups, true);
+ assertThat(res).isNull();
+ }
+
+ @Test
+ public void testGetGroups_paramsForAllGroups() {
+ // deleted=false, nongrouped=false, empty=true, blocked=true, no channel filter
+ List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups,
+ Params.forAllGroups());
+
+ NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a");
+ expectedA.setChannels(List.of(
+ makeChannel("regular", "a", false, false),
+ makeChannel("blocked", "a", true, false)));
+
+ NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b");
+ expectedB.setChannels(List.of(
+ makeChannel("alternate", "b", false, false),
+ makeChannel("anotherBlocked", "b", true, false)));
+
+ NotificationChannelGroup expectedC = new NotificationChannelGroup("c", "c");
+ expectedC.setChannels(new ArrayList<>()); // empty, no deleted
+
+ NotificationChannelGroup expectedD = new NotificationChannelGroup("d", "d");
+ expectedD.setChannels(new ArrayList<>()); // empty
+
+ assertThat(res).containsExactly(expectedA, expectedB, expectedC, expectedD);
+ }
+
+ @Test
+ public void testGetGroups_paramsForAllChannels_noDeleted() {
+ // Excluding deleted channels to means group C is not included because it's "empty"
+ List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups,
+ Params.forAllChannels(false));
+
+ NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a");
+ expectedA.setChannels(List.of(
+ makeChannel("regular", "a", false, false),
+ makeChannel("blocked", "a", true, false)));
+
+ NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b");
+ expectedB.setChannels(List.of(
+ makeChannel("alternate", "b", false, false),
+ makeChannel("anotherBlocked", "b", true, false)));
+
+ NotificationChannelGroup expectedUngrouped = new NotificationChannelGroup(null, null);
+ expectedUngrouped.setChannels(List.of(
+ makeChannel("adrift", null, false, false),
+ makeChannel("gone", null, false, true)));
+
+ assertThat(res).containsExactly(expectedA, expectedB, expectedUngrouped);
+ }
+
+ @Test
+ public void testGetGroups_paramsForAllChannels_withDeleted() {
+ // This will get everything!
+ List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups,
+ Params.forAllChannels(true));
+
+ NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a");
+ expectedA.setChannels(List.of(
+ makeChannel("regular", "a", false, false),
+ makeChannel("blocked", "a", true, false),
+ makeChannel("deleted", "a", false, true)));
+
+ NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b");
+ expectedB.setChannels(List.of(
+ makeChannel("alternate", "b", false, false),
+ makeChannel("anotherBlocked", "b", true, false)));
+
+ NotificationChannelGroup expectedC = new NotificationChannelGroup("c", "c");
+ expectedC.setChannels(List.of(makeChannel("anotherDeleted", "c", false, true)));
+
+ // no D, because D is empty
+
+ NotificationChannelGroup expectedUngrouped = new NotificationChannelGroup(null, null);
+ expectedUngrouped.setChannels(List.of(makeChannel("adrift", null, false, false)));
+
+ assertThat(res).containsExactly(expectedA, expectedB, expectedC, expectedUngrouped);
+ }
+
+ @Test
+ public void testGetGroups_onlySpecifiedOrBlocked() {
+ Set<String> filter = Set.of("regular", "blocked", "adrift", "anotherDeleted");
+
+ // also not including deleted channels to check intersection of those params
+ List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups,
+ Params.onlySpecifiedOrBlockedChannels(filter));
+
+ NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a");
+ expectedA.setChannels(List.of(
+ makeChannel("regular", "a", false, false),
+ makeChannel("blocked", "a", true, false)));
+
+ // While nothing matches the filter from group B, includeBlocked=true means all blocked
+ // channels are included even if they are not in the filter.
+ NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b");
+ expectedB.setChannels(List.of(makeChannel("anotherBlocked", "b", true, false)));
+
+ NotificationChannelGroup expectedC = new NotificationChannelGroup("c", "c");
+ expectedC.setChannels(new ArrayList<>()); // deleted channel not included
+
+ NotificationChannelGroup expectedD = new NotificationChannelGroup("d", "d");
+ expectedD.setChannels(new ArrayList<>()); // empty
+
+ NotificationChannelGroup expectedUngrouped = new NotificationChannelGroup(null, null);
+ expectedUngrouped.setChannels(List.of(makeChannel("adrift", null, false, false)));
+
+ assertThat(res).containsExactly(expectedA, expectedB, expectedC, expectedD,
+ expectedUngrouped);
+ }
+
+
+ @Test
+ public void testGetGroups_noBlockedWithFilter() {
+ Set<String> filter = Set.of("regular", "blocked", "adrift");
+
+ // The includeBlocked setting only takes effect if there is a channel filter.
+ List<NotificationChannelGroup> res = getGroupsWithChannels(mChannels, mGroups,
+ new Params(true, true, true, false, filter));
+
+ // Even though includeBlocked=false, "blocked" is included because it's explicitly specified
+ // by the channel filter.
+ NotificationChannelGroup expectedA = new NotificationChannelGroup("a", "a");
+ expectedA.setChannels(List.of(
+ makeChannel("regular", "a", false, false),
+ makeChannel("blocked", "a", true, false)));
+
+ NotificationChannelGroup expectedB = new NotificationChannelGroup("b", "b");
+ expectedB.setChannels(new ArrayList<>()); // no matches; blocked channel not in filter
+
+ NotificationChannelGroup expectedC = new NotificationChannelGroup("c", "c");
+ expectedC.setChannels(new ArrayList<>()); // no matches
+
+ NotificationChannelGroup expectedD = new NotificationChannelGroup("d", "d");
+ expectedD.setChannels(new ArrayList<>()); // empty
+
+ NotificationChannelGroup expectedUngrouped = new NotificationChannelGroup(null, null);
+ expectedUngrouped.setChannels(List.of(makeChannel("adrift", null, false, false)));
+
+ assertThat(res).containsExactly(expectedA, expectedB, expectedC, expectedD,
+ expectedUngrouped);
+ }
+
+ private NotificationChannel makeChannel(String id, String groupId, boolean blocked,
+ boolean deleted) {
+ NotificationChannel c = new NotificationChannel(id, id,
+ blocked ? IMPORTANCE_NONE : IMPORTANCE_DEFAULT);
+ if (deleted) {
+ c.setDeleted(true);
+ }
+ if (groupId != null) {
+ c.setGroup(groupId);
+ }
+ return c;
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/notification/OWNERS b/core/tests/coretests/src/com/android/internal/notification/OWNERS
new file mode 100644
index 000000000000..396fd1213aca
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/notification/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/notification/OWNERS
diff --git a/core/tests/overlaytests/host/src/com/android/server/om/hosttest/InstallOverlayTests.java b/core/tests/overlaytests/host/src/com/android/server/om/hosttest/InstallOverlayTests.java
index 36c73e2e979e..c42ddd3412be 100644
--- a/core/tests/overlaytests/host/src/com/android/server/om/hosttest/InstallOverlayTests.java
+++ b/core/tests/overlaytests/host/src/com/android/server/om/hosttest/InstallOverlayTests.java
@@ -44,8 +44,11 @@ public class InstallOverlayTests extends BaseHostJUnit4Test {
"com.android.server.om.hosttest.update_overlay_test";
private static final String DEVICE_TEST_CLS = DEVICE_TEST_PKG + ".UpdateOverlayTest";
+ private int mCurrentUserid;
+
@Before
public void ensureNoOverlays() throws Exception {
+ mCurrentUserid = getDevice().getCurrentUser();
// Make sure we're starting with a clean slate.
for (String pkg : ALL_PACKAGES) {
assertFalse(pkg + " should not be installed", isPackageInstalled(pkg));
@@ -62,7 +65,7 @@ public class InstallOverlayTests extends BaseHostJUnit4Test {
@After
public void uninstallOverlays() throws Exception {
for (String pkg : ALL_PACKAGES) {
- uninstallPackage(pkg);
+ getDevice().uninstallPackageForUser(pkg, mCurrentUserid);
}
}
@@ -166,7 +169,7 @@ public class InstallOverlayTests extends BaseHostJUnit4Test {
installPackage("OverlayHostTests_AppOverlayV1.apk");
assertTrue(getDevice().executeShellCommand("cat /data/system/overlays.xml")
.contains(APP_OVERLAY_PACKAGE_NAME));
- uninstallPackage(APP_OVERLAY_PACKAGE_NAME);
+ getDevice().uninstallPackageForUser(APP_OVERLAY_PACKAGE_NAME, mCurrentUserid);
delay();
assertFalse(getDevice().executeShellCommand("cat /data/system/overlays.xml")
.contains(APP_OVERLAY_PACKAGE_NAME));
@@ -200,12 +203,12 @@ public class InstallOverlayTests extends BaseHostJUnit4Test {
}
private void installPackage(String pkg) throws Exception {
- super.installPackage(pkg);
+ super.installPackageAsUser(pkg, true /* grantPermission */, mCurrentUserid);
delay();
}
private void installInstantPackage(String pkg) throws Exception {
- super.installPackage(pkg, "--instant");
+ super.installPackageAsUser(pkg, true /* grantPermission */, mCurrentUserid, "--instant");
delay();
}
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 1edbffa9d572..1de8664231d7 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -259,6 +259,7 @@ applications that come with the platform
<permission name="android.permission.MODIFY_DAY_NIGHT_MODE"/>
<permission name="android.permission.ACCESS_LOWPAN_STATE"/>
<permission name="android.permission.BACKUP"/>
+ <permission name="android.permission.ENTER_TRADE_IN_MODE"/>
<!-- Needed for GMSCore Location API test only -->
<permission name="android.permission.LOCATION_BYPASS"/>
<!-- Needed for test only -->
diff --git a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml
index fd578a959e3b..95cd1c72a2af 100644
--- a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml
@@ -1,10 +1,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.wm.shell.multivalenttests">
+ <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/>
+
<application android:debuggable="true" android:supportsRtl="true" >
<uses-library android:name="android.test.runner" />
<activity android:name="com.android.wm.shell.bubbles.bar.BubbleBarAnimationHelperTest$TestActivity"
android:exported="true"/>
+
+ <activity android:name=".bubbles.TestActivity"
+ android:allowEmbedded="true"
+ android:documentLaunchMode="always"
+ android:excludeFromRecents="true"
+ android:exported="false"
+ android:resizeableActivity="true" />
</application>
<instrumentation
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt
index 09a93d501f8e..bce6c5999a75 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt
@@ -35,7 +35,6 @@ import com.android.internal.statusbar.IStatusBarService
import com.android.wm.shell.Flags
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
-import com.android.wm.shell.bubbles.properties.ProdBubbleProperties
import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayImeController
@@ -48,6 +47,7 @@ import com.android.wm.shell.draganddrop.DragAndDropController
import com.android.wm.shell.shared.TransactionPool
import com.android.wm.shell.shared.bubbles.BubbleBarLocation
import com.android.wm.shell.shared.bubbles.BubbleBarUpdate
+import com.android.wm.shell.shared.bubbles.DeviceConfig
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
@@ -288,7 +288,7 @@ class BubbleControllerBubbleBarTest {
mock<Transitions>(),
SyncTransactionQueue(TransactionPool(), mainExecutor),
mock<IWindowManager>(),
- ProdBubbleProperties,
+ BubbleResizabilityChecker()
)
}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerTest.kt
new file mode 100644
index 000000000000..cec67f26af92
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerTest.kt
@@ -0,0 +1,231 @@
+/*
+ * 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.bubbles
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.LauncherApps
+import android.content.pm.PackageManager
+import android.graphics.drawable.Icon
+import android.os.Handler
+import android.os.UserHandle
+import android.os.UserManager
+import android.view.IWindowManager
+import android.view.WindowManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.internal.protolog.ProtoLog
+import com.android.internal.statusbar.IStatusBarService
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
+import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayImeController
+import com.android.wm.shell.common.DisplayInsetsController
+import com.android.wm.shell.common.FloatingContentCoordinator
+import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.common.TaskStackListenerImpl
+import com.android.wm.shell.common.TestShellExecutor
+import com.android.wm.shell.common.TestSyncExecutor
+import com.android.wm.shell.draganddrop.DragAndDropController
+import com.android.wm.shell.recents.RecentTasksController
+import com.android.wm.shell.shared.TransactionPool
+import com.android.wm.shell.sysui.ShellCommandHandler
+import com.android.wm.shell.sysui.ShellController
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.taskview.TaskViewRepository
+import com.android.wm.shell.taskview.TaskViewTransitions
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.unfold.UnfoldAnimationController
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import java.util.Optional
+
+/** Tests for [BubbleControllerTest] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleControllerTest {
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+
+ private lateinit var bubbleController: BubbleController
+ private lateinit var bubblePositioner: BubblePositioner
+ private lateinit var uiEventLoggerFake: UiEventLoggerFake
+ private lateinit var bubbleLogger: BubbleLogger
+ private lateinit var mainExecutor: TestShellExecutor
+ private lateinit var bgExecutor: TestShellExecutor
+ private lateinit var bubbleData: BubbleData
+ private lateinit var eduController: BubbleEducationController
+
+ @Before
+ fun setUp() {
+ ProtoLog.REQUIRE_PROTOLOGTOOL = false
+ ProtoLog.init()
+
+ uiEventLoggerFake = UiEventLoggerFake()
+ bubbleLogger = BubbleLogger(uiEventLoggerFake)
+ eduController = BubbleEducationController(context)
+
+ mainExecutor = TestShellExecutor()
+ bgExecutor = TestShellExecutor()
+
+ // Tests don't have permission to add our window to windowManager, so we mock it :(
+ val windowManager = mock<WindowManager>()
+ val realWindowManager = context.getSystemService(WindowManager::class.java)
+ // But we do want the metrics from the real one
+ whenever(windowManager.currentWindowMetrics)
+ .thenReturn(realWindowManager.currentWindowMetrics)
+
+ bubblePositioner = BubblePositioner(context, windowManager)
+ bubblePositioner.setShowingInBubbleBar(true)
+
+ bubbleData = BubbleData(
+ context, bubbleLogger, bubblePositioner, eduController,
+ mainExecutor, bgExecutor
+ )
+
+ bubbleController =
+ createBubbleController(
+ bubbleData,
+ windowManager,
+ bubbleLogger,
+ bubblePositioner,
+ mainExecutor,
+ bgExecutor,
+ )
+ bubbleController.asBubbles().setSysuiProxy(Mockito.mock(SysuiProxy::class.java))
+ // Flush so that proxy gets set
+ mainExecutor.flushAll()
+ }
+
+ @After
+ fun tearDown() {
+ getInstrumentation().waitForIdleSync()
+ }
+
+ @Test
+ fun showOrHideNotesBubble_createsNoteBubble() {
+ val intent = Intent(context, TestActivity::class.java)
+ intent.setPackage(context.packageName)
+ val user = UserHandle.of(0)
+ val expectedKey = Bubble.getNoteBubbleKeyForApp(intent.getPackage(), user)
+
+ getInstrumentation().runOnMainSync {
+ bubbleController.showOrHideNotesBubble(intent, user, mock<Icon>())
+ }
+ getInstrumentation().waitForIdleSync()
+
+ assertThat(bubbleController.hasBubbles()).isTrue()
+ assertThat(bubbleData.getAnyBubbleWithKey(expectedKey)).isNotNull()
+ assertThat(bubbleData.getAnyBubbleWithKey(expectedKey)!!.isNoteBubble).isTrue()
+ }
+
+
+ fun createBubbleController(
+ bubbleData: BubbleData,
+ windowManager: WindowManager?,
+ bubbleLogger: BubbleLogger,
+ bubblePositioner: BubblePositioner,
+ mainExecutor: TestShellExecutor,
+ bgExecutor: TestShellExecutor,
+ ): BubbleController {
+ val shellInit = ShellInit(mainExecutor)
+ val shellCommandHandler = ShellCommandHandler()
+ val shellController =
+ ShellController(
+ context,
+ shellInit,
+ shellCommandHandler,
+ mock<DisplayInsetsController>(),
+ mainExecutor,
+ )
+ val surfaceSynchronizer = { obj: Runnable -> obj.run() }
+
+ val bubbleDataRepository =
+ BubbleDataRepository(
+ mock<LauncherApps>(),
+ mainExecutor,
+ bgExecutor,
+ BubblePersistentRepository(context),
+ )
+
+ val shellTaskOrganizer = ShellTaskOrganizer(
+ Mockito.mock<ShellInit>(ShellInit::class.java),
+ ShellCommandHandler(),
+ null,
+ Optional.empty<UnfoldAnimationController>(),
+ Optional.empty<RecentTasksController>(),
+ TestSyncExecutor()
+ )
+
+ val resizeChecker: ResizabilityChecker =
+ object : ResizabilityChecker {
+ override fun isResizableActivity(
+ intent: Intent?,
+ packageManager: PackageManager, key: String
+ ): Boolean {
+ return true
+ }
+ }
+
+ val bubbleController = BubbleController(
+ context,
+ shellInit,
+ shellCommandHandler,
+ shellController,
+ bubbleData,
+ surfaceSynchronizer,
+ FloatingContentCoordinator(),
+ bubbleDataRepository,
+ mock<IStatusBarService>(),
+ windowManager,
+ mock<DisplayInsetsController>(),
+ mock<DisplayImeController>(),
+ mock<UserManager>(),
+ mock<LauncherApps>(),
+ bubbleLogger,
+ mock<TaskStackListenerImpl>(),
+ shellTaskOrganizer,
+ bubblePositioner,
+ mock<DisplayController>(),
+ Optional.empty(),
+ mock<DragAndDropController>(),
+ mainExecutor,
+ mock<Handler>(),
+ bgExecutor,
+ mock<TaskViewRepository>(),
+ mock<TaskViewTransitions>(),
+ mock<Transitions>(),
+ SyncTransactionQueue(TransactionPool(), mainExecutor),
+ mock<IWindowManager>(),
+ resizeChecker,
+ )
+ bubbleController.setInflateSynchronously(true)
+ bubbleController.onInit()
+
+ return bubbleController
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
index 1d0c5057c77f..ec1add21ebf8 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
@@ -31,6 +31,7 @@ import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT
import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+import com.android.wm.shell.shared.bubbles.DeviceConfig
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
import org.junit.Before
@@ -154,19 +155,19 @@ class BubblePositionerTest {
/** Test that the default resting position on tablet is middle right. */
@Test
- fun testGetDefaultPosition_appBubble_onTablet() {
+ fun testGetDefaultPosition_noteBubble_onTablet() {
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true))
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
- val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */)
+ val startPosition = positioner.getDefaultStartPosition(true /* isNoteBubble */)
assertThat(startPosition.x).isEqualTo(allowableStackRegion.right)
assertThat(startPosition.y).isEqualTo(defaultYPosition)
}
@Test
- fun testGetRestingPosition_appBubble_onTablet_RTL() {
+ fun testGetRestingPosition_noteBubble_onTablet_RTL() {
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
- val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */)
+ val startPosition = positioner.getDefaultStartPosition(true /* isNoteBubble */)
assertThat(startPosition.x).isEqualTo(allowableStackRegion.left)
assertThat(startPosition.y).isEqualTo(defaultYPosition)
}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt
index f1ba0423b422..77aee98e6f6e 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt
@@ -35,7 +35,6 @@ import com.android.internal.protolog.ProtoLog
import com.android.internal.statusbar.IStatusBarService
import com.android.launcher3.icons.BubbleIconFactory
import com.android.wm.shell.ShellTaskOrganizer
-import com.android.wm.shell.bubbles.properties.BubbleProperties
import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayImeController
@@ -161,7 +160,7 @@ class BubbleViewInfoTaskTest {
mock<Transitions>(),
SyncTransactionQueue(TransactionPool(), mainExecutor),
mock<IWindowManager>(),
- mock<BubbleProperties>()
+ BubbleResizabilityChecker()
)
// TODO: (b/371829099) - when optional overflow is no longer flagged we can enable this
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleExpandedViewManager.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleExpandedViewManager.kt
index 3c013d3636e8..adcd835d72be 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleExpandedViewManager.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleExpandedViewManager.kt
@@ -38,7 +38,7 @@ class FakeBubbleExpandedViewManager(var bubbleBar: Boolean = false, var expanded
override fun dismissBubble(bubble: Bubble, reason: Int) {}
- override fun setAppBubbleTaskId(key: String, taskId: Int) {}
+ override fun setNoteBubbleTaskId(key: String, taskId: Int) {}
override fun isStackExpanded(): Boolean {
return expanded
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/TestActivity.kt
index 979cee9d63c2..40e80d02e7b3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/TestActivity.kt
@@ -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.
@@ -13,13 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.wm.shell.bubbles
-package com.android.wm.shell.compatui;
+import android.app.Activity
+import android.os.Bundle
+import android.widget.FrameLayout
-import com.android.wm.shell.ShellTestCase;
-
-/**
- * Base class for CompatUI tests.
- */
-public class CompatUIShellTestCase extends ShellTestCase {
+class TestActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(FrameLayout(getApplicationContext()))
+ }
}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt
index d3cfbd00c4a3..68b3d8822525 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt
@@ -42,10 +42,10 @@ import com.android.wm.shell.bubbles.BubbleLogger
import com.android.wm.shell.bubbles.BubbleOverflow
import com.android.wm.shell.bubbles.BubblePositioner
import com.android.wm.shell.bubbles.BubbleTaskView
-import com.android.wm.shell.bubbles.DeviceConfig
import com.android.wm.shell.bubbles.FakeBubbleExpandedViewManager
import com.android.wm.shell.bubbles.FakeBubbleFactory
import com.android.wm.shell.common.TestShellExecutor
+import com.android.wm.shell.shared.bubbles.DeviceConfig
import com.android.wm.shell.taskview.TaskView
import com.android.wm.shell.taskview.TaskViewController
import com.android.wm.shell.taskview.TaskViewTaskController
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt
index 7f65e22736b3..037bd227d33c 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt
@@ -39,11 +39,11 @@ import com.android.wm.shell.bubbles.BubbleLogger
import com.android.wm.shell.bubbles.BubblePositioner
import com.android.wm.shell.bubbles.BubbleTaskView
import com.android.wm.shell.bubbles.BubbleTaskViewFactory
-import com.android.wm.shell.bubbles.DeviceConfig
import com.android.wm.shell.bubbles.FakeBubbleExpandedViewManager
import com.android.wm.shell.bubbles.RegionSamplingProvider
import com.android.wm.shell.bubbles.UiEventSubject.Companion.assertThat
import com.android.wm.shell.common.TestShellExecutor
+import com.android.wm.shell.shared.bubbles.DeviceConfig
import com.android.wm.shell.shared.handles.RegionSamplingHelper
import com.android.wm.shell.taskview.TaskView
import com.android.wm.shell.taskview.TaskViewController
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
index a6492476176b..c022a298e972 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
@@ -43,13 +43,13 @@ import com.android.wm.shell.bubbles.BubbleDataRepository
import com.android.wm.shell.bubbles.BubbleExpandedViewManager
import com.android.wm.shell.bubbles.BubbleLogger
import com.android.wm.shell.bubbles.BubblePositioner
+import com.android.wm.shell.bubbles.BubbleResizabilityChecker
import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
import com.android.wm.shell.bubbles.FakeBubbleExpandedViewManager
import com.android.wm.shell.bubbles.FakeBubbleFactory
import com.android.wm.shell.bubbles.FakeBubbleTaskViewFactory
import com.android.wm.shell.bubbles.UiEventSubject.Companion.assertThat
import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix
-import com.android.wm.shell.bubbles.properties.BubbleProperties
import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayImeController
@@ -200,7 +200,7 @@ class BubbleBarLayerViewTest {
mock<Transitions>(),
SyncTransactionQueue(TransactionPool(), mainExecutor),
mock<IWindowManager>(),
- mock<BubbleProperties>(),
+ BubbleResizabilityChecker()
)
}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
index d4cbe6e10971..1b0e11f7103c 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
@@ -30,13 +30,13 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.BubblePositioner
-import com.android.wm.shell.bubbles.DeviceConfig
import com.android.wm.shell.shared.bubbles.BaseBubblePinController
import com.android.wm.shell.shared.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION
import com.android.wm.shell.shared.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION
import com.android.wm.shell.shared.bubbles.BubbleBarLocation
import com.android.wm.shell.shared.bubbles.BubbleBarLocation.LEFT
import com.android.wm.shell.shared.bubbles.BubbleBarLocation.RIGHT
+import com.android.wm.shell.shared.bubbles.DeviceConfig
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/common/TestSyncExecutor.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/common/TestSyncExecutor.kt
new file mode 100644
index 000000000000..50d9f77389c8
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/common/TestSyncExecutor.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common
+
+class TestSyncExecutor : ShellExecutor {
+ override fun execute(runnable: Runnable) {
+ runnable.run()
+ }
+
+ override fun executeDelayed(runnable: Runnable, delayMillis: Long) {
+ runnable.run()
+ }
+
+ override fun removeCallbacks(runnable: Runnable) {
+ }
+
+ override fun hasCallback(runnable: Runnable): Boolean {
+ return false
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 9e2d23b41556..404bbd1d0a33 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -270,6 +270,8 @@
<dimen name="bubble_bar_expanded_view_switch_offset">48dp</dimen>
<!-- Minimum width of the bubble bar manage menu. -->
<dimen name="bubble_bar_manage_menu_min_width">200dp</dimen>
+ <!-- The Bubble Bar drop zone square size. -->
+ <dimen name="bubble_bar_drop_zone_side_size">200dp</dimen>
<!-- Size of the dismiss icon in the bubble bar manage menu. -->
<dimen name="bubble_bar_manage_menu_dismiss_icon_size">16dp</dimen>
<!-- Padding of the bubble bar manage menu, provides space for menu shadows -->
diff --git a/libs/WindowManager/Shell/shared/Android.bp b/libs/WindowManager/Shell/shared/Android.bp
index 261c63948a94..af46ca298efe 100644
--- a/libs/WindowManager/Shell/shared/Android.bp
+++ b/libs/WindowManager/Shell/shared/Android.bp
@@ -74,6 +74,7 @@ java_library {
"**/desktopmode/*.kt",
],
static_libs: [
+ "WindowManager-Shell-shared-AOSP",
"com.android.window.flags.window-aconfig-java",
"wm_shell-shared-utils",
],
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DeviceConfig.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt
index 929330918174..f479da051e06 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DeviceConfig.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.bubbles
+package com.android.wm.shell.shared.bubbles
import android.content.Context
import android.content.res.Configuration
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
index e196880aad0f..deec52d1c19e 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java
@@ -18,6 +18,8 @@ package com.android.wm.shell.shared.desktopmode;
import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED;
+import static com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper.enableBubbleToFullscreen;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -206,8 +208,7 @@ public class DesktopModeStatus {
/**
* Return {@code true} if the current device supports desktop mode.
*/
- @VisibleForTesting
- public static boolean isDesktopModeSupported(@NonNull Context context) {
+ private static boolean isDesktopModeSupported(@NonNull Context context) {
return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported);
}
@@ -230,7 +231,7 @@ public class DesktopModeStatus {
* Return {@code true} if desktop mode dev option should be shown on current device
*/
public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) {
- return Flags.showDesktopExperienceDevOption();
+ return Flags.showDesktopExperienceDevOption() && isDeviceEligibleForDesktopMode(context);
}
/** Returns if desktop mode dev option should be enabled if there is no user override. */
@@ -270,7 +271,8 @@ public class DesktopModeStatus {
* necessarily enabling desktop mode
*/
public static boolean overridesShowAppHandle(@NonNull Context context) {
- return Flags.showAppHandleLargeScreens() && deviceHasLargeScreen(context);
+ return (Flags.showAppHandleLargeScreens() || enableBubbleToFullscreen())
+ && deviceHasLargeScreen(context);
}
/**
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 c40a276cb7bd..947dbd276d3a 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
@@ -72,12 +72,18 @@ import java.util.concurrent.Executor;
public class Bubble implements BubbleViewProvider {
private static final String TAG = "Bubble";
- /** A string suffix used in app bubbles' {@link #mKey}. */
+ /** A string prefix used in app bubbles' {@link #mKey}. */
public static final String KEY_APP_BUBBLE = "key_app_bubble";
+ /** A string prefix used in note bubbles' {@link #mKey}. */
+ public static final String KEY_NOTE_BUBBLE = "key_note_bubble";
+
/** Whether the bubble is an app bubble. */
private final boolean mIsAppBubble;
+ /** Whether the bubble is a notetaking bubble. */
+ private final boolean mIsNoteBubble;
+
private final String mKey;
@Nullable
private final String mGroupKey;
@@ -245,6 +251,7 @@ public class Bubble implements BubbleViewProvider {
mTaskId = taskId;
mBubbleMetadataFlagListener = listener;
mIsAppBubble = false;
+ mIsNoteBubble = false;
}
private Bubble(
@@ -252,6 +259,7 @@ public class Bubble implements BubbleViewProvider {
UserHandle user,
@Nullable Icon icon,
boolean isAppBubble,
+ boolean isNoteBubble,
String key,
@ShellMainThread Executor mainExecutor,
@ShellBackgroundThread Executor bgExecutor) {
@@ -261,6 +269,7 @@ public class Bubble implements BubbleViewProvider {
mUser = user;
mIcon = icon;
mIsAppBubble = isAppBubble;
+ mIsNoteBubble = isNoteBubble;
mKey = key;
mShowBubbleUpdateDot = false;
mMainExecutor = mainExecutor;
@@ -279,6 +288,7 @@ public class Bubble implements BubbleViewProvider {
mUser = info.getUserHandle();
mIcon = info.getIcon();
mIsAppBubble = false;
+ mIsNoteBubble = false;
mKey = getBubbleKeyForShortcut(info);
mShowBubbleUpdateDot = false;
mMainExecutor = mainExecutor;
@@ -303,6 +313,7 @@ public class Bubble implements BubbleViewProvider {
mUser = user;
mIcon = icon;
mIsAppBubble = true;
+ mIsNoteBubble = false;
mKey = key;
mShowBubbleUpdateDot = false;
mMainExecutor = mainExecutor;
@@ -313,6 +324,17 @@ public class Bubble implements BubbleViewProvider {
mPackageName = task.baseActivity.getPackageName();
}
+ /** Creates a notetaking bubble. */
+ public static Bubble createNotesBubble(Intent intent, UserHandle user, @Nullable Icon icon,
+ @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
+ return new Bubble(intent,
+ user,
+ icon,
+ /* isAppBubble= */ true,
+ /* isNoteBubble= */ true,
+ /* key= */ getNoteBubbleKeyForApp(intent.getPackage(), user),
+ mainExecutor, bgExecutor);
+ }
/** Creates an app bubble. */
public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon,
@@ -321,6 +343,7 @@ public class Bubble implements BubbleViewProvider {
user,
icon,
/* isAppBubble= */ true,
+ /* isNoteBubble= */ false,
/* key= */ getAppBubbleKeyForApp(intent.getPackage(), user),
mainExecutor, bgExecutor);
}
@@ -353,6 +376,16 @@ public class Bubble implements BubbleViewProvider {
}
/**
+ * Returns the key for a note bubble from an app with package name, {@code packageName} on an
+ * Android user, {@code user}.
+ */
+ public static String getNoteBubbleKeyForApp(String packageName, UserHandle user) {
+ Objects.requireNonNull(packageName);
+ Objects.requireNonNull(user);
+ return KEY_NOTE_BUBBLE + ":" + user.getIdentifier() + ":" + packageName;
+ }
+
+ /**
* Returns the key for a shortcut bubble using {@code packageName}, {@code user}, and the
* {@code shortcutInfo} id.
*/
@@ -375,6 +408,7 @@ public class Bubble implements BubbleViewProvider {
final Bubbles.PendingIntentCanceledListener intentCancelListener,
@ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) {
mIsAppBubble = false;
+ mIsNoteBubble = false;
mKey = entry.getKey();
mGroupKey = entry.getGroupKey();
mLocusId = entry.getLocusId();
@@ -1122,12 +1156,19 @@ public class Bubble implements BubbleViewProvider {
}
/**
- * Returns whether this bubble is from an app versus a notification.
+ * Returns whether this bubble is from an app (as well as notetaking) versus a notification.
*/
public boolean isAppBubble() {
return mIsAppBubble;
}
+ /**
+ * Returns whether this bubble is specific from the notetaking API.
+ */
+ public boolean isNoteBubble() {
+ return mIsNoteBubble;
+ }
+
/** Creates open app settings intent */
public Intent getSettingsIntent(final Context context) {
final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
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 e3f8e0c321a4..9120e0894ccf 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
@@ -49,7 +49,6 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.ActivityInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
@@ -94,8 +93,8 @@ import com.android.launcher3.icons.BubbleIconFactory;
import com.android.wm.shell.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.bubbles.bar.BubbleBarDragListener;
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
-import com.android.wm.shell.bubbles.properties.BubbleProperties;
import com.android.wm.shell.bubbles.shortcut.BubbleShortcutHelper;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
@@ -117,6 +116,7 @@ import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.bubbles.BubbleBarUpdate;
+import com.android.wm.shell.shared.bubbles.DeviceConfig;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
@@ -149,7 +149,8 @@ import java.util.function.IntConsumer;
* The controller manages addition, removal, and visible state of bubbles on screen.
*/
public class BubbleController implements ConfigurationChangeListener,
- RemoteCallable<BubbleController>, Bubbles.SysuiProxy.Provider {
+ RemoteCallable<BubbleController>, Bubbles.SysuiProxy.Provider,
+ BubbleBarDragListener {
private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES;
@@ -205,9 +206,9 @@ public class BubbleController implements ConfigurationChangeListener,
private final ShellController mShellController;
private final ShellCommandHandler mShellCommandHandler;
private final IWindowManager mWmService;
- private final BubbleProperties mBubbleProperties;
private final BubbleTaskViewFactory mBubbleTaskViewFactory;
private final BubbleExpandedViewManager mExpandedViewManager;
+ private final ResizabilityChecker mResizabilityChecker;
// Used to post to main UI thread
private final ShellExecutor mMainExecutor;
@@ -323,7 +324,7 @@ public class BubbleController implements ConfigurationChangeListener,
Transitions transitions,
SyncTransactionQueue syncQueue,
IWindowManager wmService,
- BubbleProperties bubbleProperties) {
+ ResizabilityChecker resizabilityChecker) {
mContext = context;
mShellCommandHandler = shellCommandHandler;
mShellController = shellController;
@@ -372,7 +373,6 @@ public class BubbleController implements ConfigurationChangeListener,
mDragAndDropController = dragAndDropController;
mSyncQueue = syncQueue;
mWmService = wmService;
- mBubbleProperties = bubbleProperties;
shellInit.addInitCallback(this::onInit, this);
mBubbleTaskViewFactory = new BubbleTaskViewFactory() {
@Override
@@ -385,6 +385,7 @@ public class BubbleController implements ConfigurationChangeListener,
}
};
mExpandedViewManager = BubbleExpandedViewManager.fromBubbleController(this);
+ mResizabilityChecker = resizabilityChecker;
}
private void registerOneHandedState(OneHandedController oneHanded) {
@@ -417,7 +418,6 @@ public class BubbleController implements ConfigurationChangeListener,
mBubbleData.setListener(mBubbleDataListener);
mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
mDataRepository.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
-
mBubbleData.setPendingIntentCancelledListener(bubble -> {
if (bubble.getBubbleIntent() == null) {
return;
@@ -590,8 +590,7 @@ public class BubbleController implements ConfigurationChangeListener,
* <p>If bubble bar is supported, bubble views will be updated to switch to bar mode.
*/
public void registerBubbleStateListener(Bubbles.BubbleStateListener listener) {
- mBubbleProperties.refresh();
- if (canShowAsBubbleBar() && listener != null) {
+ if (Flags.enableBubbleBar() && mBubblePositioner.isLargeScreen() && listener != null) {
// Only set the listener if we can show the bubble bar.
mBubbleStateListener = listener;
setUpBubbleViewsForMode();
@@ -608,7 +607,6 @@ public class BubbleController implements ConfigurationChangeListener,
* will be updated accordingly.
*/
public void unregisterBubbleStateListener() {
- mBubbleProperties.refresh();
if (mBubbleStateListener != null) {
mBubbleStateListener = null;
setUpBubbleViewsForMode();
@@ -766,14 +764,11 @@ public class BubbleController implements ConfigurationChangeListener,
}
}
- /** Whether bubbles are showing in the bubble bar. */
+ /** Whether bubbles would be shown with the bubble bar UI. */
public boolean isShowingAsBubbleBar() {
- return canShowAsBubbleBar() && mBubbleStateListener != null;
- }
-
- /** Whether the current configuration supports showing as bubble bar. */
- private boolean canShowAsBubbleBar() {
- return mBubbleProperties.isBubbleBarEnabled() && mBubblePositioner.isLargeScreen();
+ return Flags.enableBubbleBar()
+ && mBubblePositioner.isLargeScreen()
+ && mBubbleStateListener != null;
}
/**
@@ -782,7 +777,7 @@ public class BubbleController implements ConfigurationChangeListener,
*/
@Nullable
public BubbleBarLocation getBubbleBarLocation() {
- if (canShowAsBubbleBar()) {
+ if (isShowingAsBubbleBar()) {
return mBubblePositioner.getBubbleBarLocation();
}
return null;
@@ -793,7 +788,7 @@ public class BubbleController implements ConfigurationChangeListener,
*/
public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation,
@BubbleBarLocation.UpdateSource int source) {
- if (canShowAsBubbleBar()) {
+ if (isShowingAsBubbleBar()) {
BubbleBarLocation previousLocation = mBubblePositioner.getBubbleBarLocation();
mBubblePositioner.setBubbleBarLocation(bubbleBarLocation);
if (mLayerView != null && !mLayerView.isExpandedViewDragged()) {
@@ -845,11 +840,52 @@ public class BubbleController implements ConfigurationChangeListener,
* {@link #setBubbleBarLocation(BubbleBarLocation, int)}.
*/
public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
- if (canShowAsBubbleBar()) {
+ if (isShowingAsBubbleBar()) {
mBubbleStateListener.animateBubbleBarLocation(bubbleBarLocation);
}
}
+ @Override
+ public void onDragItemOverBubbleBarDragZone(@Nullable BubbleBarLocation bubbleBarLocation) {
+ if (bubbleBarLocation == null) return;
+ if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
+ //TODO(b/388894910) show expanded view drop
+ mBubbleStateListener.onDragItemOverBubbleBarDragZone(bubbleBarLocation);
+ }
+ }
+
+ @Override
+ public void onItemDraggedOutsideBubbleBarDropZone() {
+ if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
+ //TODO(b/388894910) hide expanded view drop
+ mBubbleStateListener.onItemDraggedOutsideBubbleBarDropZone();
+ }
+ }
+
+ @Override
+ public void onItemDroppedOverBubbleBarDragZone(@Nullable BubbleBarLocation bubbleBarLocation) {
+ if (bubbleBarLocation == null) return;
+ if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
+ //TODO(b/388894910) handle item drop with expandStackAndSelectBubble()
+ }
+ }
+
+ @Override
+ public Map<BubbleBarLocation, Rect> getBubbleBarDropZones(int l, int t, int r, int b) {
+ Map<BubbleBarLocation, Rect> result = new HashMap<>();
+ if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) {
+ // TODO(b/393172431) : Utilise DragZoneFactory once it is ready
+ final int bubbleBarDropZoneSideSize = getContext().getResources().getDimensionPixelSize(
+ R.dimen.bubble_bar_drop_zone_side_size);
+ int top = t - bubbleBarDropZoneSideSize;
+ result.put(BubbleBarLocation.LEFT,
+ new Rect(l, top, l + bubbleBarDropZoneSideSize, b));
+ result.put(BubbleBarLocation.RIGHT,
+ new Rect(r - bubbleBarDropZoneSideSize, top, r, b));
+ }
+ return result;
+ }
+
/** Whether this userId belongs to the current user. */
private boolean isCurrentProfile(int userId) {
return userId == UserHandle.USER_ALL
@@ -1560,78 +1596,80 @@ public class BubbleController implements ConfigurationChangeListener,
/**
* This method has different behavior depending on:
- * - if an app bubble exists
- * - if an app bubble is expanded
+ * - if a notes bubble exists
+ * - if a notes bubble is expanded
*
- * If no app bubble exists, this will add and expand a bubble with the provided intent. The
+ * If no notes bubble exists, this will add and expand a bubble with the provided intent. The
* intent must be explicit (i.e. include a package name or fully qualified component class name)
* and the activity for it should be resizable.
*
- * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is
- * expanded, calling this method will collapse it. If the app bubble is not expanded, calling
+ * If a notes bubble exists, this will toggle the visibility of it, i.e. if the notes bubble is
+ * expanded, calling this method will collapse it. If the notes bubble is not expanded, calling
* this method will expand it.
*
* These bubbles are <b>not</b> backed by a notification and remain until the user dismisses
* the bubble or bubble stack.
*
- * Some notes:
- * - Only one app bubble is supported at a time, regardless of users. Multi-users support is
- * tracked in b/273533235.
- * - Calling this method with a different intent than the existing app bubble will do nothing
+ * Some details:
+ * - Calling this method with a different intent than the existing bubble will do nothing
*
* @param intent the intent to display in the bubble expanded view.
* @param user the {@link UserHandle} of the user to start this activity for.
* @param icon the {@link Icon} to use for the bubble view.
*/
- public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) {
+ public void showOrHideNotesBubble(Intent intent, UserHandle user, @Nullable Icon icon) {
if (intent == null || intent.getPackage() == null) {
- Log.w(TAG, "App bubble failed to show, invalid intent: " + intent
+ Log.w(TAG, "Notes bubble failed to show, invalid intent: " + intent
+ ((intent != null) ? " with package: " + intent.getPackage() : " "));
return;
}
- String appBubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), user);
+ String noteBubbleKey = Bubble.getNoteBubbleKeyForApp(intent.getPackage(), user);
PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier());
- if (!isResizableActivity(intent, packageManager, appBubbleKey)) return; // logs errors
+ if (!mResizabilityChecker.isResizableActivity(intent, packageManager, noteBubbleKey)) {
+ // resize check logs any errors
+ return;
+ }
- Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(appBubbleKey);
+ Bubble existingNotebubble = mBubbleData.getBubbleInStackWithKey(noteBubbleKey);
ProtoLog.d(WM_SHELL_BUBBLES,
- "showOrHideAppBubble, key=%s existingAppBubble=%s stackVisibility=%s "
+ "showOrHideNotesBubble, key=%s existingAppBubble=%s stackVisibility=%s "
+ "statusBarShade=%s",
- appBubbleKey, existingAppBubble,
+ noteBubbleKey, existingNotebubble,
(mStackView != null ? mStackView.getVisibility() : "null"),
mIsStatusBarShade);
- if (existingAppBubble != null) {
+ if (existingNotebubble != null) {
BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble();
if (isStackExpanded()) {
- if (selectedBubble != null && appBubbleKey.equals(selectedBubble.getKey())) {
- ProtoLog.d(WM_SHELL_BUBBLES, "collapseStack for %s", appBubbleKey);
- // App bubble is expanded, lets collapse
+ if (selectedBubble != null && noteBubbleKey.equals(selectedBubble.getKey())) {
+ ProtoLog.d(WM_SHELL_BUBBLES, "collapseStack for %s", noteBubbleKey);
+ // Notes bubble is expanded, lets collapse
collapseStack();
} else {
- ProtoLog.d(WM_SHELL_BUBBLES, "setSelected for %s", appBubbleKey);
- // App bubble is not selected, select it
- mBubbleData.setSelectedBubble(existingAppBubble);
+ ProtoLog.d(WM_SHELL_BUBBLES, "setSelected for %s", noteBubbleKey);
+ // Notes bubble is not selected, select it
+ mBubbleData.setSelectedBubble(existingNotebubble);
}
} else {
- ProtoLog.d(WM_SHELL_BUBBLES, "setSelectedBubbleAndExpandStack %s", appBubbleKey);
- // App bubble is not selected, select it & expand
- mBubbleData.setSelectedBubbleAndExpandStack(existingAppBubble);
+ ProtoLog.d(WM_SHELL_BUBBLES, "setSelectedBubbleAndExpandStack %s", noteBubbleKey);
+ // Notes bubble is not selected, select it & expand
+ mBubbleData.setSelectedBubbleAndExpandStack(existingNotebubble);
}
} else {
// Check if it exists in the overflow
- Bubble b = mBubbleData.getOverflowBubbleWithKey(appBubbleKey);
+ Bubble b = mBubbleData.getOverflowBubbleWithKey(noteBubbleKey);
if (b != null) {
// It's in the overflow, so remove it & reinflate
- mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL);
+ mBubbleData.dismissBubbleWithKey(noteBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL);
// Update the bubble entry in the overflow with the latest intent.
b.setAppBubbleIntent(intent);
} else {
- // App bubble does not exist, lets add and expand it
- b = Bubble.createAppBubble(intent, user, icon, mMainExecutor, mBackgroundExecutor);
+ // Notes bubble does not exist, lets add and expand it
+ b = Bubble.createNotesBubble(intent, user, icon, mMainExecutor,
+ mBackgroundExecutor);
}
- ProtoLog.d(WM_SHELL_BUBBLES, "inflateAndAdd %s", appBubbleKey);
+ ProtoLog.d(WM_SHELL_BUBBLES, "inflateAndAdd %s", noteBubbleKey);
b.setShouldAutoExpand(true);
inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
}
@@ -1679,9 +1717,9 @@ public class BubbleController implements ConfigurationChangeListener,
}
}
- /** Sets the app bubble's taskId which is cached for SysUI. */
- public void setAppBubbleTaskId(String key, int taskId) {
- mImpl.mCachedState.setAppBubbleTaskId(key, taskId);
+ /** Sets the note bubble's taskId which is cached for SysUI. */
+ public void setNoteBubbleTaskId(String key, int taskId) {
+ mImpl.mCachedState.setNoteBubbleTaskId(key, taskId);
}
/**
@@ -2539,7 +2577,7 @@ public class BubbleController implements ConfigurationChangeListener,
* @param context the context to use.
* @param entry the entry to bubble.
*/
- static boolean canLaunchInTaskView(Context context, BubbleEntry entry) {
+ boolean canLaunchInTaskView(Context context, BubbleEntry entry) {
if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) return true;
PendingIntent intent = entry.getBubbleMetadata() != null
? entry.getBubbleMetadata().getIntent()
@@ -2554,26 +2592,8 @@ public class BubbleController implements ConfigurationChangeListener,
}
PackageManager packageManager = getPackageManagerForUser(
context, entry.getStatusBarNotification().getUser().getIdentifier());
- return isResizableActivity(intent.getIntent(), packageManager, entry.getKey());
- }
-
- static boolean isResizableActivity(Intent intent, PackageManager packageManager, String key) {
- if (intent == null) {
- Log.w(TAG, "Unable to send as bubble: " + key + " null intent");
- return false;
- }
- ActivityInfo info = intent.resolveActivityInfo(packageManager, 0);
- if (info == null) {
- Log.w(TAG, "Unable to send as bubble: " + key
- + " couldn't find activity info for intent: " + intent);
- return false;
- }
- if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
- Log.w(TAG, "Unable to send as bubble: " + key
- + " activity is not resizable for intent: " + intent);
- return false;
- }
- return true;
+ return mResizabilityChecker.isResizableActivity(intent.getIntent(), packageManager,
+ entry.getKey());
}
static PackageManager getPackageManagerForUser(Context context, int userId) {
@@ -2805,7 +2825,7 @@ public class BubbleController implements ConfigurationChangeListener,
private HashMap<String, String> mSuppressedGroupToNotifKeys = new HashMap<>();
private HashMap<String, Bubble> mShortcutIdToBubble = new HashMap<>();
- private HashMap<String, Integer> mAppBubbleTaskIds = new HashMap();
+ private HashMap<String, Integer> mNoteBubbleTaskIds = new HashMap();
private ArrayList<Bubble> mTmpBubbles = new ArrayList<>();
@@ -2837,20 +2857,20 @@ public class BubbleController implements ConfigurationChangeListener,
mSuppressedBubbleKeys.clear();
mShortcutIdToBubble.clear();
- mAppBubbleTaskIds.clear();
+ mNoteBubbleTaskIds.clear();
for (Bubble b : mTmpBubbles) {
mShortcutIdToBubble.put(b.getShortcutId(), b);
updateBubbleSuppressedState(b);
- if (b.isAppBubble()) {
- mAppBubbleTaskIds.put(b.getKey(), b.getTaskId());
+ if (b.isNoteBubble()) {
+ mNoteBubbleTaskIds.put(b.getKey(), b.getTaskId());
}
}
}
- /** Sets the app bubble's taskId which is cached for SysUI. */
- synchronized void setAppBubbleTaskId(String key, int taskId) {
- mAppBubbleTaskIds.put(key, taskId);
+ /** Sets the note bubble's taskId which is cached for SysUI. */
+ synchronized void setNoteBubbleTaskId(String key, int taskId) {
+ mNoteBubbleTaskIds.put(key, taskId);
}
/**
@@ -2902,7 +2922,7 @@ public class BubbleController implements ConfigurationChangeListener,
pw.println(" suppressing: " + key);
}
- pw.println("mAppBubbleTaskIds: " + mAppBubbleTaskIds.values());
+ pw.println("mNoteBubbleTaskIds: " + mNoteBubbleTaskIds.values());
}
}
@@ -2953,14 +2973,14 @@ public class BubbleController implements ConfigurationChangeListener,
}
@Override
- public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) {
+ public void showOrHideNoteBubble(Intent intent, UserHandle user, @Nullable Icon icon) {
mMainExecutor.execute(
- () -> BubbleController.this.showOrHideAppBubble(intent, user, icon));
+ () -> BubbleController.this.showOrHideNotesBubble(intent, user, icon));
}
@Override
- public boolean isAppBubbleTaskId(int taskId) {
- return mCachedState.mAppBubbleTaskIds.values().contains(taskId);
+ public boolean isNoteBubbleTaskId(int taskId) {
+ return mCachedState.mNoteBubbleTaskIds.values().contains(taskId);
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 97b03a9f58e4..ac74a42d1359 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -285,9 +285,9 @@ public class BubbleExpandedView extends LinearLayout {
// The taskId is saved to use for removeTask, preventing appearance in recent tasks.
mTaskId = taskId;
- if (mBubble != null && mBubble.isAppBubble()) {
+ if (mBubble != null && mBubble.isNoteBubble()) {
// Let the controller know sooner what the taskId is.
- mManager.setAppBubbleTaskId(mBubble.getKey(), mTaskId);
+ mManager.setNoteBubbleTaskId(mBubble.getKey(), mTaskId);
}
// With the task org, the taskAppeared callback will only happen once the task has
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt
index a02623138f1e..6be49ddc549a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt
@@ -28,7 +28,7 @@ interface BubbleExpandedViewManager {
fun promoteBubbleFromOverflow(bubble: Bubble)
fun removeBubble(key: String, reason: Int)
fun dismissBubble(bubble: Bubble, reason: Int)
- fun setAppBubbleTaskId(key: String, taskId: Int)
+ fun setNoteBubbleTaskId(key: String, taskId: Int)
fun isStackExpanded(): Boolean
fun isShowingAsBubbleBar(): Boolean
fun hideCurrentInputMethod()
@@ -73,8 +73,8 @@ interface BubbleExpandedViewManager {
controller.dismissBubble(bubble, reason)
}
- override fun setAppBubbleTaskId(key: String, taskId: Int) {
- controller.setAppBubbleTaskId(key, taskId)
+ override fun setNoteBubbleTaskId(key: String, taskId: Int) {
+ controller.setNoteBubbleTaskId(key, taskId)
}
override fun isStackExpanded(): Boolean = controller.isStackExpanded
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index a725e04d3f8a..8cf3f7afd46a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -33,6 +33,7 @@ import com.android.internal.protolog.ProtoLog;
import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.R;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.DeviceConfig;
/**
* Keeps track of display size, configuration, and specific bubble sizes. One place for all
@@ -758,20 +759,20 @@ public class BubblePositioner {
* is being shown, for a normal bubble.
*/
public PointF getDefaultStartPosition() {
- return getDefaultStartPosition(false /* isAppBubble */);
+ return getDefaultStartPosition(false /* isNoteBubble */);
}
/**
* The stack position to use if we don't have a saved location or if user education
* is being shown.
*
- * @param isAppBubble whether this start position is for an app bubble or not.
+ * @param isNoteBubble whether this start position is for a note bubble or not.
*/
- public PointF getDefaultStartPosition(boolean isAppBubble) {
+ public PointF getDefaultStartPosition(boolean isNoteBubble) {
// Normal bubbles start on the left if we're in LTR, right otherwise.
// TODO (b/294284894): update language around "app bubble" here
// App bubbles start on the right in RTL, left otherwise.
- final boolean startOnLeft = isAppBubble ? mDeviceConfig.isRtl() : !mDeviceConfig.isRtl();
+ final boolean startOnLeft = isNoteBubble ? mDeviceConfig.isRtl() : !mDeviceConfig.isRtl();
return getStartPosition(startOnLeft ? StackPinnedEdge.LEFT : StackPinnedEdge.RIGHT);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleResizabilityChecker.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleResizabilityChecker.kt
new file mode 100644
index 000000000000..6ca08215152f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleResizabilityChecker.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles
+
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.util.Log
+
+/**
+ * Checks if an intent is resizable to display in a bubble.
+ */
+class BubbleResizabilityChecker : ResizabilityChecker {
+
+ override fun isResizableActivity(
+ intent: Intent?,
+ packageManager: PackageManager, key: String
+ ): Boolean {
+ if (intent == null) {
+ Log.w(TAG, "Unable to send as bubble: $key null intent")
+ return false
+ }
+ val info = intent.resolveActivityInfo(packageManager, 0)
+ if (info == null) {
+ Log.w(
+ TAG, ("Unable to send as bubble: " + key
+ + " couldn't find activity info for intent: " + intent)
+ )
+ return false
+ }
+ if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
+ Log.w(
+ TAG, ("Unable to send as bubble: " + key
+ + " activity is not resizable for intent: " + intent)
+ )
+ return false
+ }
+ return true
+ }
+
+ companion object {
+ private const val TAG = "BubbleResizeChecker"
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index f1f49eda75b6..3babc0d801c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -91,6 +91,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.shared.animation.Interpolators;
import com.android.wm.shell.shared.animation.PhysicsAnimator;
+import com.android.wm.shell.shared.bubbles.DeviceConfig;
import com.android.wm.shell.shared.bubbles.DismissView;
import com.android.wm.shell.shared.bubbles.RelativeTouchListener;
import com.android.wm.shell.shared.magnetictarget.MagnetizedObject;
@@ -1975,12 +1976,11 @@ public class BubbleStackView extends FrameLayout
return;
}
- if (firstBubble && bubble.isAppBubble() && !mPositioner.hasUserModifiedDefaultPosition()) {
- // TODO (b/294284894): update language around "app bubble" here
- // If it's an app bubble and we don't have a previous resting position, update the
- // controllers to use the default position for the app bubble (it'd be different from
+ if (firstBubble && bubble.isNoteBubble() && !mPositioner.hasUserModifiedDefaultPosition()) {
+ // If it's an note bubble and we don't have a previous resting position, update the
+ // controllers to use the default position for the note bubble (it'd be different from
// the position initialized with the controllers originally).
- PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */);
+ PointF startPosition = mPositioner.getDefaultStartPosition(true /* isNoteBubble */);
mStackOnLeftOrWillBe = mPositioner.isStackOnLeft(startPosition);
mStackAnimationController.setStackPosition(startPosition);
mExpandedAnimationController.setCollapsePoint(startPosition);
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 a6b858500dcb..83d311ed6cd9 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
@@ -167,9 +167,9 @@ public class BubbleTaskViewHelper {
// The taskId is saved to use for removeTask, preventing appearance in recent tasks.
mTaskId = taskId;
- if (mBubble != null && mBubble.isAppBubble()) {
+ if (mBubble != null && mBubble.isNoteBubble()) {
// Let the controller know sooner what the taskId is.
- mExpandedViewManager.setAppBubbleTaskId(mBubble.getKey(), mTaskId);
+ mExpandedViewManager.setNoteBubbleTaskId(mBubble.getKey(), mTaskId);
}
// With the task org, the taskAppeared callback will only happen once the task has
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 4297fac0f6a8..44ae74479949 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -135,33 +135,31 @@ public interface Bubbles {
/**
* This method has different behavior depending on:
- * - if an app bubble exists
- * - if an app bubble is expanded
+ * - if a notes bubble exists
+ * - if a notes bubble is expanded
*
- * If no app bubble exists, this will add and expand a bubble with the provided intent. The
+ * If no notes bubble exists, this will add and expand a bubble with the provided intent. The
* intent must be explicit (i.e. include a package name or fully qualified component class name)
* and the activity for it should be resizable.
*
- * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is
- * expanded, calling this method will collapse it. If the app bubble is not expanded, calling
+ * If a notes bubble exists, this will toggle the visibility of it, i.e. if the notes bubble is
+ * expanded, calling this method will collapse it. If the notes bubble is not expanded, calling
* this method will expand it.
*
* These bubbles are <b>not</b> backed by a notification and remain until the user dismisses
* the bubble or bubble stack.
*
- * Some notes:
- * - Only one app bubble is supported at a time, regardless of users. Multi-users support is
- * tracked in b/273533235.
- * - Calling this method with a different intent than the existing app bubble will do nothing
+ * Some details:
+ * - Calling this method with a different intent than the existing bubble will do nothing
*
* @param intent the intent to display in the bubble expanded view.
- * @param user the {@link UserHandle} of the user to start this activity for.
- * @param icon the {@link Icon} to use for the bubble view.
+ * @param user the {@link UserHandle} of the user to start this activity for.
+ * @param icon the {@link Icon} to use for the bubble view.
*/
- void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon);
+ void showOrHideNoteBubble(Intent intent, UserHandle user, @Nullable Icon icon);
/** @return true if the specified {@code taskId} corresponds to app bubble's taskId. */
- boolean isAppBubbleTaskId(int taskId);
+ boolean isNoteBubbleTaskId(int taskId);
/**
` * @return a {@link SynchronousScreenCaptureListener} after performing a screenshot that may
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ResizabilityChecker.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ResizabilityChecker.kt
new file mode 100644
index 000000000000..1ccc05d38d80
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ResizabilityChecker.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles
+
+import android.content.Intent
+import android.content.pm.PackageManager
+
+/**
+ * Interface to check whether the activity backed by a specific intent is resizable.
+ */
+interface ResizabilityChecker {
+
+ /**
+ * Returns whether the provided intent represents a resizable activity.
+ *
+ * @param intent the intent to check
+ * @param packageManager the package manager to use to do the look up
+ * @param key a key representing thing being checked (used for error logging)
+ */
+ fun isResizableActivity(intent: Intent?, packageManager: PackageManager, key: String): Boolean
+} \ No newline at end of file
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
new file mode 100644
index 000000000000..00eaad675350
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles.bar
+
+import android.graphics.Rect
+import com.android.wm.shell.shared.bubbles.BubbleBarLocation
+
+/** Controller that takes care of the bubble bar drag events. */
+interface BubbleBarDragListener {
+
+ /** Called when the drag event is over the bubble bar drop zone. */
+ fun onDragItemOverBubbleBarDragZone(location: BubbleBarLocation)
+
+ /** Called when the drag event leaves the bubble bar drop zone. */
+ fun onItemDraggedOutsideBubbleBarDropZone()
+
+ /** Called when the drop event happens over the bubble bar drop zone. */
+ fun onItemDroppedOverBubbleBarDragZone(location: BubbleBarLocation?)
+
+ /**
+ * Returns mapping of the bubble bar locations to the corresponding
+ * [rect][android.graphics.Rect] zone.
+ */
+ fun getBubbleBarDropZones(l: Int, t: Int, r: Int, b: Int): Map<BubbleBarLocation, Rect>
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index f3f8d6f96a42..b1035bc177a1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -45,11 +45,11 @@ import com.android.wm.shell.bubbles.BubbleLogger;
import com.android.wm.shell.bubbles.BubbleOverflow;
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.BubbleViewProvider;
-import com.android.wm.shell.bubbles.DeviceConfig;
import com.android.wm.shell.bubbles.DismissViewUtils;
import com.android.wm.shell.bubbles.bar.BubbleBarExpandedViewDragController.DragListener;
import com.android.wm.shell.shared.bubbles.BaseBubblePinController;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
+import com.android.wm.shell.shared.bubbles.DeviceConfig;
import com.android.wm.shell.shared.bubbles.DismissView;
import kotlin.Unit;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.kt
deleted file mode 100644
index 4206d9320b7d..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.bubbles.properties
-
-/**
- * An interface for exposing bubble properties via flags which can be controlled easily in tests.
- */
-interface BubbleProperties {
- /**
- * Whether bubble bar is enabled.
- *
- * When this is `true`, depending on additional factors, such as screen size and taskbar state,
- * bubbles will be displayed in the bubble bar instead of floating.
- *
- * When this is `false`, bubbles will be floating.
- */
- val isBubbleBarEnabled: Boolean
-
- /** Refreshes the current value of [isBubbleBarEnabled]. */
- fun refresh()
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt
deleted file mode 100644
index 33b61b164988..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.bubbles.properties
-
-import android.os.SystemProperties
-import com.android.wm.shell.Flags
-
-/** Provides bubble properties in production. */
-object ProdBubbleProperties : BubbleProperties {
-
- private var _isBubbleBarEnabled = Flags.enableBubbleBar() ||
- SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false)
-
- override val isBubbleBarEnabled
- get() = _isBubbleBarEnabled
-
- override fun refresh() {
- _isBubbleBarEnabled = Flags.enableBubbleBar() ||
- SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false)
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java
new file mode 100644
index 000000000000..c10c2c905c97
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java
@@ -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.wm.shell.common.pip;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import android.app.ActivityManager;
+import android.window.DisplayAreaInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.window.flags.Flags;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.desktopmode.DesktopRepository;
+import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
+import com.android.wm.shell.pip2.phone.PipTransition;
+
+import java.util.Optional;
+
+/** Helper class for PiP on Desktop Mode. */
+public class PipDesktopState {
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
+ private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional;
+ private final Optional<DesktopWallpaperActivityTokenProvider>
+ mDesktopWallpaperActivityTokenProviderOptional;
+ private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
+
+ public PipDesktopState(PipDisplayLayoutState pipDisplayLayoutState,
+ Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+ Optional<DesktopWallpaperActivityTokenProvider>
+ desktopWallpaperActivityTokenProviderOptional,
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+ mPipDisplayLayoutState = pipDisplayLayoutState;
+ mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional;
+ mDesktopWallpaperActivityTokenProviderOptional =
+ desktopWallpaperActivityTokenProviderOptional;
+ mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
+ }
+
+ /**
+ * Returns whether PiP in Desktop Windowing is enabled by checking the following:
+ * - Desktop Windowing in PiP flag is enabled
+ * - DesktopWallpaperActivityTokenProvider is injected
+ * - DesktopUserRepositories is injected
+ */
+ public boolean isDesktopWindowingPipEnabled() {
+ return Flags.enableDesktopWindowingPip()
+ && mDesktopWallpaperActivityTokenProviderOptional.isPresent()
+ && mDesktopUserRepositoriesOptional.isPresent();
+ }
+
+ /** Returns whether PiP in Connected Displays is enabled by checking the flag. */
+ public boolean isConnectedDisplaysPipEnabled() {
+ return Flags.enableConnectedDisplaysPip();
+ }
+
+ /** Returns whether the display with the PiP task is in freeform windowing mode. */
+ private boolean isDisplayInFreeform() {
+ final DisplayAreaInfo tdaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(
+ mPipDisplayLayoutState.getDisplayId());
+ if (tdaInfo != null) {
+ return tdaInfo.configuration.windowConfiguration.getWindowingMode()
+ == WINDOWING_MODE_FREEFORM;
+ }
+ return false;
+ }
+
+ /** Returns whether PiP is exiting while we're in a Desktop Mode session. */
+ private boolean isPipExitingToDesktopMode() {
+ // Early return if PiP in Desktop Windowing is not supported.
+ if (!isDesktopWindowingPipEnabled()) {
+ return false;
+ }
+ final int displayId = mPipDisplayLayoutState.getDisplayId();
+ return getDesktopRepository().getVisibleTaskCount(displayId) > 0
+ || getDesktopWallpaperActivityTokenProvider().isWallpaperActivityVisible(displayId)
+ || isDisplayInFreeform();
+ }
+
+ /** Returns whether {@param pipTask} would be entering in a Desktop Mode session. */
+ public boolean isPipEnteringInDesktopMode(ActivityManager.RunningTaskInfo pipTask) {
+ // Early return if PiP in Desktop Windowing is not supported.
+ if (!isDesktopWindowingPipEnabled()) {
+ return false;
+ }
+ final DesktopRepository desktopRepository = getDesktopRepository();
+ return desktopRepository.getVisibleTaskCount(pipTask.getDisplayId()) > 0
+ || desktopRepository.isMinimizedPipPresentInDisplay(pipTask.getDisplayId());
+ }
+
+ /**
+ * Invoked when an EXIT_PiP transition is detected in {@link PipTransition}.
+ * Returns whether the PiP exiting should also trigger the active Desktop Mode session to exit.
+ */
+ public boolean shouldExitPipExitDesktopMode() {
+ // Early return if PiP in Desktop Windowing is not supported.
+ if (!isDesktopWindowingPipEnabled()) {
+ return false;
+ }
+ final int displayId = mPipDisplayLayoutState.getDisplayId();
+ return getDesktopRepository().getVisibleTaskCount(displayId) == 0
+ && getDesktopWallpaperActivityTokenProvider().isWallpaperActivityVisible(displayId);
+ }
+
+ /**
+ * Returns a {@link WindowContainerTransaction} that reorders the {@link WindowContainerToken}
+ * of the DesktopWallpaperActivity for the display with the given {@param displayId}.
+ */
+ public WindowContainerTransaction getWallpaperActivityTokenWct(int displayId) {
+ return new WindowContainerTransaction().reorder(
+ getDesktopWallpaperActivityTokenProvider().getToken(displayId), /* onTop= */ false);
+ }
+
+ /**
+ * The windowing mode to restore to when resizing out of PIP direction.
+ * Defaults to undefined and can be overridden to restore to an alternate windowing mode.
+ */
+ public int getOutPipWindowingMode() {
+ // If we are exiting PiP while the device is in Desktop mode (the task should expand to
+ // freeform windowing mode):
+ // 1) If the display windowing mode is freeform, set windowing mode to UNDEFINED so it will
+ // resolve the windowing mode to the display's windowing mode.
+ // 2) If the display windowing mode is not FREEFORM, set windowing mode to FREEFORM.
+ if (isPipExitingToDesktopMode()) {
+ if (isDisplayInFreeform()) {
+ return WINDOWING_MODE_UNDEFINED;
+ } else {
+ return WINDOWING_MODE_FREEFORM;
+ }
+ }
+
+ // By default, or if the task is going to fullscreen, reset the windowing mode to undefined.
+ return WINDOWING_MODE_UNDEFINED;
+ }
+
+ private DesktopRepository getDesktopRepository() {
+ return mDesktopUserRepositoriesOptional.get().getCurrent();
+ }
+
+ private DesktopWallpaperActivityTokenProvider getDesktopWallpaperActivityTokenProvider() {
+ return mDesktopWallpaperActivityTokenProviderOptional.get();
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/OWNERS
index 752d2fd721a5..8ab53eaa5eea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/OWNERS
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/OWNERS
@@ -1,2 +1,3 @@
# WM Shell sub-module dagger owners
-jorgegil@google.com \ No newline at end of file
+jorgegil@google.com
+madym@google.com \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 0f232d50f2fc..6ab103e3bd89 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -53,13 +53,14 @@ import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
import com.android.wm.shell.apptoweb.AssistContentRequester;
import com.android.wm.shell.appzoomout.AppZoomOutController;
import com.android.wm.shell.back.BackAnimationController;
+import com.android.wm.shell.bubbles.bar.BubbleBarDragListener;
import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.BubbleData;
import com.android.wm.shell.bubbles.BubbleDataRepository;
import com.android.wm.shell.bubbles.BubbleEducationController;
import com.android.wm.shell.bubbles.BubbleLogger;
import com.android.wm.shell.bubbles.BubblePositioner;
-import com.android.wm.shell.bubbles.properties.ProdBubbleProperties;
+import com.android.wm.shell.bubbles.BubbleResizabilityChecker;
import com.android.wm.shell.bubbles.storage.BubblePersistentRepository;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
@@ -169,19 +170,18 @@ import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromo
import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController;
import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
import dagger.Binds;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
-
import kotlinx.coroutines.CoroutineScope;
import kotlinx.coroutines.ExperimentalCoroutinesApi;
import kotlinx.coroutines.MainCoroutineDispatcher;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-
/**
* Provides dependencies from {@link com.android.wm.shell}, these dependencies are only accessible
* from components within the WM subcomponent (can be explicitly exposed to the SysUIComponent, see
@@ -294,7 +294,7 @@ public abstract class WMShellModule {
transitions,
syncQueue,
wmService,
- ProdBubbleProperties.INSTANCE);
+ new BubbleResizabilityChecker());
}
//
@@ -1157,9 +1157,10 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
static DesksTransitionObserver provideDesksTransitionObserver(
- @NonNull @DynamicOverride DesktopUserRepositories desktopUserRepositories
+ @NonNull @DynamicOverride DesktopUserRepositories desktopUserRepositories,
+ @NonNull DesksOrganizer desksOrganizer
) {
- return new DesksTransitionObserver(desktopUserRepositories);
+ return new DesksTransitionObserver(desktopUserRepositories, desksOrganizer);
}
@WMSingleton
@@ -1411,6 +1412,7 @@ public abstract class WMShellModule {
IconProvider iconProvider,
GlobalDragListener globalDragListener,
Transitions transitions,
+ Lazy<BubbleController> bubbleControllerLazy,
@ShellMainThread ShellExecutor mainExecutor) {
return new DragAndDropController(
context,
@@ -1423,6 +1425,12 @@ public abstract class WMShellModule {
iconProvider,
globalDragListener,
transitions,
+ new Lazy<>() {
+ @Override
+ public BubbleBarDragListener get() {
+ return bubbleControllerLazy.get();
+ }
+ },
mainExecutor);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 413300612f7d..e7c76bbd91b2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -31,6 +31,7 @@ import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.pip.PipAppOpsListener;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDesktopState;
import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.common.pip.PipMediaController;
import com.android.wm.shell.common.pip.PipPerfHintController;
@@ -84,14 +85,11 @@ public abstract class Pip2Module {
@NonNull PipDisplayLayoutState pipDisplayLayoutState,
@NonNull PipUiStateChangeController pipUiStateChangeController,
DisplayController displayController,
- Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
- Optional<DesktopWallpaperActivityTokenProvider>
- desktopWallpaperActivityTokenProviderOptional) {
+ PipDesktopState pipDesktopState) {
return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener,
pipScheduler, pipStackListenerController, pipDisplayLayoutState,
- pipUiStateChangeController, displayController, desktopUserRepositoriesOptional,
- desktopWallpaperActivityTokenProviderOptional);
+ pipUiStateChangeController, displayController, pipDesktopState);
}
@WMSingleton
@@ -142,13 +140,9 @@ public abstract class Pip2Module {
PipBoundsState pipBoundsState,
@ShellMainThread ShellExecutor mainExecutor,
PipTransitionState pipTransitionState,
- Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
- Optional<DesktopWallpaperActivityTokenProvider>
- desktopWallpaperActivityTokenProviderOptional,
- RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+ PipDesktopState pipDesktopState) {
return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState,
- desktopUserRepositoriesOptional, desktopWallpaperActivityTokenProviderOptional,
- rootTaskDisplayAreaOrganizer);
+ pipDesktopState);
}
@WMSingleton
@@ -233,4 +227,17 @@ public abstract class Pip2Module {
return new PipTaskListener(context, shellTaskOrganizer, pipTransitionState,
pipScheduler, pipBoundsState, pipBoundsAlgorithm, mainExecutor);
}
+
+ @WMSingleton
+ @Provides
+ static PipDesktopState providePipDesktopState(
+ PipDisplayLayoutState pipDisplayLayoutState,
+ Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
+ Optional<DesktopWallpaperActivityTokenProvider>
+ desktopWallpaperActivityTokenProviderOptional,
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer
+ ) {
+ return new PipDesktopState(pipDisplayLayoutState, desktopUserRepositoriesOptional,
+ desktopWallpaperActivityTokenProviderOptional, rootTaskDisplayAreaOrganizer);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
index b93d2e396402..03bc42f08d59 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
@@ -57,7 +57,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
return false
}
if (!Flags.enableMultipleDesktopsBackend()) {
- return controller.moveTaskToDesktop(taskId, transitionSource = UNKNOWN)
+ return controller.moveTaskToDefaultDeskAndActivate(taskId, transitionSource = UNKNOWN)
}
if (args.size < 3) {
pw.println("Error: desk id should be provided as arguments")
@@ -70,8 +70,9 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
pw.println("Error: desk id should be an integer")
return false
}
+ controller.moveTaskToDesk(taskId = taskId, deskId = deskId, transitionSource = UNKNOWN)
pw.println("Not implemented.")
- return false
+ return true
}
private fun runMoveToNextDisplay(args: Array<String>, pw: PrintWriter): Boolean {
@@ -131,8 +132,8 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
pw.println("Error: desk id should be an integer")
return false
}
- pw.println("Not implemented.")
- return false
+ controller.activateDesk(deskId)
+ return true
}
private fun runRemoveDesk(args: Array<String>, pw: PrintWriter): Boolean {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index 32ee319a053b..621ccba40db2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -59,6 +59,8 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
/**
* Animated visual indicator for Desktop Mode windowing transitions.
@@ -149,12 +151,19 @@ public class DesktopModeVisualIndicator {
// left, and split right for the right edge. This is universal across all drag event types.
if (inputCoordinates.x < 0) return TO_SPLIT_LEFT_INDICATOR;
if (inputCoordinates.x > layout.width()) return TO_SPLIT_RIGHT_INDICATOR;
- // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone.
- // In drags not originating on a freeform caption, we should default to a TO_DESKTOP
- // indicator.
- IndicatorType result = mDragStartState == DragStartState.FROM_FREEFORM
- ? NO_INDICATOR
- : TO_DESKTOP_INDICATOR;
+ IndicatorType result;
+ if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()
+ && !DesktopModeStatus.canEnterDesktopMode(mContext)) {
+ // If desktop is not available, default to "no indicator"
+ result = NO_INDICATOR;
+ } else {
+ // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone.
+ // In drags not originating on a freeform caption, we should default to a TO_DESKTOP
+ // indicator.
+ result = mDragStartState == DragStartState.FROM_FREEFORM
+ ? NO_INDICATOR
+ : TO_DESKTOP_INDICATOR;
+ }
final int transitionAreaWidth = mContext.getResources().getDimensionPixelSize(
com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness);
// Because drags in freeform use task position for indicator calculation, we need to
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
index 043b353ba380..4777e7f93bc9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt
@@ -226,31 +226,42 @@ class DesktopRepository(
desktopData.setActiveDesk(displayId = displayId, deskId = deskId)
}
+ /** Returns the id of the active desk in the given display, if any. */
+ @VisibleForTesting
+ fun getActiveDeskId(displayId: Int): Int? = desktopData.getActiveDesk(displayId)?.deskId
+
/**
* Adds task with [taskId] to the list of freeform tasks on [displayId]'s active desk.
*
* TODO: b/389960283 - add explicit [deskId] argument.
*/
fun addTask(displayId: Int, taskId: Int, isVisible: Boolean) {
- addOrMoveFreeformTaskToTop(displayId, taskId)
- addActiveTask(displayId, taskId)
- updateTask(displayId, taskId, isVisible)
+ val activeDesk =
+ checkNotNull(desktopData.getDefaultDesk(displayId)) {
+ "Expected desk in display: $displayId"
+ }
+ addTaskToDesk(displayId = displayId, deskId = activeDesk.deskId, taskId = taskId, isVisible)
}
- /**
- * Adds task with [taskId] to the list of active tasks on [displayId]'s active desk.
- *
- * TODO: b/389960283 - add explicit [deskId] argument.
- */
- private fun addActiveTask(displayId: Int, taskId: Int) {
- val activeDesk = desktopData.getDefaultDesk(displayId)
- checkNotNull(activeDesk) { "Expected desk in display: $displayId" }
+ fun addTaskToDesk(displayId: Int, deskId: Int, taskId: Int, isVisible: Boolean) {
+ addOrMoveTaskToTopOfDesk(displayId = displayId, deskId = deskId, taskId = taskId)
+ addActiveTaskToDesk(displayId = displayId, deskId = deskId, taskId = taskId)
+ updateTaskInDesk(
+ displayId = displayId,
+ deskId = deskId,
+ taskId = taskId,
+ isVisible = isVisible,
+ )
+ }
+
+ private fun addActiveTaskToDesk(displayId: Int, deskId: Int, taskId: Int) {
+ val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" }
- // Removes task if it is active on another desk excluding [activeDesk].
- removeActiveTask(taskId, excludedDeskId = activeDesk.deskId)
+ // Removes task if it is active on another desk excluding this desk.
+ removeActiveTask(taskId, excludedDeskId = deskId)
- if (activeDesk.activeTasks.add(taskId)) {
- logD("Adds active task=%d displayId=%d deskId=%d", taskId, displayId, activeDesk.deskId)
+ if (desk.activeTasks.add(taskId)) {
+ logD("Adds active task=%d displayId=%d deskId=%d", taskId, displayId, deskId)
updateActiveTasksListeners(displayId)
}
}
@@ -401,10 +412,10 @@ class DesktopRepository(
emptySet()
}
- /** Removes task from visible tasks of all displays except [excludedDisplayId]. */
- private fun removeVisibleTask(taskId: Int, excludedDisplayId: Int? = null) {
+ /** Removes task from visible tasks of all desks except [excludedDeskId]. */
+ private fun removeVisibleTask(taskId: Int, excludedDeskId: Int? = null) {
desktopData.forAllDesks { displayId, desk ->
- if (displayId != excludedDisplayId && desk.visibleTasks.remove(taskId)) {
+ if (desk.deskId != excludedDeskId && desk.visibleTasks.remove(taskId)) {
notifyVisibleTaskListeners(displayId, desk.visibleTasks.size)
}
}
@@ -419,30 +430,58 @@ class DesktopRepository(
* TODO: b/389960283 - add explicit [deskId] argument.
*/
fun updateTask(displayId: Int, taskId: Int, isVisible: Boolean) {
- logD("updateTask taskId=%d, displayId=%d, isVisible=%b", taskId, displayId, isVisible)
+ val validDisplayId =
+ if (displayId == INVALID_DISPLAY) {
+ // When a task vanishes it doesn't have a displayId. Find the display of the task.
+ getDisplayIdForTask(taskId)
+ } else {
+ displayId
+ }
+ if (validDisplayId == null) {
+ logW("No display id found for task: taskId=%d", taskId)
+ return
+ }
+ val desk =
+ checkNotNull(desktopData.getDefaultDesk(validDisplayId)) {
+ "Expected a desk in display: $validDisplayId"
+ }
+ updateTaskInDesk(
+ displayId = validDisplayId,
+ deskId = desk.deskId,
+ taskId = taskId,
+ isVisible,
+ )
+ }
+
+ private fun updateTaskInDesk(displayId: Int, deskId: Int, taskId: Int, isVisible: Boolean) {
+ check(displayId != INVALID_DISPLAY) { "Display must be valid" }
+ logD(
+ "updateTaskInDesk taskId=%d, deskId=%d, displayId=%d, isVisible=%b",
+ taskId,
+ deskId,
+ displayId,
+ isVisible,
+ )
if (isVisible) {
- // If task is visible, remove it from any other display besides [displayId].
- removeVisibleTask(taskId, excludedDisplayId = displayId)
- } else if (displayId == INVALID_DISPLAY) {
- // Task has vanished. Check which display to remove the task from.
- removeVisibleTask(taskId)
- return
+ // If task is visible, remove it from any other desk besides [deskId].
+ removeVisibleTask(taskId, excludedDeskId = deskId)
}
- val prevCount = getVisibleTaskCount(displayId)
+ val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" }
+ val prevCount = getVisibleTaskCountInDesk(deskId)
if (isVisible) {
- desktopData.getDefaultDesk(displayId)?.visibleTasks?.add(taskId)
- ?: error("Expected non-null desk in display $displayId")
+ desk.visibleTasks.add(taskId)
unminimizeTask(displayId, taskId)
} else {
- desktopData.getActiveDesk(displayId)?.visibleTasks?.remove(taskId)
+ desk.visibleTasks.remove(taskId)
}
- val newCount = getVisibleTaskCount(displayId)
+ val newCount = getVisibleTaskCount(deskId)
if (prevCount != newCount) {
logD(
- "Update task visibility taskId=%d visible=%b displayId=%d",
+ "Update task visibility taskId=%d visible=%b deskId=%d displayId=%d",
taskId,
isVisible,
+ deskId,
displayId,
)
logD("VisibleTaskCount has changed from %d to %d", prevCount, newCount)
@@ -602,33 +641,32 @@ class DesktopRepository(
/**
* Gets number of visible freeform tasks on given [displayId]'s active desk.
*
- * TODO: b/389960283 - add explicit [deskId] argument.
+ * TODO: b/389960283 - migrate callers to [getVisibleTaskCountInDesk].
*/
fun getVisibleTaskCount(displayId: Int): Int =
(desktopData.getActiveDesk(displayId)?.visibleTasks?.size ?: 0).also {
logD("getVisibleTaskCount=$it")
}
+ /** Gets the number of visible tasks on the given desk. */
+ fun getVisibleTaskCountInDesk(deskId: Int): Int =
+ desktopData.getDesk(deskId)?.visibleTasks?.size ?: 0
+
/**
* Adds task (or moves if it already exists) to the top of the ordered list.
*
* Unminimizes the task if it is minimized.
- *
- * TODO: b/389960283 - add explicit [deskId] argument.
*/
- private fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) {
- val desk = getDefaultDesk(displayId) ?: error("Expected a desk in display: $displayId")
- logD(
- "Add or move task to top: display=%d taskId=%d deskId=%d",
- taskId,
- displayId,
- desk.deskId,
- )
+ private fun addOrMoveTaskToTopOfDesk(displayId: Int, deskId: Int, taskId: Int) {
+ val desk = desktopData.getDesk(deskId) ?: error("Could not find desk: $deskId")
+ logD("addOrMoveTaskToTopOfDesk: display=%d deskId=%d taskId=%d", displayId, deskId, taskId)
desktopData.forAllDesks { _, desk1 -> desk1.freeformTasksInZOrder.remove(taskId) }
desk.freeformTasksInZOrder.add(0, taskId)
+ // TODO: double check minimization logic.
// Unminimize the task if it is minimized.
unminimizeTask(displayId, taskId)
if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
+ // TODO: can probably just update the desk.
updatePersistentRepository(displayId)
}
}
@@ -644,6 +682,7 @@ class DesktopRepository(
// mark it as minimized.
getDisplayIdForTask(taskId)?.let { minimizeTask(it, taskId) }
?: logW("Minimize task: No display id found for task: taskId=%d", taskId)
+ return
} else {
logD("Minimize Task: display=%d, task=%d", displayId, taskId)
desktopData.getActiveDesk(displayId)?.minimizedTasks?.add(taskId)
@@ -676,7 +715,7 @@ class DesktopRepository(
private fun getDisplayIdForTask(taskId: Int): Int? {
var displayForTask: Int? = null
desktopData.forAllDesks { displayId, desk ->
- if (taskId in desk.freeformTasksInZOrder) {
+ if (taskId in desk.activeTasks) {
displayForTask = displayId
}
}
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 3f88e7bddd34..7b0fb1d89557 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
@@ -315,24 +315,10 @@ class DesktopTasksController(
}
/** Show all tasks, that are part of the desktop, on top of launcher */
+ @Deprecated("Use activateDesk() instead.", ReplaceWith("activateDesk()"))
fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition? = null) {
logV("showDesktopApps")
- val wct = WindowContainerTransaction()
- bringDesktopAppsToFront(displayId, wct)
-
- val transitionType = transitionType(remoteTransition)
- val handler =
- remoteTransition?.let {
- OneShotRemoteHandler(transitions.mainExecutor, remoteTransition)
- }
- transitions.startTransition(transitionType, wct, handler).also { t ->
- handler?.setTransition(t)
- }
-
- // launch from recent DesktopTaskView
- desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted(
- FREEFORM_ANIMATION_DURATION
- )
+ activateDefaultDeskInDisplay(displayId, remoteTransition)
}
/** Gets number of visible freeform tasks in [displayId]. */
@@ -371,15 +357,15 @@ class DesktopTasksController(
0 -> return
// Full screen case
1 ->
- moveRunningTaskToDesktop(
- allFocusedTasks.single(),
+ moveTaskToDefaultDeskAndActivate(
+ allFocusedTasks.single().taskId,
transitionSource = transitionSource,
)
// Split-screen case where there are two focused tasks, then we find the child
// task to move to desktop.
2 ->
- moveRunningTaskToDesktop(
- getSplitFocusedTask(allFocusedTasks[0], allFocusedTasks[1]),
+ moveTaskToDefaultDeskAndActivate(
+ getSplitFocusedTask(allFocusedTasks[0], allFocusedTasks[1]).taskId,
transitionSource = transitionSource,
)
else ->
@@ -442,15 +428,57 @@ class DesktopTasksController(
/** Moves task to desktop mode if task is running, else launches it in desktop mode. */
@JvmOverloads
- fun moveTaskToDesktop(
+ fun moveTaskToDefaultDeskAndActivate(
+ taskId: Int,
+ wct: WindowContainerTransaction = WindowContainerTransaction(),
+ transitionSource: DesktopModeTransitionSource,
+ remoteTransition: RemoteTransition? = null,
+ callback: IMoveToDesktopCallback? = null,
+ ): Boolean {
+ val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId)
+ val backgroundTask = recentTasksController?.findTaskInBackground(taskId)
+ if (runningTask == null && backgroundTask == null) {
+ logW("moveTaskToDefaultDeskAndActivate taskId=%d not found", taskId)
+ return false
+ }
+ // TODO(342378842): Instead of using default display, support multiple displays
+ val displayId = runningTask?.displayId ?: DEFAULT_DISPLAY
+ val deskId =
+ checkNotNull(taskRepository.getDefaultDeskId(displayId)) {
+ "Expected a default desk to exist"
+ }
+ return moveTaskToDesk(
+ taskId = taskId,
+ deskId = deskId,
+ wct = wct,
+ transitionSource = transitionSource,
+ remoteTransition = remoteTransition,
+ )
+ }
+
+ /** Moves task to desktop mode if task is running, else launches it in desktop mode. */
+ fun moveTaskToDesk(
taskId: Int,
+ deskId: Int,
wct: WindowContainerTransaction = WindowContainerTransaction(),
transitionSource: DesktopModeTransitionSource,
remoteTransition: RemoteTransition? = null,
callback: IMoveToDesktopCallback? = null,
): Boolean {
val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId)
- if (runningTask == null) {
+ if (runningTask != null) {
+ moveRunningTaskToDesk(
+ task = runningTask,
+ deskId = deskId,
+ wct = wct,
+ transitionSource = transitionSource,
+ remoteTransition = remoteTransition,
+ callback = callback,
+ )
+ }
+ val backgroundTask = recentTasksController?.findTaskInBackground(taskId)
+ if (backgroundTask != null) {
+ // TODO: b/391484662 - add support for |deskId|.
return moveBackgroundTaskToDesktop(
taskId,
wct,
@@ -459,8 +487,8 @@ class DesktopTasksController(
callback,
)
}
- moveRunningTaskToDesktop(runningTask, wct, transitionSource, remoteTransition, callback)
- return true
+ logW("moveTaskToDesk taskId=%d not found", taskId)
+ return false
}
private fun moveBackgroundTaskToDesktop(
@@ -514,8 +542,9 @@ class DesktopTasksController(
}
/** Moves a running task to desktop. */
- fun moveRunningTaskToDesktop(
+ private fun moveRunningTaskToDesk(
task: RunningTaskInfo,
+ deskId: Int,
wct: WindowContainerTransaction = WindowContainerTransaction(),
transitionSource: DesktopModeTransitionSource,
remoteTransition: RemoteTransition? = null,
@@ -525,20 +554,49 @@ class DesktopTasksController(
logW("Cannot enter desktop for taskId %d, ineligible top activity found", task.taskId)
return
}
- logV("moveRunningTaskToDesktop taskId=%d", task.taskId)
+ val displayId = taskRepository.getDisplayForDesk(deskId)
+ logV(
+ "moveRunningTaskToDesk taskId=%d deskId=%d displayId=%d",
+ task.taskId,
+ deskId,
+ displayId,
+ )
exitSplitIfApplicable(wct, task)
val exitResult =
desktopImmersiveController.exitImmersiveIfApplicable(
wct = wct,
- displayId = task.displayId,
+ displayId = displayId,
excludeTaskId = task.taskId,
reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH,
)
- // Bring other apps to front first
val taskIdToMinimize =
- bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
- addMoveToDesktopChanges(wct, task)
+ if (Flags.enableMultipleDesktopsBackend()) {
+ // Activate the desk first.
+ prepareForDeskActivation(displayId, wct)
+ desksOrganizer.activateDesk(wct, deskId)
+ if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
+ // TODO: 362720497 - do non-running tasks need to be restarted with
+ // |wct#startTask|?
+ }
+ taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
+ doesAnyTaskRequireTaskbarRounding(displayId)
+ )
+ // TODO: 362720497 - activating a desk with the intention to move a new task to it
+ // means we may need to minimize something in the activating desk. Do so here
+ // similar
+ // to how it's done in #bringDesktopAppsToFrontBeforeShowingNewTask instead of
+ // returning null.
+ null
+ } else {
+ // Bring other apps to front first.
+ bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
+ }
+ if (Flags.enableMultipleDesktopsBackend()) {
+ prepareMoveTaskToDesk(wct, task, deskId)
+ } else {
+ addMoveToDesktopChanges(wct, task)
+ }
val transition: IBinder
if (remoteTransition != null) {
@@ -557,6 +615,18 @@ class DesktopTasksController(
addPendingMinimizeTransition(transition, it, MinimizeReason.TASK_LIMIT)
}
exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
+ if (Flags.enableMultipleDesktopsBackend()) {
+ desksTransitionObserver.addPendingTransition(
+ DeskTransition.ActiveDeskWithTask(
+ token = transition,
+ displayId = displayId,
+ deskId = deskId,
+ enterTaskId = task.taskId,
+ )
+ )
+ } else {
+ taskRepository.setActiveDesk(displayId = displayId, deskId = deskId)
+ }
}
private fun invokeCallbackToOverview(transition: IBinder, callback: IMoveToDesktopCallback?) {
@@ -606,9 +676,9 @@ class DesktopTasksController(
val wct = WindowContainerTransaction()
exitSplitIfApplicable(wct, taskInfo)
if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
- moveHomeTask(wct, toTop = true, taskInfo.displayId)
+ moveHomeTask(taskInfo.displayId, wct)
} else {
- moveHomeTask(wct, toTop = true)
+ moveHomeTask(context.displayId, wct)
}
val taskIdToMinimize =
bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
@@ -780,7 +850,7 @@ class DesktopTasksController(
// We are moving a freeform task to fullscreen, put the home task under the fullscreen task.
if (!forceEnterDesktop(task.displayId)) {
- moveHomeTask(wct, toTop = true, task.displayId)
+ moveHomeTask(task.displayId, wct)
wct.reorder(task.token, /* onTop= */ true)
}
@@ -1018,6 +1088,23 @@ class DesktopTasksController(
}
val wct = WindowContainerTransaction()
+
+ // check if the task is part of splitscreen
+ if (
+ Flags.enableNonDefaultDisplaySplit() &&
+ Flags.enableMoveToNextDisplayShortcut() &&
+ splitScreenController.isTaskInSplitScreen(task.taskId)
+ ) {
+ val stageCoordinatorRootTaskToken =
+ splitScreenController.multiDisplayProvider.getDisplayRootForDisplayId(
+ DEFAULT_DISPLAY
+ )
+
+ wct.reparent(stageCoordinatorRootTaskToken, displayAreaInfo.token, true /* onTop */)
+ transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
+ return
+ }
+
if (!task.isFreeform) {
addMoveToDesktopChanges(wct, task, displayId)
} else if (Flags.enableMoveToNextDisplayShortcut()) {
@@ -1037,6 +1124,10 @@ class DesktopTasksController(
task.displayId,
wct,
forceToFullscreen = false,
+ // TODO: b/371096166 - Temporary turing home relaunch off to prevent home stealing
+ // display focus. Remove shouldEndUpAtHome = false when home focus handling
+ // with connected display is implemented in wm core.
+ shouldEndUpAtHome = false,
)
}
@@ -1416,33 +1507,36 @@ class DesktopTasksController(
?: WINDOWING_MODE_UNDEFINED
}
+ private fun prepareForDeskActivation(displayId: Int, wct: WindowContainerTransaction) {
+ // Move home to front, ensures that we go back home when all desktop windows are closed
+ val useParamDisplayId =
+ Flags.enableMultipleDesktopsBackend() ||
+ Flags.enablePerDisplayDesktopWallpaperActivity()
+ moveHomeTask(displayId = if (useParamDisplayId) displayId else context.displayId, wct = wct)
+ // Currently, we only handle the desktop on the default display really.
+ if (
+ (displayId == DEFAULT_DISPLAY || Flags.enablePerDisplayDesktopWallpaperActivity()) &&
+ ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
+ ) {
+ // Add translucent wallpaper activity to show the wallpaper underneath.
+ addWallpaperActivity(displayId, wct)
+ }
+ }
+
private fun bringDesktopAppsToFrontBeforeShowingNewTask(
displayId: Int,
wct: WindowContainerTransaction,
newTaskIdInFront: Int,
): Int? = bringDesktopAppsToFront(displayId, wct, newTaskIdInFront)
+ @Deprecated("Use activeDesk() instead.", ReplaceWith("activateDesk()"))
private fun bringDesktopAppsToFront(
displayId: Int,
wct: WindowContainerTransaction,
newTaskIdInFront: Int? = null,
): Int? {
logV("bringDesktopAppsToFront, newTaskId=%d", newTaskIdInFront)
- // Move home to front, ensures that we go back home when all desktop windows are closed
- if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
- moveHomeTask(wct, toTop = true, displayId)
- } else {
- moveHomeTask(wct, toTop = true)
- }
-
- // Currently, we only handle the desktop on the default display really.
- if (
- (displayId == DEFAULT_DISPLAY || Flags.enablePerDisplayDesktopWallpaperActivity()) &&
- ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue()
- ) {
- // Add translucent wallpaper activity to show the wallpaper underneath
- addWallpaperActivity(displayId, wct)
- }
+ prepareForDeskActivation(displayId, wct)
val expandedTasksOrderedFrontToBack = taskRepository.getExpandedTasksOrdered(displayId)
// If we're adding a new Task we might need to minimize an old one
@@ -1486,15 +1580,11 @@ class DesktopTasksController(
return taskIdToMinimize
}
- private fun moveHomeTask(
- wct: WindowContainerTransaction,
- toTop: Boolean,
- displayId: Int = DEFAULT_DISPLAY,
- ) {
+ private fun moveHomeTask(displayId: Int, wct: WindowContainerTransaction) {
shellTaskOrganizer
.getRunningTasks(displayId)
.firstOrNull { task -> task.activityType == ACTIVITY_TYPE_HOME }
- ?.let { homeTask -> wct.reorder(homeTask.getToken(), /* onTop= */ toTop) }
+ ?.let { homeTask -> wct.reorder(homeTask.getToken(), /* onTop= */ true) }
}
private fun addLaunchHomePendingIntent(wct: WindowContainerTransaction, displayId: Int) {
@@ -1528,16 +1618,11 @@ class DesktopTasksController(
private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) {
logV("addWallpaperActivity")
if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) {
-
- // If the wallpaper activity for this display already exists, let's reorder it to top.
- val wallpaperActivityToken = desktopWallpaperActivityTokenProvider.getToken(displayId)
- if (wallpaperActivityToken != null) {
- wct.reorder(wallpaperActivityToken, /* onTop= */ true)
- return
- }
-
val intent = Intent(context, DesktopWallpaperActivity::class.java)
- if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
+ if (
+ desktopWallpaperActivityTokenProvider.getToken(displayId) == null &&
+ Flags.enablePerDisplayDesktopWallpaperActivity()
+ ) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}
@@ -2155,6 +2240,7 @@ class DesktopTasksController(
* different [displayId] if the task should be moved to a different display.
*/
@VisibleForTesting
+ @Deprecated("Deprecated with multiple desks", ReplaceWith("prepareMoveTaskToDesk()"))
fun addMoveToDesktopChanges(
wct: WindowContainerTransaction,
taskInfo: RunningTaskInfo,
@@ -2182,6 +2268,24 @@ class DesktopTasksController(
}
}
+ private fun prepareMoveTaskToDesk(
+ wct: WindowContainerTransaction,
+ taskInfo: RunningTaskInfo,
+ deskId: Int,
+ ) {
+ if (!Flags.enableMultipleDesktopsBackend()) return
+ val displayId = taskRepository.getDisplayForDesk(deskId)
+ val displayLayout = displayController.getDisplayLayout(displayId) ?: return
+ val initialBounds = getInitialBounds(displayLayout, taskInfo, displayId)
+ if (canChangeTaskPosition(taskInfo)) {
+ wct.setBounds(taskInfo.token, initialBounds)
+ }
+ desksOrganizer.moveTaskToDesk(wct, deskId = deskId, task = taskInfo)
+ if (useDesktopOverrideDensity()) {
+ wct.setDensityDpi(taskInfo.token, DESKTOP_DENSITY_OVERRIDE)
+ }
+ }
+
/**
* Apply changes to move a freeform task from one display to another, which includes handling
* density changes between displays.
@@ -2377,6 +2481,57 @@ class DesktopTasksController(
)
}
+ private fun activateDefaultDeskInDisplay(
+ displayId: Int,
+ remoteTransition: RemoteTransition? = null,
+ ) {
+ val deskId =
+ checkNotNull(taskRepository.getDefaultDeskId(displayId)) {
+ "Expected a default desk to exist"
+ }
+ activateDesk(deskId, remoteTransition)
+ }
+
+ /** Activates the given desk. */
+ fun activateDesk(deskId: Int, remoteTransition: RemoteTransition? = null) {
+ val displayId = taskRepository.getDisplayForDesk(deskId)
+ val wct = WindowContainerTransaction()
+ if (Flags.enableMultipleDesktopsBackend()) {
+ prepareForDeskActivation(displayId, wct)
+ desksOrganizer.activateDesk(wct, deskId)
+ if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
+ // TODO: 362720497 - do non-running tasks need to be restarted with |wct#startTask|?
+ }
+ taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
+ doesAnyTaskRequireTaskbarRounding(displayId)
+ )
+ } else {
+ bringDesktopAppsToFront(displayId, wct)
+ }
+
+ val transitionType = transitionType(remoteTransition)
+ val handler =
+ remoteTransition?.let {
+ OneShotRemoteHandler(transitions.mainExecutor, remoteTransition)
+ }
+
+ val transition = transitions.startTransition(transitionType, wct, handler)
+ handler?.setTransition(transition)
+ if (Flags.enableMultipleDesktopsBackend()) {
+ desksTransitionObserver.addPendingTransition(
+ DeskTransition.ActivateDesk(
+ token = transition,
+ displayId = displayId,
+ deskId = deskId,
+ )
+ )
+ }
+
+ desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted(
+ FREEFORM_ANIMATION_DURATION
+ )
+ }
+
/** Removes the default desk in the given display. */
@Deprecated("Deprecated with multi-desks.", ReplaceWith("removeDesk()"))
fun removeDefaultDeskInDisplay(displayId: Int) {
@@ -3125,7 +3280,7 @@ class DesktopTasksController(
callback: IMoveToDesktopCallback?,
) {
executeRemoteCallWithTaskPermission(controller, "moveTaskToDesktop") { c ->
- c.moveTaskToDesktop(
+ c.moveTaskToDefaultDeskAndActivate(
taskId,
transitionSource = transitionSource,
remoteTransition = remoteTransition,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index cc3d86c0c056..2ac76f319d32 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -959,9 +959,16 @@ constructor(
super.setupEndDragToDesktop(info, startTransaction, finishTransaction)
val state = requireTransitionState()
- val homeLeash = state.homeChange?.leash ?: error("Expects home leash to be non-null")
- // Hide home on finish to prevent flickering when wallpaper activity flag is enabled
- finishTransaction.hide(homeLeash)
+ val homeLeash = state.homeChange?.leash
+ if (homeLeash == null) {
+ ProtoLog.e(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "DragToDesktop: home leash is null",
+ )
+ } else {
+ // Hide home on finish to prevent flickering when wallpaper activity flag is enabled
+ finishTransaction.hide(homeLeash)
+ }
// Setup freeform tasks before animation
state.freeformTaskChanges.forEach { change ->
val startScale = FREEFORM_TASKS_INITIAL_SCALE
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt
index 47088c0b545a..8c4fd9db050f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt
@@ -30,4 +30,16 @@ sealed class DeskTransition {
val tasks: Set<Int>,
val onDeskRemovedListener: OnDeskRemovedListener?,
) : DeskTransition()
+
+ /** A transition to activate a desk in its display. */
+ data class ActivateDesk(override val token: IBinder, val displayId: Int, val deskId: Int) :
+ DeskTransition()
+
+ /** A transition to activate a desk by moving an outside task to it. */
+ data class ActiveDeskWithTask(
+ override val token: IBinder,
+ val displayId: Int,
+ val deskId: Int,
+ val enterTaskId: Int,
+ ) : DeskTransition()
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
index 5cbb59fbf323..547890a6200a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
@@ -43,6 +43,9 @@ interface DesksOrganizer {
*/
fun getDeskAtEnd(change: TransitionInfo.Change): Int?
+ /** Whether the desk is activate according to the given change at the end of a transition. */
+ fun isDeskActiveAtEnd(change: TransitionInfo.Change, deskId: Int): Boolean
+
/** A callback that is invoked when the desk container is created. */
fun interface OnCreateCallback {
/** Calls back when the [deskId] has been created. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt
index 3e49b8a4538b..6d88c3310a63 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt
@@ -25,7 +25,10 @@ import com.android.wm.shell.desktopmode.DesktopUserRepositories
* Observer of desk-related transitions, such as adding, removing or activating a whole desk. It
* tracks pending transitions and updates repository state once they finish.
*/
-class DesksTransitionObserver(private val desktopUserRepositories: DesktopUserRepositories) {
+class DesksTransitionObserver(
+ private val desktopUserRepositories: DesktopUserRepositories,
+ private val desksOrganizer: DesksOrganizer,
+) {
private val deskTransitions = mutableMapOf<IBinder, DeskTransition>()
/** Adds a pending desk transition to be tracked. */
@@ -53,6 +56,38 @@ class DesksTransitionObserver(private val desktopUserRepositories: DesktopUserRe
desktopRepository.removeDesk(deskTransition.deskId)
deskTransition.onDeskRemovedListener?.onDeskRemoved(displayId, deskId)
}
+ is DeskTransition.ActivateDesk -> {
+ val activeDeskChange =
+ info.changes.find { change ->
+ desksOrganizer.isDeskActiveAtEnd(change, deskTransition.deskId)
+ }
+ activeDeskChange?.let {
+ desktopRepository.setActiveDesk(
+ displayId = deskTransition.displayId,
+ deskId = deskTransition.deskId,
+ )
+ }
+ }
+ is DeskTransition.ActiveDeskWithTask -> {
+ val withTask =
+ info.changes.find { change ->
+ change.taskInfo?.taskId == deskTransition.enterTaskId &&
+ change.taskInfo?.isVisibleRequested == true &&
+ desksOrganizer.getDeskAtEnd(change) == deskTransition.deskId
+ }
+ withTask?.let {
+ desktopRepository.setActiveDesk(
+ displayId = deskTransition.displayId,
+ deskId = deskTransition.deskId,
+ )
+ desktopRepository.addTaskToDesk(
+ displayId = deskTransition.displayId,
+ deskId = deskTransition.deskId,
+ taskId = deskTransition.enterTaskId,
+ isVisible = true,
+ )
+ }
+ }
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
index 79c48c5e9594..5cda76e2f3e0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt
@@ -22,6 +22,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.util.SparseArray
import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
import androidx.core.util.forEach
@@ -88,12 +89,18 @@ class RootTaskDesksOrganizer(
task: RunningTaskInfo,
) {
val root = roots[deskId] ?: error("Root not found for desk: $deskId")
+ wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED)
wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true)
}
override fun getDeskAtEnd(change: TransitionInfo.Change): Int? =
change.taskInfo?.parentTaskId?.takeIf { it in roots }
+ override fun isDeskActiveAtEnd(change: TransitionInfo.Change, deskId: Int): Boolean =
+ change.taskInfo?.taskId == deskId &&
+ change.taskInfo?.isVisibleRequested == true &&
+ change.mode == TRANSIT_TO_FRONT
+
override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) {
if (taskInfo.parentTaskId in roots) {
val deskId = taskInfo.parentTaskId
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
index faa97ac4512f..f50d253ddf42 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
@@ -1,4 +1,5 @@
# Making changes in the Shell
+[Back to home](README.md)
---
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md
index 7070dead9957..9b09904527bf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md
@@ -1,4 +1,5 @@
# Usage of Dagger in the Shell library
+[Back to home](README.md)
---
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
index 09e627c0e02c..dd5827af97d9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
@@ -1,4 +1,5 @@
# Debugging in the Shell
+[Back to home](README.md)
---
@@ -50,6 +51,11 @@ adb shell wm shell protolog enable-text TAG
adb shell wm shell protolog disable-text TAG
```
+### R8 optimizations & ProtoLog
+
+If the APK that the Shell library is included into has R8 optimizations enabled, then you may need
+to update the proguard flags to keep the generated protolog classes (ie. AOSP SystemUI's [proguard.flags](base/packages/SystemUI/proguard_common.flags)).
+
## Winscope Tracing
The Winscope tool is extremely useful in determining what is happening on-screen in both
@@ -57,25 +63,42 @@ WindowManager and SurfaceFlinger. Follow [go/winscope](http://go/winscope-help)
use the tool. This trace will contain all the information about the windows/activities/surfaces on
screen.
-## WindowManager/SurfaceFlinger hierarchy dump
+## WindowManager/SurfaceFlinger/InputDispatcher information
A quick way to view the WindowManager hierarchy without a winscope trace is via the wm dumps:
```shell
adb shell dumpsys activity containers
+# The output lists the containers in the hierarchy from top -> bottom in z-order
+```
+
+To get more information about windows on the screen:
+```shell
+# All windows in WM
+adb shell dumpsys window -a
+# The windows are listed from top -> bottom in z-order
+
+# Visible windows only
+adb shell dumpsys window -a visible
```
Likewise, the SurfaceFlinger hierarchy can be dumped for inspection by running:
```shell
adb shell dumpsys SurfaceFlinger
-# Search output for "Layer Hierarchy"
+# Search output for "Layer Hierarchy", the surfaces in the table are listed bottom -> top in z-order
+```
+
+And the visible input windows can be dumped via:
+```shell
+adb shell dumpsys input
+# Search output for "Windows:", they are ordered top -> bottom in z-order
```
## Tracing global SurfaceControl transaction updates
While Winscope traces are very useful, it sometimes doesn't give you enough information about which
part of the code is initiating the transaction updates. In such cases, it can be helpful to get
-stack traces when specific surface transaction calls are made, which is possible by enabling the
-following system properties for example:
+stack traces when specific surface transaction calls are made (regardless of process), which is
+possible by enabling the following system properties for example:
```shell
# Enabling
adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,setPosition # matches the name of the SurfaceControlTransaction methods
@@ -94,9 +117,16 @@ properties.
It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite
noisy if unfiltered.
-It can sometimes be useful to trace specific logs and when they are applied (sometimes we build
-transactions that can be applied later). You can do this by adding the "merge" and "apply" calls to
-the set of requested calls:
+### Tracing transaction merge & apply
+
+Tracing the method calls on SurfaceControl.Transaction tells you where a change is requested, but
+the changes are not actually committed until the transaction itself is applied. And because
+transactions can be passed across processes, or prepared in advance for later application (ie.
+when restoring state after a Transition), the ordering of the change logs is not always clear
+by itself.
+
+In such cases, you can also enable the "merge" and "apply" calls to get additional information
+about how/when transactions are respectively merged/applied:
```shell
# Enabling
adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,merge,apply # apply will dump logs of each setAlpha or merge call on that tx
@@ -104,6 +134,11 @@ adb reboot
adb logcat -s "SurfaceControlRegistry"
```
+Using those logs, you can first look at where the desired change is called, note the transaction
+id, and then search the logs for where that transaction id is used. If it is merged into another
+transaction, you can continue the search using the merged transaction until you find the final
+transaction which is applied.
+
## Tracing activity starts & finishes in the app process
It's sometimes useful to know when to see a stack trace of when an activity starts in the app code
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md
index 061ae00e2b25..f7707da33189 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md
@@ -1,4 +1,5 @@
# Extending the Shell for Products/OEMs
+[Back to home](README.md)
---
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md
index b489fe8ea1a9..bed0fba453d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md
@@ -1,4 +1,5 @@
# What is the WindowManager Shell
+[Back to home](README.md)
---
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md
index 5e92010d4b68..47383b0a81a0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md
@@ -1,4 +1,5 @@
# Shell & SystemUI
+[Back to home](README.md)
---
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md
index 98af930c4486..b4553131284b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md
@@ -1,4 +1,5 @@
# Testing
+[Back to home](README.md)
---
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
index 837a6dd32ff2..bde722357308 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md
@@ -1,4 +1,5 @@
# Threading
+[Back to home](README.md)
---
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index e8996bc03eeb..a67557bd7bd0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
@@ -62,6 +62,7 @@ import com.android.internal.protolog.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.bubbles.bar.BubbleBarDragListener;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.RemoteCallable;
@@ -80,6 +81,8 @@ import java.util.ArrayList;
import java.util.function.Consumer;
import java.util.function.Function;
+import dagger.Lazy;
+
/**
* Handles the global drag and drop handling for the Shell.
*/
@@ -101,6 +104,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll
private final GlobalDragListener mGlobalDragListener;
private final Transitions mTransitions;
private SplitScreenController mSplitScreen;
+ private Lazy<BubbleBarDragListener> mBubbleBarDragController;
private ShellExecutor mMainExecutor;
private ArrayList<DragAndDropListener> mListeners = new ArrayList<>();
@@ -143,6 +147,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll
IconProvider iconProvider,
GlobalDragListener globalDragListener,
Transitions transitions,
+ Lazy<BubbleBarDragListener> bubbleBarDragController,
ShellExecutor mainExecutor) {
mContext = context;
mShellController = shellController;
@@ -153,6 +158,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll
mIconProvider = iconProvider;
mGlobalDragListener = globalDragListener;
mTransitions = transitions;
+ mBubbleBarDragController = bubbleBarDragController;
mMainExecutor = mainExecutor;
shellInit.addInitCallback(this::onInit, this);
}
@@ -246,7 +252,8 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll
R.layout.global_drop_target, null);
rootView.setOnDragListener(this);
rootView.setVisibility(View.INVISIBLE);
- DragLayoutProvider dragLayout = new DragLayout(context, mSplitScreen, mIconProvider);
+ DragLayoutProvider dragLayout = new DragLayout(context, mSplitScreen,
+ mBubbleBarDragController.get(), mIconProvider);
dragLayout.addDraggingView(rootView);
try {
wm.addView(rootView, layoutParams);
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 5c72cb7f71a6..f0e0295336a7 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
@@ -44,10 +44,8 @@ import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
-import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
-import android.util.Log;
import android.view.DragEvent;
import android.view.SurfaceControl;
import android.view.View;
@@ -66,9 +64,11 @@ import com.android.internal.logging.InstanceId;
import com.android.internal.protolog.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.R;
+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.BubbleBarLocation;
import com.android.wm.shell.splitscreen.SplitScreenController;
import java.io.PrintWriter;
@@ -106,9 +106,11 @@ public class DragLayout extends LinearLayout
private boolean mIsLeftRightSplit;
private SplitDragPolicy.Target mCurrentTarget = null;
+ private final BubbleBarDragListener mBubbleBarDragListener;
+ private final Map<BubbleBarLocation, Rect> mBubbleBarLocations = new HashMap<>();
+ private BubbleBarLocation mCurrentBubbleBarTarget = null;
private DropZoneView mDropZoneView1;
private DropZoneView mDropZoneView2;
-
private int mDisplayMargin;
private int mDividerSize;
private int mLaunchIntentEdgeMargin;
@@ -128,11 +130,14 @@ public class DragLayout extends LinearLayout
// Used with enableFlexibleSplit() flag
@SuppressLint("WrongConstant")
- public DragLayout(Context context, SplitScreenController splitScreenController,
+ public DragLayout(Context context,
+ SplitScreenController splitScreenController,
+ BubbleBarDragListener bubbleBarDragListener,
IconProvider iconProvider) {
super(context);
mSplitScreenController = splitScreenController;
mIconProvider = iconProvider;
+ mBubbleBarDragListener = bubbleBarDragListener;
mPolicy = new SplitDragPolicy(context, splitScreenController, this);
mStatusBarManager = context.getSystemService(StatusBarManager.class);
mLastConfiguration.setTo(context.getResources().getConfiguration());
@@ -188,6 +193,12 @@ public class DragLayout extends LinearLayout
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
updateTouchableRegion();
+ updateBubbleBarRegions(l, t, r, b);
+ }
+
+ private void updateBubbleBarRegions(int l, int t, int r, int b) {
+ mBubbleBarLocations.clear();
+ mBubbleBarLocations.putAll(mBubbleBarDragListener.getBubbleBarDropZones(l, t, r, b));
}
/**
@@ -514,17 +525,18 @@ public class DragLayout extends LinearLayout
if (mHasDropped) {
return;
}
+ // if event is over the bubble don't let split handle it
+ if (interceptBubbleBarEvent(x, y)) {
+ mLastPosition.set(x, y);
+ return;
+ }
// Find containing region, if the same as mCurrentRegion, then skip, otherwise, animate the
// visibility of the current region
SplitDragPolicy.Target target = mPolicy.getTargetAtLocation(x, y);
if (mCurrentTarget != target) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current target: %s", target);
if (target == null) {
- // Animating to no target
- animateSplitContainers(false, null /* animCompleteCallback */);
- if (enableFlexibleSplit()) {
- animateHighlight(target);
- }
+ animateToNoTarget();
} else if (mCurrentTarget == null) {
if (mPolicy.getNumTargets() == 1) {
animateFullscreenContainer(true);
@@ -565,6 +577,45 @@ public class DragLayout extends LinearLayout
mLastPosition.set(x, y);
}
+ private boolean interceptBubbleBarEvent(int x, int y) {
+ BubbleBarLocation bubbleBarLocation = getBubbleBarLocation(x, y);
+ boolean isOverTheBubbleBar = bubbleBarLocation != null;
+ if (mCurrentBubbleBarTarget != bubbleBarLocation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current bubble bar location: %s",
+ isOverTheBubbleBar);
+ mCurrentBubbleBarTarget = bubbleBarLocation;
+ if (isOverTheBubbleBar) {
+ mBubbleBarDragListener.onDragItemOverBubbleBarDragZone(bubbleBarLocation);
+ if (mCurrentTarget != null) {
+ animateToNoTarget();
+ mCurrentTarget = null;
+ }
+ } else {
+ mBubbleBarDragListener.onItemDraggedOutsideBubbleBarDropZone();
+ }
+ //TODO(b/388894910): handle accessibility
+ }
+ return isOverTheBubbleBar;
+ }
+
+ @Nullable
+ private BubbleBarLocation getBubbleBarLocation(int x, int y) {
+ for (BubbleBarLocation location : mBubbleBarLocations.keySet()) {
+ if (mBubbleBarLocations.get(location).contains(x, y)) {
+ return location;
+ }
+ }
+ return null;
+ }
+
+ private void animateToNoTarget() {
+ // Animating to no target
+ animateSplitContainers(false, null /* animCompleteCallback */);
+ if (enableFlexibleSplit()) {
+ animateHighlight(null);
+ }
+ }
+
/**
* Hides the drag layout and animates out the visible drop targets.
*/
@@ -596,11 +647,13 @@ public class DragLayout extends LinearLayout
*/
public boolean drop(DragEvent event, @NonNull SurfaceControl dragSurface,
@Nullable WindowContainerToken hideTaskToken, Runnable dropCompleteCallback) {
- final boolean handledDrop = mCurrentTarget != null;
+ final boolean handledDrop = mCurrentTarget != null || mCurrentBubbleBarTarget != null;
mHasDropped = true;
// Process the drop
mPolicy.onDropped(mCurrentTarget, hideTaskToken);
+ //TODO(b/388894910) add info about the application
+ mBubbleBarDragListener.onItemDroppedOverBubbleBarDragZone(mCurrentBubbleBarTarget);
// Start animating the drop UI out with the drag surface
hide(event, dropCompleteCallback);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
index 21b0820f523a..e17587ff18bc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java
@@ -16,14 +16,10 @@
package com.android.wm.shell.pip2.phone;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.view.SurfaceControl;
-import android.window.DisplayAreaInfo;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
@@ -32,20 +28,14 @@ import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
-import com.android.window.flags.Flags;
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.pip.PipBoundsState;
-import com.android.wm.shell.desktopmode.DesktopUserRepositories;
-import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
+import com.android.wm.shell.common.pip.PipDesktopState;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import java.util.Objects;
-import java.util.Optional;
-
/**
* Scheduler for Shell initiated PiP transitions and animations.
*/
@@ -56,10 +46,7 @@ public class PipScheduler {
private final PipBoundsState mPipBoundsState;
private final ShellExecutor mMainExecutor;
private final PipTransitionState mPipTransitionState;
- private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional;
- private final Optional<DesktopWallpaperActivityTokenProvider>
- mDesktopWallpaperActivityTokenProviderOptional;
- private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
+ private final PipDesktopState mPipDesktopState;
private PipTransitionController mPipTransitionController;
private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
mSurfaceControlTransactionFactory;
@@ -72,18 +59,12 @@ public class PipScheduler {
PipBoundsState pipBoundsState,
ShellExecutor mainExecutor,
PipTransitionState pipTransitionState,
- Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
- Optional<DesktopWallpaperActivityTokenProvider>
- desktopWallpaperActivityTokenProviderOptional,
- RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+ PipDesktopState pipDesktopState) {
mContext = context;
mPipBoundsState = pipBoundsState;
mMainExecutor = mainExecutor;
mPipTransitionState = pipTransitionState;
- mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional;
- mDesktopWallpaperActivityTokenProviderOptional =
- desktopWallpaperActivityTokenProviderOptional;
- mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
+ mPipDesktopState = pipDesktopState;
mSurfaceControlTransactionFactory =
new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
@@ -105,7 +86,7 @@ public class PipScheduler {
wct.setBounds(pipTaskToken, null);
// if we are hitting a multi-activity case
// windowing mode change will reparent to original host task
- wct.setWindowingMode(pipTaskToken, getOutPipWindowingMode());
+ wct.setWindowingMode(pipTaskToken, mPipDesktopState.getOutPipWindowingMode());
return wct;
}
@@ -235,55 +216,6 @@ public class PipScheduler {
maybeUpdateMovementBounds();
}
- /** Returns whether the display is in freeform windowing mode. */
- private boolean isDisplayInFreeform() {
- final DisplayAreaInfo tdaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(
- Objects.requireNonNull(mPipTransitionState.getPipTaskInfo()).displayId);
- if (tdaInfo != null) {
- return tdaInfo.configuration.windowConfiguration.getWindowingMode()
- == WINDOWING_MODE_FREEFORM;
- }
- return false;
- }
-
- /** Returns whether PiP is exiting while we're in desktop mode. */
- private boolean isPipExitingToDesktopMode() {
- // Early return if PiP in Desktop Windowing is not supported.
- if (!Flags.enableDesktopWindowingPip() || mDesktopUserRepositoriesOptional.isEmpty()
- || mDesktopWallpaperActivityTokenProviderOptional.isEmpty()) {
- return false;
- }
- final int displayId = Objects.requireNonNull(
- mPipTransitionState.getPipTaskInfo()).displayId;
- return mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(displayId)
- > 0
- || mDesktopWallpaperActivityTokenProviderOptional.get().isWallpaperActivityVisible(
- displayId)
- || isDisplayInFreeform();
- }
-
- /**
- * The windowing mode to restore to when resizing out of PIP direction. Defaults to undefined
- * and can be overridden to restore to an alternate windowing mode.
- */
- private int getOutPipWindowingMode() {
- // If we are exiting PiP while the device is in Desktop mode (the task should expand to
- // freeform windowing mode):
- // 1) If the display windowing mode is freeform, set windowing mode to undefined so it will
- // resolve the windowing mode to the display's windowing mode.
- // 2) If the display windowing mode is not freeform, set windowing mode to freeform.
- if (isPipExitingToDesktopMode()) {
- if (isDisplayInFreeform()) {
- return WINDOWING_MODE_UNDEFINED;
- } else {
- return WINDOWING_MODE_FREEFORM;
- }
- }
-
- // By default, or if the task is going to fullscreen, reset the windowing mode to undefined.
- return WINDOWING_MODE_UNDEFINED;
- }
-
@VisibleForTesting
void setSurfaceControlTransactionFactory(
@NonNull PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java
index d735375b0fc9..dbcbf3663827 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java
@@ -132,8 +132,9 @@ public class PipTaskListener implements ShellTaskOrganizer.TaskListener,
"onTaskInfoChanged: %s, state=%s oldParams=%s newParams=%s",
taskInfo.topActivity, mPipTransitionState, mPictureInPictureParams, params);
setPictureInPictureParams(params);
+ // Note: params is nullable while mPictureInPictureParams is never null
float newAspectRatio = mPictureInPictureParams.getAspectRatioFloat();
- if (params.hasSetAspectRatio()
+ if (mPictureInPictureParams.hasSetAspectRatio()
&& mPipBoundsAlgorithm.isValidPictureInPictureAspectRatio(newAspectRatio)
&& PipUtils.aspectRatioChanged(newAspectRatio, mPipBoundsState.getAspectRatio())) {
mPipTransitionState.setOnIdlePipTransitionStateRunnable(() -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 78aa686a3a0e..bb9b479524e5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -56,19 +56,16 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.Nullable;
import com.android.internal.util.Preconditions;
-import com.android.window.flags.Flags;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.ComponentUtils;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDesktopState;
import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.common.pip.PipUtils;
-import com.android.wm.shell.desktopmode.DesktopRepository;
-import com.android.wm.shell.desktopmode.DesktopUserRepositories;
-import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
@@ -79,8 +76,6 @@ import com.android.wm.shell.shared.pip.PipContentOverlay;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
-import java.util.Optional;
-
/**
* Implementation of transitions for PiP on phone.
*/
@@ -117,9 +112,7 @@ public class PipTransition extends PipTransitionController implements
private final PipDisplayLayoutState mPipDisplayLayoutState;
private final DisplayController mDisplayController;
private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper;
- private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional;
- private final Optional<DesktopWallpaperActivityTokenProvider>
- mDesktopWallpaperActivityTokenProviderOptional;
+ private final PipDesktopState mPipDesktopState;
//
// Transition caches
@@ -159,9 +152,7 @@ public class PipTransition extends PipTransitionController implements
PipDisplayLayoutState pipDisplayLayoutState,
PipUiStateChangeController pipUiStateChangeController,
DisplayController displayController,
- Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
- Optional<DesktopWallpaperActivityTokenProvider>
- desktopWallpaperActivityTokenProviderOptional) {
+ PipDesktopState pipDesktopState) {
super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
pipBoundsAlgorithm);
@@ -174,9 +165,7 @@ public class PipTransition extends PipTransitionController implements
mPipDisplayLayoutState = pipDisplayLayoutState;
mDisplayController = displayController;
mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mContext);
- mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional;
- mDesktopWallpaperActivityTokenProviderOptional =
- desktopWallpaperActivityTokenProviderOptional;
+ mPipDesktopState = pipDesktopState;
}
@Override
@@ -802,7 +791,7 @@ public class PipTransition extends PipTransitionController implements
&& getFixedRotationDelta(info, pipTaskChange) == ROTATION_90) {
adjustedSourceRectHint.offset(cutoutInsets.left, cutoutInsets.top);
}
- if (Flags.enableDesktopWindowingPip()) {
+ if (mPipDesktopState.isDesktopWindowingPipEnabled()) {
adjustedSourceRectHint.offset(-pipActivityChange.getStartAbsBounds().left,
-pipActivityChange.getStartAbsBounds().top);
}
@@ -862,7 +851,7 @@ public class PipTransition extends PipTransitionController implements
// If PiP is enabled on Connected Displays, update PipDisplayLayoutState to have the correct
// display info that PiP is entering in.
- if (Flags.enableConnectedDisplaysPip()) {
+ if (mPipDesktopState.isConnectedDisplaysPipEnabled()) {
final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(
pipTask.displayId);
if (displayLayout != null) {
@@ -910,12 +899,7 @@ public class PipTransition extends PipTransitionController implements
// Since opening a new task while in Desktop Mode always first open in Fullscreen
// until DesktopMode Shell code resolves it to Freeform, PipTransition will get a
// possibility to handle it also. In this case return false to not have it enter PiP.
- final boolean isInDesktopSession = !mDesktopUserRepositoriesOptional.isEmpty()
- && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(
- pipTask.displayId) > 0
- || mDesktopUserRepositoriesOptional.get().getCurrent()
- .isMinimizedPipPresentInDisplay(pipTask.displayId));
- if (isInDesktopSession) {
+ if (mPipDesktopState.isPipEnteringInDesktopMode(pipTask)) {
return false;
}
@@ -1089,26 +1073,13 @@ public class PipTransition extends PipTransitionController implements
"Unexpected bundle for " + mPipTransitionState);
break;
case PipTransitionState.EXITED_PIP:
- final TaskInfo pipTask = mPipTransitionState.getPipTaskInfo();
- final boolean desktopPipEnabled = Flags.enableDesktopWindowingPip()
- && mDesktopUserRepositoriesOptional.isPresent()
- && mDesktopWallpaperActivityTokenProviderOptional.isPresent();
- if (desktopPipEnabled && pipTask != null) {
- final DesktopRepository desktopRepository =
- mDesktopUserRepositoriesOptional.get().getCurrent();
- final boolean wallpaperIsVisible =
- mDesktopWallpaperActivityTokenProviderOptional.get()
- .isWallpaperActivityVisible(pipTask.displayId);
- if (desktopRepository.getVisibleTaskCount(pipTask.displayId) == 0
- && wallpaperIsVisible) {
- mTransitions.startTransition(
- TRANSIT_TO_BACK,
- new WindowContainerTransaction().reorder(
- mDesktopWallpaperActivityTokenProviderOptional.get()
- .getToken(pipTask.displayId), /* onTop= */ false),
- null
- );
- }
+ if (mPipDesktopState.shouldExitPipExitDesktopMode()) {
+ mTransitions.startTransition(
+ TRANSIT_TO_BACK,
+ mPipDesktopState.getWallpaperActivityTokenWct(
+ mPipTransitionState.getPipTaskInfo().getDisplayId()),
+ null /* firstHandler */
+ );
}
mPipTransitionState.setPinnedTaskLeash(null);
mPipTransitionState.setPipTaskInfo(null);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitMultiDisplayProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitMultiDisplayProvider.java
new file mode 100644
index 000000000000..d2e57e51762b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitMultiDisplayProvider.java
@@ -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.wm.shell.splitscreen;
+
+import android.window.WindowContainerToken;
+
+public interface SplitMultiDisplayProvider {
+ /**
+ * Returns the WindowContainerToken for the root of the given display ID.
+ *
+ * @param displayId The ID of the display.
+ * @return The {@link WindowContainerToken} associated with the display's root task.
+ */
+ WindowContainerToken getDisplayRootForDisplayId(int displayId);
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index ae0159263364..e9f8a4a86d27 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -321,6 +321,10 @@ public class SplitScreenController implements SplitDragPolicy.Starter,
return mStageCoordinator;
}
+ public SplitMultiDisplayProvider getMultiDisplayProvider() {
+ return mStageCoordinator;
+ }
+
@Nullable
public ActivityManager.RunningTaskInfo getTaskInfo(@SplitPosition int splitPosition) {
if (!isSplitScreenVisible() || splitPosition == SPLIT_POSITION_UNDEFINED) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 6783df8f8324..13940b1da257 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -189,7 +189,8 @@ import java.util.function.Predicate;
*/
public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
DisplayController.OnDisplaysChangedListener, Transitions.TransitionHandler,
- ShellTaskOrganizer.TaskListener, StageTaskListener.StageListenerCallbacks {
+ ShellTaskOrganizer.TaskListener, StageTaskListener.StageListenerCallbacks,
+ SplitMultiDisplayProvider {
private static final String TAG = StageCoordinator.class.getSimpleName();
@@ -287,6 +288,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitTransitions.registerSplitAnimListener(listener, executor);
}
+ @Override
+ public WindowContainerToken getDisplayRootForDisplayId(int displayId) {
+ if (displayId == DEFAULT_DISPLAY) {
+ return mRootTaskInfo != null ? mRootTaskInfo.token : null;
+ }
+
+ // TODO(b/393217881): support different root task on external displays.
+ return null; // Return null for unknown display IDs
+ }
+
class SplitRequest {
@SplitPosition
int mActivatePosition;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 6a2a7b6becdc..17d619c9bee8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -131,6 +131,7 @@ import com.android.wm.shell.recents.RecentsTransitionStateListener;
import com.android.wm.shell.shared.FocusTransitionListener;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper;
import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource;
@@ -754,7 +755,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
// App sometimes draws before the insets from WindowDecoration#relayout have
// been added, so they must be added here
decoration.addCaptionInset(wct);
- mDesktopTasksController.moveTaskToDesktop(taskId, wct, source,
+ mDesktopTasksController.moveTaskToDefaultDeskAndActivate(taskId, wct, source,
/* remoteTransition= */ null, /* moveToDesktopCallback */ null);
decoration.closeHandleMenu();
@@ -1439,13 +1440,17 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
mDragToDesktopAnimationStartBounds.set(
relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds());
boolean dragFromStatusBarAllowed = false;
+ final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode();
if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
// In proto2 any full screen or multi-window task can be dragged to
// freeform.
- final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode();
dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN
|| windowingMode == WINDOWING_MODE_MULTI_WINDOW;
}
+ if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
+ // TODO(b/388851898): add support for split screen (multi-window wm mode)
+ dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN;
+ }
final boolean shouldStartTransitionDrag =
relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)
|| DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index c1a6240516c1..afb234899339 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -483,7 +483,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) {
Trace.beginSection("DesktopModeWindowDecoration#relayout");
- if (Flags.enableDesktopWindowingAppToWeb()) {
+ if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_APP_TO_WEB.isTrue()) {
setCapturedLink(taskInfo.capturedLink, taskInfo.capturedLinkTimestamp);
}
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 32a2f8294877..1d9564948772 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
@@ -252,6 +252,7 @@ class HandleMenu(
view = handleMenuView.rootView,
forciblyShownTypes = if (forceShowSystemBars) { systemBars() } else { 0 },
ignoreCutouts = Flags.showAppHandleLargeScreens()
+ || BubbleAnythingFlagHelper.enableBubbleToFullscreen()
)
} else {
parentDecor.addWindow(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
index 1264c013faf5..2948fdaf16af 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
@@ -41,6 +41,7 @@ import com.android.internal.policy.SystemBarUtils
import com.android.window.flags.Flags
import com.android.wm.shell.R
import com.android.wm.shell.shared.animation.Interpolators
+import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
import com.android.wm.shell.windowdecor.WindowManagerWrapper
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
@@ -146,6 +147,7 @@ internal class AppHandleViewHolder(
taskInfo.taskId, handlePosition.x, handlePosition.y, handleWidth, handleHeight,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
ignoreCutouts = Flags.showAppHandleLargeScreens()
+ || BubbleAnythingFlagHelper.enableBubbleToFullscreen()
)
val view = statusBarInputLayer?.view ?: error("Unable to find statusBarInputLayer View")
val lp = statusBarInputLayer?.lp ?: error("Unable to find statusBarInputLayer " +
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml
index 1bbbefadaa03..8fc974d4381e 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml
@@ -47,6 +47,8 @@
<uses-permission android:name="android.permission.STATUS_BAR_SERVICE" />
<!-- Allow the test to connect to perfetto trace processor -->
<uses-permission android:name="android.permission.INTERNET"/>
+ <!-- Use trusted virtual displays to emulate an external display -->
+ <uses-permission android:name="android.permission.ADD_TRUSTED_DISPLAY"/>
<!-- Allow the test to write directly to /sdcard/ and connect to trace processor -->
<application android:requestLegacyExternalStorage="true"
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
index ab1ac1a0efa3..9b402734a4c4 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt
@@ -19,7 +19,7 @@ package com.android.wm.shell.flicker
import android.tools.PlatformConsts.DESKTOP_MODE_MINIMUM_WINDOW_HEIGHT
import android.tools.PlatformConsts.DESKTOP_MODE_MINIMUM_WINDOW_WIDTH
import android.tools.flicker.AssertionInvocationGroup
-import android.tools.flicker.assertors.assertions.AppLayerIncreasesInSize
+import android.tools.flicker.assertors.assertions.ResizeVeilKeepsIncreasingInSize
import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd
import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways
import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart
@@ -36,6 +36,7 @@ import android.tools.flicker.assertors.assertions.AppWindowHasMaxDisplayHeight
import android.tools.flicker.assertors.assertions.AppWindowHasMaxDisplayWidth
import android.tools.flicker.assertors.assertions.AppWindowHasSizeOfAtLeast
import android.tools.flicker.assertors.assertions.AppWindowInsideDisplayBoundsAtEnd
+import android.tools.flicker.assertors.assertions.AppWindowIsBiggerThanInitialBoundsAtEnd
import android.tools.flicker.assertors.assertions.AppWindowIsInvisibleAtEnd
import android.tools.flicker.assertors.assertions.AppWindowIsVisibleAlways
import android.tools.flicker.assertors.assertions.AppWindowMaintainsAspectRatioAlways
@@ -168,9 +169,12 @@ class DesktopModeFlickerScenarios {
TaggedCujTransitionMatcher(associatedTransitionRequired = false)
)
.build(),
- // TODO(373638597) Add AppLayerIncreasesInSize assertion
- assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS
- )
+ assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
+ listOf(
+ ResizeVeilKeepsIncreasingInSize(DESKTOP_MODE_APP),
+ AppWindowIsBiggerThanInitialBoundsAtEnd(DESKTOP_MODE_APP),
+ ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING })
+ )
val EDGE_RESIZE =
FlickerConfigEntry(
@@ -184,7 +188,8 @@ class DesktopModeFlickerScenarios {
.build(),
assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
listOf(
- AppLayerIncreasesInSize(DESKTOP_MODE_APP),
+ ResizeVeilKeepsIncreasingInSize(DESKTOP_MODE_APP),
+ AppWindowIsBiggerThanInitialBoundsAtEnd(DESKTOP_MODE_APP),
).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
)
@@ -223,9 +228,9 @@ class DesktopModeFlickerScenarios {
assertions =
AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
listOf(
- // TODO(373638597) Add AppLayerIncreasesInSize assertion
AppWindowHasMaxDisplayHeight(DESKTOP_MODE_APP),
- AppWindowHasMaxDisplayWidth(DESKTOP_MODE_APP)
+ AppWindowHasMaxDisplayWidth(DESKTOP_MODE_APP),
+ ResizeVeilKeepsIncreasingInSize(DESKTOP_MODE_APP),
).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
)
@@ -368,7 +373,7 @@ class DesktopModeFlickerScenarios {
),
assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
listOf(
- AppLayerIncreasesInSize(DESKTOP_MODE_APP),
+ ResizeVeilKeepsIncreasingInSize(DESKTOP_MODE_APP),
AppWindowHasMaxDisplayHeight(DESKTOP_MODE_APP),
AppWindowHasMaxDisplayWidth(DESKTOP_MODE_APP)
).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
@@ -393,7 +398,7 @@ class DesktopModeFlickerScenarios {
assertions =
AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS +
listOf(
- AppLayerIncreasesInSize(DESKTOP_MODE_APP),
+ ResizeVeilKeepsIncreasingInSize(DESKTOP_MODE_APP),
AppWindowMaintainsAspectRatioAlways(DESKTOP_MODE_APP),
AppWindowHasMaxBoundsInOnlyOneDimension(DESKTOP_MODE_APP)
).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
@@ -541,5 +546,29 @@ class DesktopModeFlickerScenarios {
AppWindowBecomesPinned(DESKTOP_MODE_APP),
).associateBy({ it }, { AssertionInvocationGroup.BLOCKING })
)
+
+ val OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED =
+ FlickerConfigEntry(
+ scenarioId = ScenarioId("OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED"),
+ extractor =
+ ShellTransitionScenarioExtractor(
+ transitionMatcher =
+ object : ITransitionMatcher {
+ override fun findAll(
+ transitions: Collection<Transition>
+ ): Collection<Transition> {
+ return listOf(transitions
+ .filter { it.type == TransitionType.OPEN }
+ .maxByOrNull { it.id }!!)
+ }
+ }
+ ),
+ assertions =
+ listOf(
+ AppWindowBecomesVisible(DESKTOP_MODE_APP),
+ AppWindowOnTopAtEnd(DESKTOP_MODE_APP),
+ AppWindowBecomesVisible(DESKTOP_WALLPAPER),
+ ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ )
}
}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppWithExternalDisplayConnected.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppWithExternalDisplayConnected.kt
new file mode 100644
index 000000000000..66d2ea95c67f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppWithExternalDisplayConnected.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker
+
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED
+import com.android.wm.shell.scenarios.OpenAppWithExternalDisplayConnected
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Open an app on the default display when an external display is connected.
+ *
+ * Assert that the app launches in desktop mode.
+ */
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class OpenAppWithExternalDisplayConnected : OpenAppWithExternalDisplayConnected() {
+ @ExpectedScenarios(["OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED"])
+ @Test
+ override fun openAppWithExternalDisplayConnected() = super.openAppWithExternalDisplayConnected()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig()
+ .use(FlickerServiceConfig.DEFAULT)
+ .use(OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/OpenAppWithExternalDisplayConnectedTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/OpenAppWithExternalDisplayConnectedTest.kt
new file mode 100644
index 000000000000..cc9a799fb50c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/OpenAppWithExternalDisplayConnectedTest.kt
@@ -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.wm.shell.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.scenarios.OpenAppWithExternalDisplayConnected
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/* Functional test for [OpenAppWithExternalDisplayConnected]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+class OpenAppWithExternalDisplayConnectedTest : OpenAppWithExternalDisplayConnected()
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt
index 8d04749d76a5..2115f70faad0 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt
@@ -50,7 +50,7 @@ constructor(
@Test
open fun enterDesktopWithDrag() {
// By default this method uses drag to desktop
- testApp.enterDesktopMode(wmHelper, device)
+ testApp.enterDesktopMode(wmHelper, device, shouldUseDragToDesktop = true)
}
@After
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt
index 814478af67c1..9a1919304675 100644
--- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt
@@ -62,7 +62,7 @@ constructor(
@Test
open fun reenterDesktopWithDrag() {
// By default this method uses drag to desktop
- testApp.enterDesktopMode(wmHelper, device)
+ testApp.enterDesktopMode(wmHelper, device, shouldUseDragToDesktop = true)
}
@After
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
new file mode 100644
index 000000000000..81c46f13b384
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.scenarios
+
+import android.app.Instrumentation
+import android.content.Context
+import android.hardware.display.DisplayManager
+import android.hardware.display.VirtualDisplay
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.flicker.rules.ChangeDisplayOrientationRule
+import android.tools.traces.parsers.WindowManagerStateHelper
+import android.util.DisplayMetrics
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import com.android.wm.shell.ExtendedDisplaySettingsSession
+import com.android.wm.shell.Utils
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+/**
+ * Base scenario test for launching an app in desktop mode by default when an external display is
+ * connected.
+ */
+@Ignore("Test Base Class")
+abstract class OpenAppWithExternalDisplayConnected
+constructor(private val rotation: Rotation = Rotation.ROTATION_0) {
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val tapl = LauncherInstrumentation()
+ private val wmHelper = WindowManagerStateHelper(instrumentation)
+ private val device = UiDevice.getInstance(instrumentation)
+ private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+ private val displayManager =
+ instrumentation.getContext().getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+ private var virtualDisplay: VirtualDisplay? = null
+
+ private val extendedDisplaySettingsSession =
+ ExtendedDisplaySettingsSession(instrumentation.context.contentResolver)
+
+ @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+ Assume.assumeTrue(Flags.enableDisplayWindowingModeSwitching())
+ tapl.setEnableRotation(true)
+ tapl.setExpectedRotation(rotation.value)
+ ChangeDisplayOrientationRule.setRotation(rotation)
+ extendedDisplaySettingsSession.open()
+ virtualDisplay = displayManager.createVirtualDisplay(
+ /* displayName= */ DISPLAY_NAME,
+ /* width= */ DISPLAY_WIDTH,
+ /* height= */ DISPLAY_HEIGHT,
+ /* densityDpi= */ DisplayMetrics.DENSITY_DEFAULT,
+ /* surface= */ null,
+ /* flags= */ DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC or
+ DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY or
+ DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED
+ )
+ }
+
+ @Test
+ open fun openAppWithExternalDisplayConnected() {
+ testApp.open()
+ }
+
+ @After
+ fun teardown() {
+ testApp.exit(wmHelper)
+ virtualDisplay?.let {
+ it.release()
+ }
+ extendedDisplaySettingsSession.close()
+ }
+
+ companion object {
+ const val DISPLAY_NAME = "testVirtualDisplay"
+ const val DISPLAY_HEIGHT = 600
+ const val DISPLAY_WIDTH = 800
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/ExtendedDisplaySettingsSession.kt b/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/ExtendedDisplaySettingsSession.kt
new file mode 100644
index 000000000000..0b2aacd00aa6
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/ExtendedDisplaySettingsSession.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell
+
+import android.content.ContentResolver
+import android.provider.Settings
+import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
+
+class ExtendedDisplaySettingsSession(private val contentResolver: ContentResolver) {
+ private val settingName = DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
+ private val initialValue = Settings.Global.getInt(contentResolver, settingName, 0)
+
+ fun open() {
+ Settings.Global.putInt(contentResolver, settingName, 1)
+ }
+
+ fun close() {
+ Settings.Global.putInt(contentResolver, settingName, initialValue)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
index 417b43a9c6c0..22cc65d8ffaf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
@@ -34,7 +34,6 @@ import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView
-import com.android.wm.shell.bubbles.properties.BubbleProperties
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayImeController
import com.android.wm.shell.common.DisplayInsetsController
@@ -143,7 +142,7 @@ class BubbleViewInfoTest : ShellTestCase() {
mock<Transitions>(),
mock<SyncTransactionQueue>(),
mock<IWindowManager>(),
- mock<BubbleProperties>()
+ BubbleResizabilityChecker()
)
val bubbleStackViewManager = BubbleStackViewManager.fromBubbleController(bubbleController)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java
new file mode 100644
index 000000000000..75ad621e1cad
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.common.pip;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+
+import static com.android.window.flags.Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_PIP;
+import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.platform.test.annotations.EnableFlags;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.window.DisplayAreaInfo;
+import android.window.WindowContainerToken;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
+import com.android.wm.shell.desktopmode.DesktopRepository;
+import com.android.wm.shell.desktopmode.DesktopUserRepositories;
+import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+/**
+ * Unit test against {@link PipDesktopState}.
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP)
+public class PipDesktopStateTest {
+ @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState;
+ @Mock private Optional<DesktopUserRepositories> mMockDesktopUserRepositoriesOptional;
+ @Mock private Optional<DesktopWallpaperActivityTokenProvider>
+ mMockDesktopWallpaperActivityTokenProviderOptional;
+ @Mock private DesktopUserRepositories mMockDesktopUserRepositories;
+ @Mock private DesktopWallpaperActivityTokenProvider mMockDesktopWallpaperActivityTokenProvider;
+ @Mock private DesktopRepository mMockDesktopRepository;
+ @Mock private RootTaskDisplayAreaOrganizer mMockRootTaskDisplayAreaOrganizer;
+ @Mock private ActivityManager.RunningTaskInfo mMockTaskInfo;
+
+ private static final int DISPLAY_ID = 1;
+ private DisplayAreaInfo mDefaultTda;
+ private PipDesktopState mPipDesktopState;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mMockDesktopUserRepositoriesOptional.get()).thenReturn(mMockDesktopUserRepositories);
+ when(mMockDesktopWallpaperActivityTokenProviderOptional.get()).thenReturn(
+ mMockDesktopWallpaperActivityTokenProvider);
+ when(mMockDesktopUserRepositories.getCurrent()).thenReturn(mMockDesktopRepository);
+ when(mMockDesktopUserRepositoriesOptional.isPresent()).thenReturn(true);
+ when(mMockDesktopWallpaperActivityTokenProviderOptional.isPresent()).thenReturn(true);
+
+ when(mMockTaskInfo.getDisplayId()).thenReturn(DISPLAY_ID);
+ when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(DISPLAY_ID);
+
+ mDefaultTda = new DisplayAreaInfo(Mockito.mock(WindowContainerToken.class), DISPLAY_ID, 0);
+ when(mMockRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DISPLAY_ID)).thenReturn(
+ mDefaultTda);
+
+ mPipDesktopState = new PipDesktopState(mMockPipDisplayLayoutState,
+ mMockDesktopUserRepositoriesOptional,
+ mMockDesktopWallpaperActivityTokenProviderOptional,
+ mMockRootTaskDisplayAreaOrganizer);
+ }
+
+ @Test
+ public void isDesktopWindowingPipEnabled_returnsTrue() {
+ assertTrue(mPipDesktopState.isDesktopWindowingPipEnabled());
+ }
+
+ @Test
+ public void isDesktopWindowingPipEnabled_desktopRepositoryEmpty_returnsFalse() {
+ when(mMockDesktopUserRepositoriesOptional.isPresent()).thenReturn(false);
+
+ assertFalse(mPipDesktopState.isDesktopWindowingPipEnabled());
+ }
+
+ @Test
+ public void isDesktopWindowingPipEnabled_desktopWallpaperEmpty_returnsFalse() {
+ when(mMockDesktopWallpaperActivityTokenProviderOptional.isPresent()).thenReturn(false);
+
+ assertFalse(mPipDesktopState.isDesktopWindowingPipEnabled());
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_CONNECTED_DISPLAYS_PIP)
+ public void isConnectedDisplaysPipEnabled_returnsTrue() {
+ assertTrue(mPipDesktopState.isConnectedDisplaysPipEnabled());
+ }
+
+ @Test
+ public void isPipEnteringInDesktopMode_visibleCountZero_minimizedPipPresent_returnsTrue() {
+ when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(0);
+ when(mMockDesktopRepository.isMinimizedPipPresentInDisplay(DISPLAY_ID)).thenReturn(true);
+
+ assertTrue(mPipDesktopState.isPipEnteringInDesktopMode(mMockTaskInfo));
+ }
+
+ @Test
+ public void isPipEnteringInDesktopMode_visibleCountNonzero_minimizedPipAbsent_returnsTrue() {
+ when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(1);
+ when(mMockDesktopRepository.isMinimizedPipPresentInDisplay(DISPLAY_ID)).thenReturn(false);
+
+ assertTrue(mPipDesktopState.isPipEnteringInDesktopMode(mMockTaskInfo));
+ }
+
+ @Test
+ public void isPipEnteringInDesktopMode_visibleCountZero_minimizedPipAbsent_returnsFalse() {
+ when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(0);
+ when(mMockDesktopRepository.isMinimizedPipPresentInDisplay(DISPLAY_ID)).thenReturn(false);
+
+ assertFalse(mPipDesktopState.isPipEnteringInDesktopMode(mMockTaskInfo));
+ }
+
+ @Test
+ public void shouldExitPipExitDesktopMode_visibleCountZero_wallpaperInvisible_returnsFalse() {
+ when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(0);
+ when(mMockDesktopWallpaperActivityTokenProvider.isWallpaperActivityVisible(
+ DISPLAY_ID)).thenReturn(false);
+
+ assertFalse(mPipDesktopState.shouldExitPipExitDesktopMode());
+ }
+
+ @Test
+ public void shouldExitPipExitDesktopMode_visibleCountNonzero_wallpaperVisible_returnsFalse() {
+ when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(1);
+ when(mMockDesktopWallpaperActivityTokenProvider.isWallpaperActivityVisible(
+ DISPLAY_ID)).thenReturn(true);
+
+ assertFalse(mPipDesktopState.shouldExitPipExitDesktopMode());
+ }
+
+ @Test
+ public void shouldExitPipExitDesktopMode_visibleCountZero_wallpaperVisible_returnsTrue() {
+ when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(0);
+ when(mMockDesktopWallpaperActivityTokenProvider.isWallpaperActivityVisible(
+ DISPLAY_ID)).thenReturn(true);
+
+ assertTrue(mPipDesktopState.shouldExitPipExitDesktopMode());
+ }
+
+ @Test
+ public void getOutPipWindowingMode_exitToDesktop_displayFreeform_returnsUndefined() {
+ // Set visible task count to 1 so isPipExitingToDesktopMode returns true
+ when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(1);
+ setDisplayWindowingMode(WINDOWING_MODE_FREEFORM);
+
+ assertEquals(WINDOWING_MODE_UNDEFINED, mPipDesktopState.getOutPipWindowingMode());
+ }
+
+ @Test
+ public void getOutPipWindowingMode_exitToDesktop_displayFullscreen_returnsFreeform() {
+ // Set visible task count to 1 so isPipExitingToDesktopMode returns true
+ when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(1);
+ setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+ assertEquals(WINDOWING_MODE_FREEFORM, mPipDesktopState.getOutPipWindowingMode());
+ }
+
+ @Test
+ public void getOutPipWindowingMode_exitToFullscreen_displayFullscreen_returnsUndefined() {
+ setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+ assertEquals(WINDOWING_MODE_UNDEFINED, mPipDesktopState.getOutPipWindowingMode());
+ }
+
+ private void setDisplayWindowingMode(int windowingMode) {
+ mDefaultTda.configuration.windowConfiguration.setWindowingMode(windowingMode);
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index b5c9fa151dac..2264adec9a19 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -49,6 +49,7 @@ import androidx.test.filters.SmallTest;
import com.android.window.flags.Flags;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
@@ -85,7 +86,7 @@ import org.mockito.MockitoAnnotations;
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
-public class CompatUIControllerTest extends CompatUIShellTestCase {
+public class CompatUIControllerTest extends ShellTestCase {
private static final int DISPLAY_ID = 0;
private static final int TASK_ID = 12;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
index 2117b062bf57..c567b5fbbb70 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
@@ -38,6 +38,7 @@ import androidx.test.filters.SmallTest;
import com.android.window.flags.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
@@ -62,7 +63,7 @@ import java.util.function.Consumer;
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
-public class CompatUILayoutTest extends CompatUIShellTestCase {
+public class CompatUILayoutTest extends ShellTestCase {
private static final int TASK_ID = 1;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java
index 0b37648faeec..8fd7c0ec3099 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java
@@ -27,6 +27,8 @@ import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
+import com.android.wm.shell.ShellTestCase;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -42,7 +44,7 @@ import java.util.function.IntSupplier;
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
-public class CompatUIStatusManagerTest extends CompatUIShellTestCase {
+public class CompatUIStatusManagerTest extends ShellTestCase {
private FakeCompatUIStatusManagerTest mTestState;
private CompatUIStatusManager mStatusManager;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
index 010474e42195..0562bb835671 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -53,6 +53,7 @@ import androidx.test.filters.SmallTest;
import com.android.window.flags.Flags;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState;
@@ -77,7 +78,7 @@ import java.util.function.Consumer;
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
-public class CompatUIWindowManagerTest extends CompatUIShellTestCase {
+public class CompatUIWindowManagerTest extends ShellTestCase {
private static final int TASK_ID = 1;
private static final int TASK_WIDTH = 2000;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java
index e786fef1855c..c6884ea17302 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java
@@ -32,6 +32,7 @@ import android.view.View;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTestCase;
import org.junit.Before;
import org.junit.Test;
@@ -47,7 +48,7 @@ import org.mockito.MockitoAnnotations;
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
-public class LetterboxEduDialogLayoutTest extends CompatUIShellTestCase {
+public class LetterboxEduDialogLayoutTest extends ShellTestCase {
@Mock
private Runnable mDismissCallback;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
index 09fc082a63e3..cbf5d1bb65dd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java
@@ -62,6 +62,7 @@ import androidx.test.filters.SmallTest;
import com.android.window.flags.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.DockStateReader;
@@ -90,7 +91,7 @@ import java.util.function.Consumer;
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
-public class LetterboxEduWindowManagerTest extends CompatUIShellTestCase {
+public class LetterboxEduWindowManagerTest extends ShellTestCase {
private static final int USER_ID_1 = 1;
private static final int USER_ID_2 = 2;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java
index 02c099b3cfb2..31ea8f76359f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java
@@ -34,6 +34,7 @@ import android.view.View;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTestCase;
import org.junit.Before;
import org.junit.Test;
@@ -50,7 +51,7 @@ import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class ReachabilityEduLayoutTest extends CompatUIShellTestCase {
+public class ReachabilityEduLayoutTest extends ShellTestCase {
private ReachabilityEduLayout mLayout;
private View mMoveUpButton;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
index fa04e070250e..1b2c0944777e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java
@@ -30,6 +30,7 @@ import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
@@ -52,7 +53,7 @@ import java.util.function.BiConsumer;
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
-public class ReachabilityEduWindowManagerTest extends CompatUIShellTestCase {
+public class ReachabilityEduWindowManagerTest extends ShellTestCase {
@Mock
private SyncTransactionQueue mSyncTransactionQueue;
@Mock
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java
index 2cded9d9776c..5075453d8c73 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java
@@ -34,6 +34,7 @@ import android.widget.CheckBox;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTestCase;
import org.junit.Before;
import org.junit.Test;
@@ -51,7 +52,7 @@ import java.util.function.Consumer;
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
-public class RestartDialogLayoutTest extends CompatUIShellTestCase {
+public class RestartDialogLayoutTest extends ShellTestCase {
@Mock private Runnable mDismissCallback;
@Mock private Consumer<Boolean> mRestartCallback;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java
index ebd0f412a0a1..779a5ca10648 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java
@@ -28,6 +28,7 @@ import android.util.Pair;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.transition.Transitions;
@@ -50,7 +51,7 @@ import java.util.function.Consumer;
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
-public class RestartDialogWindowManagerTest extends CompatUIShellTestCase {
+public class RestartDialogWindowManagerTest extends ShellTestCase {
@Mock
private SyncTransactionQueue mSyncTransactionQueue;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
index c6532e13f3cc..2b4d5f125783 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
@@ -38,6 +38,7 @@ import androidx.test.filters.SmallTest;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
@@ -62,7 +63,7 @@ import java.util.function.BiConsumer;
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
-public class UserAspectRatioSettingsLayoutTest extends CompatUIShellTestCase {
+public class UserAspectRatioSettingsLayoutTest extends ShellTestCase {
private static final int TASK_ID = 1;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
index 096e900199ba..af7c1f5d7692 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
@@ -54,6 +54,7 @@ import android.view.View;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
@@ -83,7 +84,7 @@ import java.util.function.Supplier;
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
-public class UserAspectRatioSettingsWindowManagerTest extends CompatUIShellTestCase {
+public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase {
private static final int TASK_ID = 1;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
index 09ffd946ea19..d6b13610c9c1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt
@@ -113,7 +113,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() {
.strictness(Strictness.LENIENT)
.spyStatic(DesktopModeStatus::class.java)
.startMocking()
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
shellInit = spy(ShellInit(testExecutor))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
index 470c110fd49b..403d468a7034 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt
@@ -112,7 +112,7 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() {
.strictness(Strictness.LENIENT)
.spyStatic(DesktopModeStatus::class.java)
.startMocking()
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
shellInit = spy(ShellInit(testExecutor))
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
index e46d2c7147ed..13b44977e9c7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
@@ -19,22 +19,30 @@ package com.android.wm.shell.desktopmode
import android.app.ActivityManager.RunningTaskInfo
import android.graphics.PointF
import android.graphics.Rect
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
import android.view.SurfaceControl
import androidx.test.filters.SmallTest
import com.android.internal.policy.SystemBarUtils
+import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
import com.android.wm.shell.R
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.google.common.truth.Truth.assertThat
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
+import org.mockito.kotlin.any
import org.mockito.kotlin.whenever
/**
@@ -43,9 +51,19 @@ import org.mockito.kotlin.whenever
* Usage: atest WMShellUnitTests:DesktopModeVisualIndicatorTest
*/
@SmallTest
+@RunWithLooper
@RunWith(AndroidTestingRunner::class)
+@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
class DesktopModeVisualIndicatorTest : ShellTestCase() {
- @Mock private lateinit var taskInfo: RunningTaskInfo
+
+ @JvmField @Rule val setFlagsRule = SetFlagsRule()
+
+ @JvmField
+ @Rule
+ val extendedMockitoRule =
+ ExtendedMockitoRule.Builder(this).mockStatic(DesktopModeStatus::class.java).build()!!
+
+ private lateinit var taskInfo: RunningTaskInfo
@Mock private lateinit var syncQueue: SyncTransactionQueue
@Mock private lateinit var displayController: DisplayController
@Mock private lateinit var taskSurface: SurfaceControl
@@ -56,10 +74,13 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
@Before
fun setUp() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true)
whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width())
whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height())
whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS)
whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
+ whenever(displayController.getDisplay(anyInt())).thenReturn(mContext.display)
+ taskInfo = DesktopTestHelpers.createFullscreenTask()
}
@Test
@@ -190,6 +211,39 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
}
+ @Test
+ @EnableFlags(
+ com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN,
+ com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE,
+ )
+ fun testDefaultIndicatorWithNoDesktop() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false)
+
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
+ var result = visualIndicator.updateIndicatorType(PointF(500f, 500f))
+ assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
+
+ result = visualIndicator.updateIndicatorType(PointF(10000f, 500f))
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR)
+
+ result = visualIndicator.updateIndicatorType(PointF(-10000f, 500f))
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR)
+
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT)
+ result = visualIndicator.updateIndicatorType(PointF(500f, 500f))
+ assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
+
+ result = visualIndicator.updateIndicatorType(PointF(500f, 0f))
+ assertThat(result)
+ .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR)
+
+ createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT)
+ result = visualIndicator.updateIndicatorType(PointF(500f, 500f))
+ assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR)
+ }
+
private fun createVisualIndicator(dragStartState: DesktopModeVisualIndicator.DragStartState) {
visualIndicator =
DesktopModeVisualIndicator(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
index 6a343c56d364..8510441c0557 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt
@@ -47,6 +47,7 @@ import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
+import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -163,6 +164,69 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun addTask_deskDoesNotExist_throws() {
+ repo.removeDesk(deskId = 0)
+
+ assertThrows(Exception::class.java) {
+ repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 5, isVisible = true)
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun addTaskToDesk_deskDoesNotExist_throws() {
+ repo.removeDesk(deskId = 2)
+
+ assertThrows(Exception::class.java) {
+ repo.addTaskToDesk(
+ displayId = DEFAULT_DISPLAY,
+ deskId = 2,
+ taskId = 4,
+ isVisible = true,
+ )
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun addTaskToDesk_addsToZOrderList() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 2)
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 3)
+ repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 5, isVisible = true)
+ repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 6, isVisible = true)
+ repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 7, isVisible = true)
+ repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 3, taskId = 8, isVisible = true)
+
+ val orderedTasks = repo.getFreeformTasksIdsInDeskInZOrder(deskId = 2)
+ assertThat(orderedTasks[0]).isEqualTo(7)
+ assertThat(orderedTasks[1]).isEqualTo(6)
+ assertThat(orderedTasks[2]).isEqualTo(5)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun addTaskToDesk_visible_addsToVisible() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 2)
+
+ repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 5, isVisible = true)
+
+ assertThat(repo.isVisibleTask(5)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun addTaskToDesk_removesFromAllOtherDesks() {
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 2)
+ repo.addDesk(DEFAULT_DISPLAY, deskId = 3)
+ repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 7, isVisible = true)
+
+ repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 3, taskId = 7, isVisible = true)
+
+ assertThat(repo.getActiveTaskIdsInDesk(2)).doesNotContain(7)
+ }
+
+ @Test
fun removeActiveTask_notifiesActiveTaskListener() {
val listener = TestListener()
repo.addActiveTaskListener(listener)
@@ -467,8 +531,8 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() {
val listener = TestVisibilityListener()
val executor = TestShellExecutor()
repo.addVisibleTasksListener(listener, executor)
- repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
- repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true)
+ repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true)
+ repo.addTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true)
executor.flushAll()
assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2)
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 8e7545c2a99c..aa7944cc837f 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
@@ -66,6 +66,7 @@ import android.widget.Toast
import android.window.DisplayAreaInfo
import android.window.IWindowContainerToken
import android.window.RemoteTransition
+import android.window.TransitionInfo
import android.window.TransitionRequestInfo
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
@@ -290,7 +291,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
.spyStatic(DesktopModeStatus::class.java)
.spyStatic(Toast::class.java)
.startMocking()
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
shellInit = spy(ShellInit(testExecutor))
@@ -519,7 +520,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -540,8 +544,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
markTaskHidden(task1)
@@ -559,6 +563,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun showDesktopApps_deskInactive_bringsToFront_multipleDesksEnabled() {
+ whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
+ .thenReturn(Binder())
+ val deskId = 0
+ // Make desk inactive by activating another desk.
+ taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 1)
+ taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = 1)
+
+ controller.activateDesk(deskId, RemoteTransition(TestRemoteTransition()))
+
+ val wct =
+ getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ // Wallpaper is moved to front.
+ wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+ // Desk is activated.
+ verify(desksOrganizer).activateDesk(wct, deskId)
+ }
+
+ @Test
fun isDesktopModeShowing_noTasks_returnsFalse() {
assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse()
}
@@ -632,58 +659,83 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
)
- @DisableFlags(
- /** TODO: b/362720497 - re-enable when activation is implemented. */
- Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND
- )
- fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_shouldShowWallpaper() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_bringsTasksToFront() {
taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
- val homeTask = setUpHomeTask(SECOND_DISPLAY)
+ setUpHomeTask(SECOND_DISPLAY)
val task1 = setUpFreeformTask(SECOND_DISPLAY)
val task2 = setUpFreeformTask(SECOND_DISPLAY)
markTaskHidden(task1)
markTaskHidden(task2)
+ assertThat(taskRepository.getExpandedTasksOrdered(SECOND_DISPLAY)).contains(task1.taskId)
+ assertThat(taskRepository.getExpandedTasksOrdered(SECOND_DISPLAY)).contains(task2.taskId)
controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition()))
val wct =
getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(4)
- // Expect order to be from bottom: home, wallpaperIntent, task1, task2
- wct.assertReorderAt(index = 0, homeTask)
- wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent)
- wct.assertReorderAt(index = 2, task1)
- wct.assertReorderAt(index = 3, task2)
+ wct.assertReorder(task1)
+ wct.assertReorder(task2)
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- @DisableFlags(
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
- /** TODO: b/362720497 - re-enable when activation is implemented. */
Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
)
+ fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_multipleDesksEnabled_bringsDeskToFront() {
+ whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
+ .thenReturn(Binder())
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2)
+ setUpHomeTask(SECOND_DISPLAY)
+
+ controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct =
+ getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ verify(desksOrganizer).activateDesk(wct, deskId = 2)
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
+ )
+ fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_shouldShowWallpaper() {
+ whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
+ .thenReturn(Binder())
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
+ setUpHomeTask(SECOND_DISPLAY)
+
+ controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct =
+ getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ wct.assertPendingIntent(desktopWallpaperIntent)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY)
fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() {
+ whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
+ .thenReturn(Binder())
taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val homeTask = setUpHomeTask(SECOND_DISPLAY)
- val task1 = setUpFreeformTask(SECOND_DISPLAY)
- val task2 = setUpFreeformTask(SECOND_DISPLAY)
- markTaskHidden(task1)
- markTaskHidden(task2)
controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition()))
val wct =
getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(3)
- // Expect order to be from bottom: home, task1, task2 (no wallpaper intent)
- wct.assertReorderAt(index = 0, homeTask)
- wct.assertReorderAt(index = 1, task1)
- wct.assertReorderAt(index = 2, task2)
+ wct.assertWithoutPendingIntent(desktopWallpaperIntent)
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -705,7 +757,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@DisableFlags(
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
- /** TODO: b/362720497 - re-enable when activation is implemented. */
Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
)
fun showDesktopApps_onSecondaryDisplay_desktopWallpaperDisabled_shouldNotMoveLauncher() {
@@ -729,8 +780,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
markTaskVisible(task1)
@@ -748,7 +799,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperDisabled() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
@@ -769,8 +823,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersOnlyFreeformTasks() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() {
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
markTaskHidden(task1)
@@ -787,26 +841,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
wct.assertReorderAt(index = 2, task2)
}
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersAll() {
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
- markTaskHidden(task1)
- markTaskVisible(task2)
-
- controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
- val wct =
- getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(3)
- // Expect order to be from bottom: wallpaper intent, task1, task2
- wct.assertReorderAt(index = 0, wallpaperToken)
- wct.assertReorderAt(index = 1, task1)
- wct.assertReorderAt(index = 2, task2)
- }
-
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() {
val homeTask = setUpHomeTask()
@@ -820,9 +859,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_addsDesktopWallpaper() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
-
+ fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() {
+ whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
+ .thenReturn(Binder())
controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
val wct =
@@ -831,17 +870,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_reordersDesktopWallpaper() {
- controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
- val wct =
- getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- wct.assertReorderAt(index = 0, wallpaperToken)
- }
-
- @Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() {
taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
@@ -864,7 +896,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
+ whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
+ .thenReturn(Binder())
taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
@@ -877,17 +910,63 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val wct =
getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(3)
// Move home to front
wct.assertReorderAt(index = 0, homeTaskDefaultDisplay)
// Add desktop wallpaper activity
wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplayTasks_desktopWallpaperEnabled_multiDesksDisabled() {
+ whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
+ .thenReturn(Binder())
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
+ val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
+ val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
+ setUpHomeTask(SECOND_DISPLAY)
+ val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY)
+ markTaskHidden(taskDefaultDisplay)
+ markTaskHidden(taskSecondDisplay)
+
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct =
+ getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
// Move freeform task to front
wct.assertReorderAt(index = 2, taskDefaultDisplay)
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplayTasks_desktopWallpaperEnabled_multiDesksEnabled() {
+ whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
+ .thenReturn(Binder())
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
+ val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
+ val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
+ setUpHomeTask(SECOND_DISPLAY)
+ val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY)
+ markTaskHidden(taskDefaultDisplay)
+ markTaskHidden(taskSecondDisplay)
+
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct =
+ getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ // Move desktop tasks to front
+ verify(desksOrganizer).activateDesk(wct, deskId = DEFAULT_DISPLAY)
+ }
+
+ @Test
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun showDesktopApps_desktopWallpaperDisabled_dontReorderMinimizedTask() {
val homeTask = setUpHomeTask()
val freeformTask = setUpFreeformTask()
@@ -908,8 +987,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ /** TODO: b/362720497 - add multi-desk version when minimization is implemented. */
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val homeTask = setUpHomeTask()
val freeformTask = setUpFreeformTask()
val minimizedTask = setUpFreeformTask()
@@ -1325,11 +1405,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
val task = setUpFullscreenTask()
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
- controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
val wct = getLatestEnterDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FREEFORM)
@@ -1338,11 +1419,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveRunningTaskToDesktop_tdaFreeform_windowingModeSetToUndefined() {
val task = setUpFullscreenTask()
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
- controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
val wct = getLatestEnterDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_UNDEFINED)
@@ -1351,11 +1433,78 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun moveTaskToDesktop_nonExistentTask_doesNothing() {
- controller.moveTaskToDesktop(999, transitionSource = UNKNOWN)
- verifyEnterDesktopWCTNotExecuted()
- verify(desktopModeEnterExitTransitionListener, times(0))
- .onEnterDesktopModeTransitionStarted(anyInt())
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveRunningTaskToDesktop_movesTaskToDefaultDesk() {
+ val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
+
+ val wct = getLatestEnterDesktopWct()
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveRunningTaskToDesktop_activatesDesk() {
+ val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
+
+ val wct = getLatestEnterDesktopWct()
+ verify(desksOrganizer).activateDesk(wct, deskId = 0)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveRunningTaskToDesktop_triggersEnterDesktopListener() {
+ val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
+
+ verify(desktopModeEnterExitTransitionListener)
+ .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveTaskToDesk_nonDefaultDesk_movesTaskToDesk() {
+ val transition = Binder()
+ whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenReturn(transition)
+ taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 3)
+ val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+ task.isVisible = true
+
+ controller.moveTaskToDesk(taskId = task.taskId, deskId = 3, transitionSource = UNKNOWN)
+
+ val wct = getLatestEnterDesktopWct()
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId = 3, task)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveTaskToDesk_nonDefaultDesk_activatesDesk() {
+ val transition = Binder()
+ whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenReturn(transition)
+ taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 3)
+ val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+ task.isVisible = true
+
+ controller.moveTaskToDesk(taskId = task.taskId, deskId = 3, transitionSource = UNKNOWN)
+
+ val wct = getLatestEnterDesktopWct()
+ verify(desksOrganizer).activateDesk(wct, deskId = 3)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveTaskToDesk_nonDefaultDesk_triggersEnterDesktopListener() {
+ taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 3)
+ val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+
+ controller.moveTaskToDesk(taskId = task.taskId, deskId = 3, transitionSource = UNKNOWN)
+
+ verify(desktopModeEnterExitTransitionListener)
+ .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
}
@Test
@@ -1365,7 +1514,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
- controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
with(getLatestEnterDesktopWct()) {
assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
@@ -1375,12 +1524,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun moveTaskToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val task = createTaskInfo(1)
whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
- controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
with(getLatestEnterDesktopWct()) {
// Add desktop wallpaper activity
@@ -1392,7 +1540,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
- fun moveRunningTaskToDesktop_topActivityTranslucentWithoutDisplay_taskIsMovedToDesktop() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveRunningTaskToDesktop_topActivityTranslucentWithoutDisplay_taskIsMovedToDesktop_multiDesksDisabled() {
val task =
setUpFullscreenTask().apply {
isActivityStackTransparent = true
@@ -1400,7 +1549,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
numActivities = 1
}
- controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
val wct = getLatestEnterDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FREEFORM)
@@ -1409,6 +1558,26 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun moveRunningTaskToDesktop_topActivityTranslucentWithoutDisplay_taskIsMovedToDesktop_multiDesksEnabled() {
+ val task =
+ setUpFullscreenTask().apply {
+ isActivityStackTransparent = true
+ isTopActivityNoDisplay = true
+ numActivities = 1
+ }
+
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task = task)
+ verify(desktopModeEnterExitTransitionListener)
+ .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun moveRunningTaskToDesktop_topActivityTranslucentWithDisplay_doesNothing() {
val task =
@@ -1418,7 +1587,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
numActivities = 1
}
- controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
verifyEnterDesktopWCTNotExecuted()
verify(desktopModeEnterExitTransitionListener, times(0))
.onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
@@ -1437,13 +1606,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
isTopActivityNoDisplay = false
}
- controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
verifyEnterDesktopWCTNotExecuted()
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
- fun moveRunningTaskToDesktop_systemUIActivityWithoutDisplay_doesNothing() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveRunningTaskToDesktop_systemUIActivityWithoutDisplay_doesNothing_multiDesksDisabled() {
// Set task as systemUI package
val systemUIPackageName =
context.resources.getString(com.android.internal.R.string.config_systemUi)
@@ -1454,7 +1624,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
isTopActivityNoDisplay = true
}
- controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
val wct = getLatestEnterDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
@@ -1473,7 +1643,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
mContext.setMockPackageManager(packageManager)
whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities)
- controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
verifyEnterDesktopWCTNotExecuted()
}
@@ -1489,7 +1659,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
mContext.setMockPackageManager(packageManager)
whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities)
- controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
val wct = getLatestEnterDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
@@ -1497,6 +1667,28 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun moveRunningTaskToDesktop_systemUIActivityWithoutDisplay_doesNothing_multiDesksEnabled() {
+ // Set task as systemUI package
+ val systemUIPackageName =
+ context.resources.getString(com.android.internal.R.string.config_systemUi)
+ val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "")
+ val task =
+ setUpFullscreenTask().apply {
+ baseActivity = baseComponent
+ isTopActivityNoDisplay = true
+ }
+
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
+
+ val wct = getLatestEnterDesktopWct()
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task = task)
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() {
val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java)
@@ -1506,7 +1698,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val task = createTaskInfo(1)
whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
- controller.moveTaskToDesktop(
+ controller.moveTaskToDefaultDeskAndActivate(
taskId = task.taskId,
transitionSource = UNKNOWN,
remoteTransition = RemoteTransition(spy(TestRemoteTransition())),
@@ -1523,8 +1715,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()))
.thenReturn(Binder())
- controller.moveRunningTaskToDesktop(
- task = setUpFullscreenTask(),
+ controller.moveTaskToDefaultDeskAndActivate(
+ taskId = setUpFullscreenTask().taskId,
transitionSource = UNKNOWN,
remoteTransition = RemoteTransition(spy(TestRemoteTransition())),
)
@@ -1535,14 +1727,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() {
val homeTask = setUpHomeTask()
val freeformTask = setUpFreeformTask()
val fullscreenTask = setUpFullscreenTask()
markTaskHidden(freeformTask)
- controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(
+ fullscreenTask.taskId,
+ transitionSource = UNKNOWN,
+ )
with(getLatestEnterDesktopWct()) {
// Operations should include home task, freeform task
@@ -1557,13 +1755,16 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val freeformTask = setUpFreeformTask()
val fullscreenTask = setUpFullscreenTask()
markTaskHidden(freeformTask)
- controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(
+ fullscreenTask.taskId,
+ transitionSource = UNKNOWN,
+ )
with(getLatestEnterDesktopWct()) {
// Operations should include wallpaper intent, freeform task, fullscreen task
@@ -1579,6 +1780,43 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun moveRunningTaskToDesktop_desktopWallpaperEnabled_multiDesksEnabled() {
+ val freeformTask = setUpFreeformTask()
+ val fullscreenTask = setUpFullscreenTask()
+ markTaskHidden(freeformTask)
+
+ controller.moveTaskToDefaultDeskAndActivate(
+ fullscreenTask.taskId,
+ transitionSource = UNKNOWN,
+ )
+
+ val wct = getLatestEnterDesktopWct()
+ wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, fullscreenTask)
+ verify(desksOrganizer).activateDesk(wct, deskId = 0)
+ verify(desktopModeEnterExitTransitionListener)
+ .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveRunningTaskToDesktop_activatesDesk_desktopWallpaperEnabled_multiDesksDisabled() {
+ val fullscreenTask = setUpFullscreenTask()
+
+ controller.moveTaskToDefaultDeskAndActivate(
+ fullscreenTask.taskId,
+ transitionSource = UNKNOWN,
+ )
+
+ assertThat(taskRepository.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(DEFAULT_DISPLAY)
+ }
+
+ @Test
fun moveRunningTaskToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() {
setUpHomeTask(displayId = DEFAULT_DISPLAY)
val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -1590,7 +1828,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY)
markTaskHidden(freeformTaskSecond)
- controller.moveRunningTaskToDesktop(fullscreenTaskDefault, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(
+ fullscreenTaskDefault.taskId,
+ transitionSource = UNKNOWN,
+ )
with(getLatestEnterDesktopWct()) {
// Check that hierarchy operations do not include tasks from second display
@@ -1604,9 +1845,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun moveRunningTaskToDesktop_splitTaskExitsSplit() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveRunningTaskToDesktop_splitTaskExitsSplit_multiDesksDisabled() {
val task = setUpSplitScreenTask()
- controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
val wct = getLatestEnterDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FREEFORM)
@@ -1621,12 +1863,27 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveRunningTaskToDesktop_splitTaskExitsSplit_multiDesksEnabled() {
+ val task = setUpSplitScreenTask()
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task)
+ verify(desktopModeEnterExitTransitionListener)
+ .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
+ verify(splitScreenController)
+ .prepareExitSplitScreen(
+ any(),
+ anyInt(),
+ eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE),
+ )
+ }
+
+ @Test
fun moveRunningTaskToDesktop_fullscreenTaskDoesNotExitSplit() {
val task = setUpFullscreenTask()
- controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN)
val wct = getLatestEnterDesktopWct()
- assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
verify(desktopModeEnterExitTransitionListener)
.onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION)
verify(splitScreenController, never())
@@ -1638,13 +1895,16 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun moveRunningTaskToDesktop_desktopWallpaperDisabled_bringsTasksOver_dontShowBackTask() {
val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
val newTask = setUpFullscreenTask()
val homeTask = setUpHomeTask()
- controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(newTask.taskId, transitionSource = UNKNOWN)
val wct = getLatestEnterDesktopWct()
verify(desktopModeEnterExitTransitionListener)
@@ -1660,13 +1920,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() }
val newTask = setUpFullscreenTask()
val homeTask = setUpHomeTask()
- controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(newTask.taskId, transitionSource = UNKNOWN)
val wct = getLatestEnterDesktopWct()
verify(desktopModeEnterExitTransitionListener)
@@ -2682,7 +2942,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
@@ -2814,7 +3073,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val freeformTask1 = setUpFreeformTask()
val freeformTask2 = createFreeformTask()
@@ -2849,9 +3107,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val task = createFreeformTask()
-
val result = controller.handleRequest(Binder(), createTransition(task))
assertNotNull(result, "Should handle request")
@@ -2879,7 +3135,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
// Second display task
createFreeformTask(displayId = SECOND_DISPLAY)
@@ -3492,7 +3747,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop_multiDesksDisabled() {
val task1 = setUpFullscreenTask()
val task2 = setUpFullscreenTask()
val task3 = setUpFullscreenTask()
@@ -3509,7 +3765,25 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() {
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop_multiDesksEnabled() {
+ val task1 = setUpFullscreenTask()
+ val task2 = setUpFullscreenTask()
+ val task3 = setUpFullscreenTask()
+
+ task1.isFocused = true
+ task2.isFocused = false
+ task3.isFocused = false
+
+ controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+ val wct = getLatestEnterDesktopWct()
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task1)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop_multiDesksDisabled() {
val task1 = setUpSplitScreenTask()
val task2 = setUpFullscreenTask()
val task3 = setUpFullscreenTask()
@@ -3536,6 +3810,33 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop_multiDesksEnabled() {
+ val task1 = setUpSplitScreenTask()
+ val task2 = setUpFullscreenTask()
+ val task3 = setUpFullscreenTask()
+ val task4 = setUpSplitScreenTask()
+
+ task1.isFocused = true
+ task2.isFocused = false
+ task3.isFocused = false
+ task4.isFocused = true
+
+ task4.parentTaskId = task1.taskId
+
+ controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+ val wct = getLatestEnterDesktopWct()
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task4)
+ verify(splitScreenController)
+ .prepareExitSplitScreen(
+ any(),
+ anyInt(),
+ eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE),
+ )
+ }
+
+ @Test
fun moveFocusedTaskToFullscreen() {
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
@@ -3684,6 +3985,59 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun activateDesk_multipleDesks_addsPendingTransition() {
+ val deskId = 0
+ val transition = Binder()
+ val deskChange = mock(TransitionInfo.Change::class.java)
+ whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull()))
+ .thenReturn(transition)
+ whenever(desksOrganizer.isDeskActiveAtEnd(deskChange, deskId)).thenReturn(true)
+ // Make desk inactive by activating another desk.
+ taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 1)
+ taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = 1)
+
+ controller.activateDesk(deskId, RemoteTransition(TestRemoteTransition()))
+
+ verify(desksTransitionsObserver)
+ .addPendingTransition(
+ argThat {
+ this is DeskTransition.ActivateDesk &&
+ this.token == transition &&
+ this.deskId == 0
+ }
+ )
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun moveTaskToDesk_multipleDesks_addsPendingTransition() {
+ val transition = Binder()
+ whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenReturn(transition)
+ taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 3)
+ val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+ task.isVisible = true
+
+ controller.moveTaskToDesk(taskId = task.taskId, deskId = 3, transitionSource = UNKNOWN)
+
+ verify(desksTransitionsObserver)
+ .addPendingTransition(
+ argThat {
+ this is DeskTransition.ActiveDeskWithTask &&
+ this.token == transition &&
+ this.deskId == 3 &&
+ this.enterTaskId == task.taskId
+ }
+ )
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
val spyController = spy(controller)
@@ -5054,7 +5408,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
.thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit))
whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
- controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(
+ taskId = task.taskId,
+ wct = wct,
+ transitionSource = UNKNOWN,
+ )
verify(mMockDesktopImmersiveController)
.exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any())
@@ -5078,7 +5436,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
.thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit))
whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
- controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
+ controller.moveTaskToDefaultDeskAndActivate(
+ taskId = task.taskId,
+ wct = wct,
+ transitionSource = UNKNOWN,
+ )
verify(mMockDesktopImmersiveController)
.exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any())
@@ -5660,6 +6022,29 @@ private fun WindowContainerTransaction.assertIndexInBounds(index: Int) {
.isGreaterThan(index)
}
+private fun WindowContainerTransaction.assertHop(
+ predicate: (WindowContainerTransaction.HierarchyOp) -> Boolean
+) {
+ assertThat(hierarchyOps.any(predicate)).isTrue()
+}
+
+private fun WindowContainerTransaction.assertWithoutHop(
+ predicate: (WindowContainerTransaction.HierarchyOp) -> Boolean
+) {
+ assertThat(hierarchyOps.none(predicate)).isTrue()
+}
+
+private fun WindowContainerTransaction.assertReorder(
+ task: RunningTaskInfo,
+ toTop: Boolean? = null,
+) {
+ assertHop { hop ->
+ hop.type == HIERARCHY_OP_TYPE_REORDER &&
+ (toTop == null || hop.toTop == toTop) &&
+ hop.container == task.token.asBinder()
+ }
+}
+
private fun WindowContainerTransaction.assertReorderAt(
index: Int,
task: RunningTaskInfo,
@@ -5721,6 +6106,20 @@ private fun WindowContainerTransaction.hasRemoveAt(index: Int, token: WindowCont
assertThat(op.container).isEqualTo(token.asBinder())
}
+private fun WindowContainerTransaction.assertPendingIntent(intent: Intent) {
+ assertHop { hop ->
+ hop.type == HIERARCHY_OP_TYPE_PENDING_INTENT &&
+ hop.pendingIntent?.intent?.component == intent.component
+ }
+}
+
+private fun WindowContainerTransaction.assertWithoutPendingIntent(intent: Intent) {
+ assertWithoutHop { hop ->
+ hop.type == HIERARCHY_OP_TYPE_PENDING_INTENT &&
+ hop.pendingIntent?.intent?.component == intent.component
+ }
+}
+
private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) {
assertIndexInBounds(index)
val op = hierarchyOps[index]
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index 33dc1aadf548..25246d9984c3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -477,6 +477,40 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
}
@Test
+ fun mergeAnimation_endTransition_springHandler_noStartHomeChange_doesntCrash() {
+ whenever(dragAnimator.computeCurrentVelocity()).thenReturn(PointF())
+ val playingFinishTransaction = mock<SurfaceControl.Transaction>()
+ val mergedStartTransaction = mock<SurfaceControl.Transaction>()
+ val mergedFinishTransaction = mock<SurfaceControl.Transaction>()
+ val finishCallback = mock<Transitions.TransitionFinishCallback>()
+ val task = createTask()
+ val startTransition = startDrag(
+ springHandler, task, finishTransaction = playingFinishTransaction, homeChange = null)
+ springHandler.onTaskResizeAnimationListener = mock()
+
+ springHandler.mergeAnimation(
+ transition = mock<IBinder>(),
+ info =
+ createTransitionInfo(
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ draggedTask = task,
+ ),
+ startT = mergedStartTransaction,
+ finishT = mergedFinishTransaction,
+ mergeTarget = startTransition,
+ finishCallback = finishCallback,
+ )
+
+ // Should show dragged task layer in start and finish transaction
+ verify(mergedStartTransaction).show(draggedTaskLeash)
+ verify(playingFinishTransaction).show(draggedTaskLeash)
+ // Should update the dragged task layer
+ verify(mergedStartTransaction).setLayer(eq(draggedTaskLeash), anyInt())
+ // Should merge animation
+ verify(finishCallback).onTransitionFinished(null)
+ }
+
+ @Test
fun propertyValue_returnsSystemPropertyValue() {
val name = "property_name"
val value = 10f
@@ -589,6 +623,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
handler: DragToDesktopTransitionHandler,
task: RunningTaskInfo = createTask(),
finishTransaction: SurfaceControl.Transaction = mock(),
+ homeChange: TransitionInfo.Change? = createHomeChange(),
): IBinder {
whenever(dragAnimator.position).thenReturn(PointF())
// Simulate transition is started and is ready to animate.
@@ -599,6 +634,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
createTransitionInfo(
type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
draggedTask = task,
+ homeChange = homeChange,
),
startTransaction = mock(),
finishTransaction = finishTransaction,
@@ -684,16 +720,12 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
}
}
- private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo) =
+ private fun createTransitionInfo(
+ type: Int,
+ draggedTask: RunningTaskInfo,
+ homeChange: TransitionInfo.Change? = createHomeChange()) =
TransitionInfo(type, /* flags= */ 0).apply {
- addChange( // Home.
- TransitionInfo.Change(mock(), homeTaskLeash).apply {
- parent = null
- taskInfo =
- TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
- flags = flags or FLAG_IS_WALLPAPER
- }
- )
+ homeChange?.let { addChange(it) }
addChange( // Dragged Task.
TransitionInfo.Change(mock(), draggedTaskLeash).apply {
parent = null
@@ -709,6 +741,12 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
)
}
+ private fun createHomeChange() = TransitionInfo.Change(mock(), homeTaskLeash).apply {
+ parent = null
+ taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
+ flags = flags or FLAG_IS_WALLPAPER
+ }
+
private fun systemPropertiesKey(name: String) =
"${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name"
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt
index bfbaa84e9312..9f09e3f57927 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt
@@ -21,20 +21,26 @@ import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
import androidx.test.filters.SmallTest
import com.android.window.flags.Flags
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.desktopmode.DesktopRepository
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
import com.android.wm.shell.desktopmode.DesktopUserRepositories
import com.android.wm.shell.sysui.ShellInit
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
/**
* Tests for [DesksTransitionObserver].
@@ -47,6 +53,9 @@ class DesksTransitionObserverTest : ShellTestCase() {
@JvmField @Rule val setFlagsRule = SetFlagsRule()
+ private val mockDesksOrganizer = mock<DesksOrganizer>()
+ val testScope = TestScope()
+
private lateinit var desktopUserRepositories: DesktopUserRepositories
private lateinit var observer: DesksTransitionObserver
@@ -62,10 +71,10 @@ class DesksTransitionObserverTest : ShellTestCase() {
/* shellController= */ mock(),
/* persistentRepository= */ mock(),
/* repositoryInitializer= */ mock(),
- /* mainCoroutineScope= */ mock(),
+ testScope,
/* userManager= */ mock(),
)
- observer = DesksTransitionObserver(desktopUserRepositories)
+ observer = DesksTransitionObserver(desktopUserRepositories, mockDesksOrganizer)
}
@Test
@@ -121,4 +130,51 @@ class DesksTransitionObserverTest : ShellTestCase() {
assertThat(removeListener.lastDeskRemoved).isEqualTo(5)
}
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onTransitionReady_activateDesk_updatesRepository() {
+ val transition = Binder()
+ val change = Change(mock(), mock())
+ whenever(mockDesksOrganizer.isDeskActiveAtEnd(change, deskId = 5)).thenReturn(true)
+ val activateTransition =
+ DeskTransition.ActivateDesk(transition, displayId = DEFAULT_DISPLAY, deskId = 5)
+ repository.addDesk(DEFAULT_DISPLAY, deskId = 5)
+
+ observer.addPendingTransition(activateTransition)
+ observer.onTransitionReady(
+ transition = transition,
+ info = TransitionInfo(TRANSIT_TO_FRONT, /* flags= */ 0).apply { addChange(change) },
+ )
+
+ assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(5)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun onTransitionReady_activateDeskWithTask_updatesRepository() =
+ testScope.runTest {
+ val deskId = 5
+ val task = createFreeformTask(DEFAULT_DISPLAY).apply { isVisibleRequested = true }
+ val transition = Binder()
+ val change = Change(mock(), mock()).apply { taskInfo = task }
+ whenever(mockDesksOrganizer.getDeskAtEnd(change)).thenReturn(deskId)
+ val activateTransition =
+ DeskTransition.ActiveDeskWithTask(
+ transition,
+ displayId = DEFAULT_DISPLAY,
+ deskId = deskId,
+ enterTaskId = task.taskId,
+ )
+ repository.addDesk(DEFAULT_DISPLAY, deskId = deskId)
+
+ observer.addPendingTransition(activateTransition)
+ observer.onTransitionReady(
+ transition = transition,
+ info = TransitionInfo(TRANSIT_TO_FRONT, /* flags= */ 0).apply { addChange(change) },
+ )
+
+ assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(deskId)
+ assertThat(repository.getActiveTaskIdsInDesk(deskId)).contains(task.taskId)
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
index a07203d86b75..4d4b15389eca 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt
@@ -15,9 +15,11 @@
*/
package com.android.wm.shell.desktopmode.multidesks
+import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.testing.AndroidTestingRunner
import android.view.Display
import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
import android.window.WindowContainerTransaction.HierarchyOp
@@ -216,6 +218,13 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
)
.isTrue()
+ assertThat(
+ wct.changes.any { change ->
+ change.key == desktopTask.token.asBinder() &&
+ change.value.windowingMode == WINDOWING_MODE_UNDEFINED
+ }
+ )
+ .isTrue()
}
@Test
@@ -244,6 +253,26 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
assertThat(endDesk).isEqualTo(freeformRoot.taskId)
}
+ @Test
+ fun testIsDeskActiveAtEnd() {
+ organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
+ val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
+ freeformRoot.isVisibleRequested = true
+ organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+
+ val isActive =
+ organizer.isDeskActiveAtEnd(
+ change =
+ TransitionInfo.Change(freeformRoot.token, SurfaceControl()).apply {
+ taskInfo = freeformRoot
+ mode = TRANSIT_TO_FRONT
+ },
+ deskId = freeformRoot.taskId,
+ )
+
+ assertThat(isActive).isTrue()
+ }
+
private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback {
var deskId: Int? = null
val created: Boolean
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
index 1b1a5a909220..06dcd8812350 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
@@ -47,6 +47,7 @@ import com.android.internal.logging.UiEventLogger;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.bubbles.bar.BubbleBarDragListener;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -60,6 +61,8 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import dagger.Lazy;
+
/**
* Tests for the drag and drop controller.
*/
@@ -91,6 +94,8 @@ public class DragAndDropControllerTest extends ShellTestCase {
private Transitions mTransitions;
@Mock
private GlobalDragListener mGlobalDragListener;
+ @Mock
+ private Lazy<BubbleBarDragListener> mBubbleBarDragControllerLazy;
private DragAndDropController mController;
@@ -99,7 +104,8 @@ public class DragAndDropControllerTest extends ShellTestCase {
MockitoAnnotations.initMocks(this);
mController = new DragAndDropController(mContext, mShellInit, mShellController,
mShellCommandHandler, mShellTaskOrganizer, mDisplayController, mUiEventLogger,
- mIconProvider, mGlobalDragListener, mTransitions, mMainExecutor);
+ mIconProvider, mGlobalDragListener, mTransitions, mBubbleBarDragControllerLazy,
+ mMainExecutor);
mController.onInit();
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
index bd857c7dcd45..8e0381e4f933 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java
@@ -26,7 +26,6 @@ import static org.mockito.kotlin.MatchersKt.eq;
import static org.mockito.kotlin.VerificationKt.times;
import static org.mockito.kotlin.VerificationKt.verify;
-import android.app.TaskInfo;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Matrix;
@@ -39,12 +38,9 @@ import android.window.WindowContainerTransaction;
import androidx.test.filters.SmallTest;
-import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.pip.PipBoundsState;
-import com.android.wm.shell.desktopmode.DesktopRepository;
-import com.android.wm.shell.desktopmode.DesktopUserRepositories;
-import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider;
+import com.android.wm.shell.common.pip.PipDesktopState;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
@@ -55,11 +51,8 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
-import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
-import java.util.Optional;
-
/**
* Unit test against {@link PipScheduler}
*/
@@ -77,16 +70,13 @@ public class PipSchedulerTest {
@Mock private PipBoundsState mMockPipBoundsState;
@Mock private ShellExecutor mMockMainExecutor;
@Mock private PipTransitionState mMockPipTransitionState;
+ @Mock private PipDesktopState mMockPipDesktopState;
@Mock private PipTransitionController mMockPipTransitionController;
@Mock private Runnable mMockUpdateMovementBoundsRunnable;
@Mock private WindowContainerToken mMockPipTaskToken;
@Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory;
@Mock private SurfaceControl.Transaction mMockTransaction;
@Mock private PipAlphaAnimator mMockAlphaAnimator;
- @Mock private DesktopUserRepositories mMockDesktopUserRepositories;
- @Mock private DesktopWallpaperActivityTokenProvider mMockDesktopWallpaperActivityTokenProvider;
- @Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
-
@Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
@Captor private ArgumentCaptor<WindowContainerTransaction> mWctArgumentCaptor;
@@ -101,14 +91,9 @@ public class PipSchedulerTest {
when(mMockFactory.getTransaction()).thenReturn(mMockTransaction);
when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any()))
.thenReturn(mMockTransaction);
- when(mMockDesktopUserRepositories.getCurrent())
- .thenReturn(Mockito.mock(DesktopRepository.class));
- when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(Mockito.mock(TaskInfo.class));
mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor,
- mMockPipTransitionState, Optional.of(mMockDesktopUserRepositories),
- Optional.of(mMockDesktopWallpaperActivityTokenProvider),
- mRootTaskDisplayAreaOrganizer);
+ mMockPipTransitionState, mMockPipDesktopState);
mPipScheduler.setPipTransitionController(mMockPipTransitionController);
mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory);
mPipScheduler.setPipAlphaAnimatorSupplier((context, leash, startTx, finishTx, direction) ->
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java
index 3923a1ef5633..1b462c30e017 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java
@@ -165,6 +165,25 @@ public class PipTaskListenerTest {
}
@Test
+ public void onTaskInfoChanged_withNullPipParams_doNothing() {
+ mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer,
+ mMockPipTransitionState, mMockPipScheduler, mMockPipBoundsState,
+ mMockPipBoundsAlgorithm, mMockShellExecutor);
+ mPipTaskListener.addParamsChangedListener(mMockPipParamsChangedCallback);
+ Rational aspectRatio = new Rational(4, 3);
+ when(mMockPipBoundsState.getAspectRatio()).thenReturn(aspectRatio.toFloat());
+ String action1 = "action1";
+ mPipTaskListener.onTaskInfoChanged(getTaskInfo(aspectRatio, action1));
+
+ clearInvocations(mMockPipParamsChangedCallback);
+ mPipTaskListener.onTaskInfoChanged(new ActivityManager.RunningTaskInfo());
+
+ verifyZeroInteractions(mMockPipParamsChangedCallback);
+ verify(mMockPipTransitionState, times(0))
+ .setOnIdlePipTransitionStateRunnable(any(Runnable.class));
+ }
+
+ @Test
public void onTaskInfoChanged_withActionsChanged_callbackActionsChanged() {
mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer,
mMockPipTransitionState, mMockPipScheduler, mMockPipBoundsState,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt
index a8a7be8fe7e3..d9791bb43489 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt
@@ -21,7 +21,7 @@ import android.content.pm.PackageManager
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.internal.R
-import com.android.wm.shell.compatui.CompatUIShellTestCase
+import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -39,7 +39,7 @@ import org.mockito.kotlin.whenever
*/
@RunWith(AndroidTestingRunner::class)
@SmallTest
-class DesktopModeCompatPolicyTest : CompatUIShellTestCase() {
+class DesktopModeCompatPolicyTest : ShellTestCase() {
private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy
@Before
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
index 4dac99b14aaf..33f14acd0f02 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt
@@ -39,6 +39,9 @@ import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
+/**
+ * Test class for [DesktopModeStatus].
+ */
@SmallTest
@Presubmit
@EnableFlags(Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION)
@@ -56,6 +59,7 @@ class DesktopModeStatusTest : ShellTestCase() {
doReturn(false).whenever(mockResources).getBoolean(
eq(R.bool.config_isDesktopModeDevOptionSupported)
)
+ setDeviceEligibleForDesktopMode(false)
doReturn(context.contentResolver).whenever(mockContext).contentResolver
resetDesktopModeFlagsCache()
resetEnforceDeviceRestriction()
@@ -74,7 +78,7 @@ class DesktopModeStatusTest : ShellTestCase() {
Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION
)
@Test
- fun canEnterDesktopMode_DWFlagDisabled_configsOff_returnsFalse() {
+ fun canEnterDesktopMode_DWFlagDisabled_deviceNotEligible_returnsFalse() {
assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse()
}
@@ -83,8 +87,8 @@ class DesktopModeStatusTest : ShellTestCase() {
Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION
)
@Test
- fun canEnterDesktopMode_DWFlagDisabled_configsOn_disableDeviceRestrictions_returnsFalse() {
- doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported))
+ fun canEnterDesktopMode_DWFlagDisabled_deviceEligible_configDevOptionOn_returnsFalse() {
+ setDeviceEligibleForDesktopMode(true)
doReturn(true).whenever(mockResources).getBoolean(
eq(R.bool.config_isDesktopModeDevOptionSupported)
)
@@ -98,7 +102,7 @@ class DesktopModeStatusTest : ShellTestCase() {
Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION
)
@Test
- fun canEnterDesktopMode_DWFlagDisabled_configDevOptionOn_returnsFalse() {
+ fun canEnterDesktopMode_DWFlagDisabled_deviceNotEligible_configDevOptionOn_returnsFalse() {
doReturn(true).whenever(mockResources).getBoolean(
eq(R.bool.config_isDesktopModeDevOptionSupported)
)
@@ -111,7 +115,7 @@ class DesktopModeStatusTest : ShellTestCase() {
Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION
)
@Test
- fun canEnterDesktopMode_DWFlagDisabled_configDevOptionOn_flagOverrideOn_returnsTrue() {
+ fun canEnterDesktopMode_DWFlagDisabled_deviceNotEligible_forceUsingDevOption_returnsTrue() {
doReturn(true).whenever(mockResources).getBoolean(
eq(R.bool.config_isDesktopModeDevOptionSupported)
)
@@ -123,14 +127,7 @@ class DesktopModeStatusTest : ShellTestCase() {
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@Test
- fun canEnterDesktopMode_DWFlagEnabled_configsOff_returnsFalse() {
- assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse()
- }
-
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- @Test
- fun canEnterDesktopMode_DWFlagEnabled_configDesktopModeOff_returnsFalse() {
+ fun canEnterDesktopMode_DWFlagEnabled_deviceNotEligible_returnsFalse() {
doReturn(true).whenever(mockResources).getBoolean(
eq(R.bool.config_isDesktopModeDevOptionSupported)
)
@@ -141,17 +138,8 @@ class DesktopModeStatusTest : ShellTestCase() {
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@Test
- fun canEnterDesktopMode_DWFlagEnabled_configDesktopModeOn_returnsTrue() {
- doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported))
-
- assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue()
- }
-
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- @Test
- fun canEnterDesktopMode_DWFlagEnabled_configsOff_disableDeviceRestrictions_returnsTrue() {
- disableEnforceDeviceRestriction()
+ fun canEnterDesktopMode_DWFlagEnabled_deviceEligible_returnsTrue() {
+ setDeviceEligibleForDesktopMode(true)
assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue()
}
@@ -159,7 +147,7 @@ class DesktopModeStatusTest : ShellTestCase() {
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@Test
- fun canEnterDesktopMode_DWFlagEnabled_configDevOptionOn_flagOverrideOn_returnsTrue() {
+ fun canEnterDesktopMode_DWFlagEnabled_deviceNotEligible_forceUsingDevOption_returnsTrue() {
doReturn(true).whenever(mockResources).getBoolean(
eq(R.bool.config_isDesktopModeDevOptionSupported)
)
@@ -198,6 +186,28 @@ class DesktopModeStatusTest : ShellTestCase() {
assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue()
}
+ @DisableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION)
+ @Test
+ fun canShowDesktopExperienceDevOption_flagDisabled_returnsFalse() {
+ setDeviceEligibleForDesktopMode(true)
+
+ assertThat(DesktopModeStatus.canShowDesktopExperienceDevOption(mockContext)).isFalse()
+ }
+
+ @EnableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION)
+ @Test
+ fun canShowDesktopExperienceDevOption_flagEnabled_deviceNotEligible_returnsFalse() {
+ assertThat(DesktopModeStatus.canShowDesktopExperienceDevOption(mockContext)).isFalse()
+ }
+
+ @EnableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION)
+ @Test
+ fun canShowDesktopExperienceDevOption_flagEnabled_deviceEligible_returnsTrue() {
+ setDeviceEligibleForDesktopMode(true)
+
+ assertThat(DesktopModeStatus.canShowDesktopExperienceDevOption(mockContext)).isTrue()
+ }
+
private fun resetEnforceDeviceRestriction() {
setEnforceDeviceRestriction(true)
}
@@ -232,4 +242,10 @@ class DesktopModeStatusTest : ShellTestCase() {
DEVELOPMENT_OVERRIDE_DESKTOP_MODE_FEATURES, override.setting
)
}
+
+ private fun setDeviceEligibleForDesktopMode(eligible: Boolean) {
+ val deviceRestrictions = DesktopModeStatus::class.java.getDeclaredField("ENFORCE_DEVICE_RESTRICTIONS")
+ deviceRestrictions.isAccessible = true
+ deviceRestrictions.setBoolean(/* obj= */ null, /* z= */ !eligible)
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt
index 8b4cf6d1fabe..e40d97c68554 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt
@@ -67,7 +67,7 @@ class DesktopModeWindowDecorViewModelAppHandleOnlyTest :
.spyStatic(DesktopModeStatus::class.java)
.spyStatic(DragPositioningCallbackUtility::class.java)
.startMocking()
- doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ doReturn(false).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
doReturn(true).`when` { DesktopModeStatus.overridesShowAppHandle(any())}
setUpCommon()
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 737780ed8024..f15418adf1e3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -116,7 +116,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
.spyStatic(DragPositioningCallbackUtility::class.java)
.startMocking()
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(Mockito.any()) }
+ doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(Mockito.any()) }
doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(Mockito.any()) }
setUpCommon()
@@ -379,37 +379,21 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- fun testWindowDecor_desktopModeUnsupportedOnDevice_deviceRestrictionsOverridden_decorCreated() {
- // Simulate enforce device restrictions system property overridden to false
- whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false)
- // Simulate device that doesn't support desktop mode
- doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
-
- val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
- setUpMockDecorationsForTasks(task)
-
- onTaskOpening(task)
- assertTrue(windowDecorByTaskIdSpy.contains(task.taskId))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
- fun testWindowDecor_deviceSupportsDesktopMode_decorCreated() {
+ fun testWindowDecor_deviceEligibleForDesktopMode_decorCreated() {
// Simulate default enforce device restrictions system property
whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ doReturn(true).`when` { DesktopModeStatus.isDeviceEligibleForDesktopMode(any()) }
setUpMockDecorationsForTasks(task)
onTaskOpening(task)
- assertTrue(windowDecorByTaskIdSpy.contains(task.taskId))
+ assertTrue(task.taskId in windowDecorByTaskIdSpy)
}
@Test
fun testOnDecorMaximizedOrRestored_togglesTaskSize_maximize() {
- val maxOrRestoreListenerCaptor = forClass(Function0::class.java)
- as ArgumentCaptor<Function0<Unit>>
+ val maxOrRestoreListenerCaptor = forClass(Function0::class.java as Class<Function0<Unit>>)
val decor = createOpenTaskDecoration(
windowingMode = WINDOWING_MODE_FREEFORM,
onMaxOrRestoreListenerCaptor = maxOrRestoreListenerCaptor
@@ -655,7 +639,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
toDesktopListenerCaptor.value.accept(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON)
- verify(mockDesktopTasksController).moveTaskToDesktop(
+ verify(mockDesktopTasksController).moveTaskToDefaultDeskAndActivate(
eq(decor.mTaskInfo.taskId),
any(),
eq(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON),
@@ -893,7 +877,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
)
verify(mockDesktopTasksController, times(1))
- .moveTaskToDesktop(any(), any(), any(), anyOrNull(), anyOrNull())
+ .moveTaskToDefaultDeskAndActivate(any(), any(), any(), anyOrNull(), anyOrNull())
}
@Test
diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp
index 12b1dd794a03..3dc53c5051e9 100644
--- a/libs/androidfw/Android.bp
+++ b/libs/androidfw/Android.bp
@@ -288,6 +288,7 @@ cc_benchmark {
"tests/AttributeResolution_bench.cpp",
"tests/CursorWindow_bench.cpp",
"tests/Generic_bench.cpp",
+ "tests/LocaleDataLookup_bench.cpp",
"tests/SparseEntry_bench.cpp",
"tests/Theme_bench.cpp",
],
diff --git a/libs/androidfw/LocaleDataLookup.cpp b/libs/androidfw/LocaleDataLookup.cpp
index 6e751a77f355..ea9e9a2d4280 100644
--- a/libs/androidfw/LocaleDataLookup.cpp
+++ b/libs/androidfw/LocaleDataLookup.cpp
@@ -7518,6 +7518,13 @@ const char* lookupLikelyScript(uint32_t packed_lang_region) {
}
}
+/*
+ * TODO: Consider turning the below switch statement into binary search
+ * to save the disk space when the table is larger in the future.
+ * Disassembled code shows that the jump table emitted by clang can be
+ * 4x larger than the data in disk size, but it depends on the optimization option.
+ * However, a switch statement will benefit from the future of compiler improvement.
+ */
bool isLocaleRepresentative(uint32_t language_and_region, const char* script) {
const uint64_t packed_locale =
((static_cast<uint64_t>(language_and_region)) << 32u) |
diff --git a/libs/androidfw/TypeWrappers.cpp b/libs/androidfw/TypeWrappers.cpp
index 970463492c1a..2a20106b6ee1 100644
--- a/libs/androidfw/TypeWrappers.cpp
+++ b/libs/androidfw/TypeWrappers.cpp
@@ -18,8 +18,11 @@
namespace android {
-TypeVariant::TypeVariant(const ResTable_type* data) : data(data), mLength(dtohl(data->entryCount)) {
- if (data->flags & ResTable_type::FLAG_SPARSE) {
+TypeVariant::TypeVariant(const ResTable_type* data)
+ : data(data)
+ , mLength(dtohl(data->entryCount))
+ , mSparse(data->flags & ResTable_type::FLAG_SPARSE) {
+ if (mSparse) {
const uint32_t entryCount = dtohl(data->entryCount);
const uintptr_t containerEnd = reinterpret_cast<uintptr_t>(data) + dtohl(data->header.size);
const uint32_t* const entryIndices = reinterpret_cast<const uint32_t*>(
@@ -40,18 +43,18 @@ TypeVariant::iterator& TypeVariant::iterator::operator++() {
mIndex = mTypeVariant->mLength;
}
- const ResTable_type* type = mTypeVariant->data;
- if ((type->flags & ResTable_type::FLAG_SPARSE) == 0) {
+ if (!mTypeVariant->mSparse) {
return *this;
}
// Need to adjust |mSparseIndex| as well if we've passed its current element.
+ const ResTable_type* type = mTypeVariant->data;
const uint32_t entryCount = dtohl(type->entryCount);
- const auto entryIndices = reinterpret_cast<const uint32_t*>(
- reinterpret_cast<uintptr_t>(type) + dtohs(type->header.headerSize));
if (mSparseIndex >= entryCount) {
return *this; // done
}
+ const auto entryIndices = reinterpret_cast<const uint32_t*>(
+ reinterpret_cast<uintptr_t>(type) + dtohs(type->header.headerSize));
const auto element = (const ResTable_sparseTypeEntry*)(entryIndices + mSparseIndex);
if (mIndex > dtohs(element->idx)) {
++mSparseIndex;
@@ -79,7 +82,7 @@ const ResTable_entry* TypeVariant::iterator::operator*() const {
}
uint32_t entryOffset;
- if (type->flags & ResTable_type::FLAG_SPARSE) {
+ if (mTypeVariant->mSparse) {
if (mSparseIndex >= entryCount) {
return nullptr;
}
diff --git a/libs/androidfw/include/androidfw/TypeWrappers.h b/libs/androidfw/include/androidfw/TypeWrappers.h
index db641b78a4e4..d901af3c908b 100644
--- a/libs/androidfw/include/androidfw/TypeWrappers.h
+++ b/libs/androidfw/include/androidfw/TypeWrappers.h
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-#ifndef __TYPE_WRAPPERS_H
-#define __TYPE_WRAPPERS_H
+#pragma once
#include <androidfw/ResourceTypes.h>
#include <utils/ByteOrder.h>
@@ -54,7 +53,7 @@ struct TypeVariant {
enum class Kind { Begin, End };
iterator(const TypeVariant* tv, Kind kind)
: mTypeVariant(tv) {
- mSparseIndex = mIndex = kind == Kind::Begin ? 0 : tv->mLength;
+ mSparseIndex = mIndex = (kind == Kind::Begin ? 0 : tv->mLength);
// mSparseIndex here is technically past the number of sparse entries, but it is still
// ok as it is enough to infer that this is the end iterator.
}
@@ -75,9 +74,11 @@ struct TypeVariant {
const ResTable_type* data;
private:
- size_t mLength;
+ // For a dense table, this is the number of the elements.
+ // For a sparse table, this is the index of the last element + 1.
+ // In both cases, it can be used for iteration as the upper loop bound as in |i < mLength|.
+ uint32_t mLength;
+ bool mSparse;
};
} // namespace android
-
-#endif // __TYPE_WRAPPERS_H
diff --git a/libs/androidfw/tests/LocaleDataLookup_bench.cpp b/libs/androidfw/tests/LocaleDataLookup_bench.cpp
new file mode 100644
index 000000000000..60ce3b944551
--- /dev/null
+++ b/libs/androidfw/tests/LocaleDataLookup_bench.cpp
@@ -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.
+ */
+
+#include "benchmark/benchmark.h"
+
+#include "androidfw/LocaleDataLookup.h"
+
+namespace android {
+
+static void BM_LocaleDataLookupIsLocaleRepresentative(benchmark::State& state) {
+ for (auto&& _ : state) {
+ isLocaleRepresentative(packLocale("en", "US"), "Latn");
+ isLocaleRepresentative(packLocale("es", "ES"), "Latn");
+ isLocaleRepresentative(packLocale("zh", "CN"), "Hans");
+ isLocaleRepresentative(packLocale("pt", "BR"), "Latn");
+ isLocaleRepresentative(packLocale("ar", "EG"), "Arab");
+ isLocaleRepresentative(packLocale("hi", "IN"), "Deva");
+ isLocaleRepresentative(packLocale("jp", "JP"), "Jpan");
+ }
+}
+BENCHMARK(BM_LocaleDataLookupIsLocaleRepresentative);
+
+static void BM_LocaleDataLookupLikelyScript(benchmark::State& state) {
+ for (auto&& _ : state) {
+ lookupLikelyScript(packLocale("en", ""));
+ lookupLikelyScript(packLocale("es", ""));
+ lookupLikelyScript(packLocale("zh", ""));
+ lookupLikelyScript(packLocale("pt", ""));
+ lookupLikelyScript(packLocale("ar", ""));
+ lookupLikelyScript(packLocale("hi", ""));
+ lookupLikelyScript(packLocale("jp", ""));
+ lookupLikelyScript(packLocale("en", "US"));
+ lookupLikelyScript(packLocale("es", "ES"));
+ lookupLikelyScript(packLocale("zh", "CN"));
+ lookupLikelyScript(packLocale("pt", "BR"));
+ lookupLikelyScript(packLocale("ar", "EG"));
+ lookupLikelyScript(packLocale("hi", "IN"));
+ lookupLikelyScript(packLocale("jp", "JP"));
+ }
+}
+BENCHMARK(BM_LocaleDataLookupLikelyScript);
+
+
+} // namespace android
diff --git a/libs/androidfw/tests/TypeWrappers_test.cpp b/libs/androidfw/tests/TypeWrappers_test.cpp
index d66e05805484..69c24c5d8956 100644
--- a/libs/androidfw/tests/TypeWrappers_test.cpp
+++ b/libs/androidfw/tests/TypeWrappers_test.cpp
@@ -121,6 +121,7 @@ TEST(TypeVariantIteratorTest, shouldIterateOverTypeWithoutErrors) {
values.push_back(std::nullopt);
values.push_back(std::nullopt);
values.push_back(Res_value{ sizeof(Res_value), 0, Res_value::TYPE_STRING, 0x87654321});
+ values.push_back(std::nullopt);
// test for combinations of compact_entry and short_offsets
for (size_t i = 0; i < 8; i++) {
@@ -191,6 +192,17 @@ TEST(TypeVariantIteratorTest, shouldIterateOverTypeWithoutErrors) {
++iter;
+ ASSERT_EQ(uint32_t(9), iter.index());
+ ASSERT_TRUE(NULL == *iter);
+ if (sparse) {
+ // Sparse iterator doesn't know anything beyond the last entry.
+ ASSERT_EQ(v.endEntries(), iter);
+ } else {
+ ASSERT_NE(v.endEntries(), iter);
+ }
+
+ ++iter;
+
ASSERT_EQ(v.endEntries(), iter);
}
}
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 71013f7f4e34..5dc49a07a6d6 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -10406,6 +10406,23 @@ public class AudioManager {
}
}
+ /**
+ * Enable strict audio hardening (background) enforcement, regardless of release or temporary
+ * exemptions for debugging purposes.
+ * Enforced hardening can be found in the audio dumpsys with the API being restricted and the
+ * level of restriction which was encountered.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+ public void setEnableHardening(boolean shouldEnable) {
+ final IAudioService service = getService();
+ try {
+ service.setEnableHardening(shouldEnable);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
//====================================================================
// Mute await connection
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 2a740f85aa72..7b8d6663c957 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -819,4 +819,8 @@ interface IAudioService {
@EnforcePermission("QUERY_AUDIO_STATE")
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.QUERY_AUDIO_STATE)")
boolean shouldNotificationSoundPlay(in AudioAttributes aa);
+
+ @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED")
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)")
+ void setEnableHardening(in boolean shouldEnable);
}
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index 1a8437108b72..fb1b5b57cce6 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -3888,7 +3888,9 @@ final public class MediaCodec {
/**
* Set a hardware graphic buffer to this queue request. Exactly one buffer must
- * be set for a queue request before calling {@link #queue}.
+ * be set for a queue request before calling {@link #queue}. Ownership of the
+ * hardware buffer is not transferred to this queue request, nor will it be transferred
+ * to the codec once {@link #queue} is called.
* <p>
* Note: buffers should have format {@link HardwareBuffer#YCBCR_420_888},
* a single layer, and an appropriate usage ({@link HardwareBuffer#USAGE_CPU_READ_OFTEN}
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index 405d292dfafa..2e8c28d8e65b 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -21,6 +21,16 @@ flag {
}
flag {
+ name: "disable_transfer_when_apps_do_not_support"
+ namespace: "media_better_together"
+ description: "Fixes a bug causing output switcher routes to be incorrectly enabled for media transfer."
+ bug: "373404114"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_audio_input_device_routing_and_volume_control"
namespace: "media_better_together"
description: "Allows audio input devices routing and volume control via system settings."
diff --git a/nfc-non-updatable/flags/flags.aconfig b/nfc-non-updatable/flags/flags.aconfig
index 54ded0cddffa..eb30bbe1bfe7 100644
--- a/nfc-non-updatable/flags/flags.aconfig
+++ b/nfc-non-updatable/flags/flags.aconfig
@@ -198,10 +198,6 @@ flag {
bug: "380892385"
}
-flag {
- name: "nfc_hce_latency_events"
- is_exported: true
- namespace: "wallet_integration"
- description: "Enables tracking latency for HCE"
- bug: "379849603"
-}
+# Unless you are adding a flag for a file under nfc-non-updatable, you should
+# not add a flag here for Android 16+ targeting features. Use the flags
+# in com.android.nfc.module.flags (packages/modules/Nfc/flags) instead.
diff --git a/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java
index 93d6eb73dcae..e83b9f1afddb 100644
--- a/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -572,8 +572,10 @@ public final class ApduServiceInfo implements Parcelable {
if (mAutoTransact.getOrDefault(plf.toUpperCase(Locale.ROOT), false)) {
return true;
}
- List<Pattern> patternMatches = mAutoTransactPatterns.keySet().stream()
- .filter(p -> p.matcher(plf).matches()).toList();
+ boolean isPattern = plf.contains("?") || plf.contains("*");
+ List<Pattern> patternMatches = mAutoTransactPatterns.keySet().stream().filter(
+ p -> isPattern ? p.toString().equals(plf) : p.matcher(plf).matches()).toList();
+
if (patternMatches == null || patternMatches.size() == 0) {
return false;
}
diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/ExplicitHealthCheckService.java b/packages/CrashRecovery/framework/java/android/service/watchdog/ExplicitHealthCheckService.java
deleted file mode 100644
index fdb0fc538fdf..000000000000
--- a/packages/CrashRecovery/framework/java/android/service/watchdog/ExplicitHealthCheckService.java
+++ /dev/null
@@ -1,359 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.service.watchdog;
-
-import static android.os.Parcelable.Creator;
-
-import android.annotation.CallbackExecutor;
-import android.annotation.FlaggedApi;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.SdkConstant;
-import android.annotation.SuppressLint;
-import android.annotation.SystemApi;
-import android.app.Service;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.crashrecovery.flags.Flags;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.os.RemoteCallback;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.internal.util.Preconditions;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Consumer;
-
-/**
- * A service to provide packages supporting explicit health checks and route checks to these
- * packages on behalf of the package watchdog.
- *
- * <p>To extend this class, you must declare the service in your manifest file with the
- * {@link android.Manifest.permission#BIND_EXPLICIT_HEALTH_CHECK_SERVICE} permission,
- * and include an intent filter with the {@link #SERVICE_INTERFACE} action. In adddition,
- * your implementation must live in
- * {@link PackageManager#getServicesSystemSharedLibraryPackageName()}.
- * For example:</p>
- * <pre>
- * &lt;service android:name=".FooExplicitHealthCheckService"
- * android:exported="true"
- * android:priority="100"
- * android:permission="android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE"&gt;
- * &lt;intent-filter&gt;
- * &lt;action android:name="android.service.watchdog.ExplicitHealthCheckService" /&gt;
- * &lt;/intent-filter&gt;
- * &lt;/service&gt;
- * </pre>
- * @hide
- */
-@SystemApi
-public abstract class ExplicitHealthCheckService extends Service {
-
- private static final String TAG = "ExplicitHealthCheckService";
-
- /**
- * {@link Bundle} key for a {@link List} of {@link PackageConfig} value.
- *
- * {@hide}
- */
- public static final String EXTRA_SUPPORTED_PACKAGES =
- "android.service.watchdog.extra.supported_packages";
-
- /**
- * {@link Bundle} key for a {@link List} of {@link String} value.
- *
- * {@hide}
- */
- public static final String EXTRA_REQUESTED_PACKAGES =
- "android.service.watchdog.extra.requested_packages";
-
- /**
- * {@link Bundle} key for a {@link String} value.
- */
- @FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY)
- public static final String EXTRA_HEALTH_CHECK_PASSED_PACKAGE =
- "android.service.watchdog.extra.HEALTH_CHECK_PASSED_PACKAGE";
-
- /**
- * The Intent action that a service must respond to. Add it to the intent filter of the service
- * in its manifest.
- */
- @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
- public static final String SERVICE_INTERFACE =
- "android.service.watchdog.ExplicitHealthCheckService";
-
- /**
- * The permission that a service must require to ensure that only Android system can bind to it.
- * If this permission is not enforced in the AndroidManifest of the service, the system will
- * skip that service.
- */
- public static final String BIND_PERMISSION =
- "android.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE";
-
- private final ExplicitHealthCheckServiceWrapper mWrapper =
- new ExplicitHealthCheckServiceWrapper();
-
- /**
- * Called when the system requests an explicit health check for {@code packageName}.
- *
- * <p> When {@code packageName} passes the check, implementors should call
- * {@link #notifyHealthCheckPassed} to inform the system.
- *
- * <p> It could take many hours before a {@code packageName} passes a check and implementors
- * should never drop requests unless {@link onCancel} is called or the service dies.
- *
- * <p> Requests should not be queued and additional calls while expecting a result for
- * {@code packageName} should have no effect.
- */
- public abstract void onRequestHealthCheck(@NonNull String packageName);
-
- /**
- * Called when the system cancels the explicit health check request for {@code packageName}.
- * Should do nothing if there are is no active request for {@code packageName}.
- */
- public abstract void onCancelHealthCheck(@NonNull String packageName);
-
- /**
- * Called when the system requests for all the packages supporting explicit health checks. The
- * system may request an explicit health check for any of these packages with
- * {@link #onRequestHealthCheck}.
- *
- * @return all packages supporting explicit health checks
- */
- @NonNull public abstract List<PackageConfig> onGetSupportedPackages();
-
- /**
- * Called when the system requests for all the packages that it has currently requested
- * an explicit health check for.
- *
- * @return all packages expecting an explicit health check result
- */
- @NonNull public abstract List<String> onGetRequestedPackages();
-
- private final Handler mHandler = Handler.createAsync(Looper.getMainLooper());
- @Nullable private Consumer<Bundle> mHealthCheckResultCallback;
- @Nullable private Executor mCallbackExecutor;
-
- @Override
- @NonNull
- public final IBinder onBind(@NonNull Intent intent) {
- return mWrapper;
- }
-
- /**
- * Sets a callback to be invoked when an explicit health check passes for a package.
- * <p>
- * The callback will receive a {@link Bundle} containing the package name that passed the
- * health check, identified by the key {@link #EXTRA_HEALTH_CHECK_PASSED_PACKAGE}.
- * <p>
- * <b>Note:</b> This API is primarily intended for testing purposes. Calling this outside of a
- * test environment will override the default callback mechanism used to notify the system
- * about health check results. Use with caution in production code.
- *
- * @param executor The executor on which the callback should be invoked. If {@code null}, the
- * callback will be executed on the main thread.
- * @param callback A callback that receives a {@link Bundle} containing the package name that
- * passed the health check.
- */
- @FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY)
- public final void setHealthCheckPassedCallback(@CallbackExecutor @Nullable Executor executor,
- @Nullable Consumer<Bundle> callback) {
- mCallbackExecutor = executor;
- mHealthCheckResultCallback = callback;
- }
-
- private void executeCallback(@NonNull String packageName) {
- if (mHealthCheckResultCallback != null) {
- Objects.requireNonNull(packageName,
- "Package passing explicit health check must be non-null");
- Bundle bundle = new Bundle();
- bundle.putString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE, packageName);
- mHealthCheckResultCallback.accept(bundle);
- } else {
- Log.wtf(TAG, "System missed explicit health check result for " + packageName);
- }
- }
-
- /**
- * Implementors should call this to notify the system when explicit health check passes
- * for {@code packageName};
- */
- public final void notifyHealthCheckPassed(@NonNull String packageName) {
- if (mCallbackExecutor != null) {
- mCallbackExecutor.execute(() -> executeCallback(packageName));
- } else {
- mHandler.post(() -> executeCallback(packageName));
- }
- }
-
- /**
- * A PackageConfig contains a package supporting explicit health checks and the
- * timeout in {@link System#uptimeMillis} across reboots after which health
- * check requests from clients are failed.
- *
- * @hide
- */
- @SystemApi
- public static final class PackageConfig implements Parcelable {
- private static final long DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(1);
-
- private final String mPackageName;
- private final long mHealthCheckTimeoutMillis;
-
- /**
- * Creates a new instance.
- *
- * @param packageName the package name
- * @param durationMillis the duration in milliseconds, must be greater than or
- * equal to 0. If it is 0, it will use a system default value.
- */
- public PackageConfig(@NonNull String packageName, long healthCheckTimeoutMillis) {
- mPackageName = Preconditions.checkNotNull(packageName);
- if (healthCheckTimeoutMillis == 0) {
- mHealthCheckTimeoutMillis = DEFAULT_HEALTH_CHECK_TIMEOUT_MILLIS;
- } else {
- mHealthCheckTimeoutMillis = Preconditions.checkArgumentNonnegative(
- healthCheckTimeoutMillis);
- }
- }
-
- private PackageConfig(Parcel parcel) {
- mPackageName = parcel.readString();
- mHealthCheckTimeoutMillis = parcel.readLong();
- }
-
- /**
- * Gets the package name.
- *
- * @return the package name
- */
- public @NonNull String getPackageName() {
- return mPackageName;
- }
-
- /**
- * Gets the timeout in milliseconds to evaluate an explicit health check result after a
- * request.
- *
- * @return the duration in {@link System#uptimeMillis} across reboots
- */
- public long getHealthCheckTimeoutMillis() {
- return mHealthCheckTimeoutMillis;
- }
-
- @NonNull
- @Override
- public String toString() {
- return "PackageConfig{" + mPackageName + ", " + mHealthCheckTimeoutMillis + "}";
- }
-
- @Override
- public boolean equals(@Nullable Object other) {
- if (other == this) {
- return true;
- }
- if (!(other instanceof PackageConfig)) {
- return false;
- }
-
- PackageConfig otherInfo = (PackageConfig) other;
- return Objects.equals(otherInfo.getHealthCheckTimeoutMillis(),
- mHealthCheckTimeoutMillis)
- && Objects.equals(otherInfo.getPackageName(), mPackageName);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(mPackageName, mHealthCheckTimeoutMillis);
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(@SuppressLint({"MissingNullability"}) Parcel parcel, int flags) {
- parcel.writeString(mPackageName);
- parcel.writeLong(mHealthCheckTimeoutMillis);
- }
-
- public static final @NonNull Creator<PackageConfig> CREATOR = new Creator<PackageConfig>() {
- @Override
- public PackageConfig createFromParcel(Parcel source) {
- return new PackageConfig(source);
- }
-
- @Override
- public PackageConfig[] newArray(int size) {
- return new PackageConfig[size];
- }
- };
- }
-
-
- private class ExplicitHealthCheckServiceWrapper extends IExplicitHealthCheckService.Stub {
- @Override
- public void setCallback(RemoteCallback callback) throws RemoteException {
- mHandler.post(() -> mHealthCheckResultCallback = callback::sendResult);
- }
-
- @Override
- public void request(String packageName) throws RemoteException {
- mHandler.post(() -> ExplicitHealthCheckService.this.onRequestHealthCheck(packageName));
- }
-
- @Override
- public void cancel(String packageName) throws RemoteException {
- mHandler.post(() -> ExplicitHealthCheckService.this.onCancelHealthCheck(packageName));
- }
-
- @Override
- public void getSupportedPackages(RemoteCallback callback) throws RemoteException {
- mHandler.post(() -> {
- List<PackageConfig> packages =
- ExplicitHealthCheckService.this.onGetSupportedPackages();
- Objects.requireNonNull(packages, "Supported package list must be non-null");
- Bundle bundle = new Bundle();
- bundle.putParcelableArrayList(EXTRA_SUPPORTED_PACKAGES, new ArrayList<>(packages));
- callback.sendResult(bundle);
- });
- }
-
- @Override
- public void getRequestedPackages(RemoteCallback callback) throws RemoteException {
- mHandler.post(() -> {
- List<String> packages =
- ExplicitHealthCheckService.this.onGetRequestedPackages();
- Objects.requireNonNull(packages, "Requested package list must be non-null");
- Bundle bundle = new Bundle();
- bundle.putStringArrayList(EXTRA_REQUESTED_PACKAGES, new ArrayList<>(packages));
- callback.sendResult(bundle);
- });
- }
- }
-}
diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/OWNERS b/packages/CrashRecovery/framework/java/android/service/watchdog/OWNERS
deleted file mode 100644
index 1c045e10c0ec..000000000000
--- a/packages/CrashRecovery/framework/java/android/service/watchdog/OWNERS
+++ /dev/null
@@ -1,3 +0,0 @@
-narayan@google.com
-nandana@google.com
-olilan@google.com
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java b/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java
deleted file mode 100644
index da9a13961f79..000000000000
--- a/packages/CrashRecovery/services/module/java/com/android/server/ExplicitHealthCheckController.java
+++ /dev/null
@@ -1,447 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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;
-
-import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_HEALTH_CHECK_PASSED_PACKAGE;
-import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_REQUESTED_PACKAGES;
-import static android.service.watchdog.ExplicitHealthCheckService.EXTRA_SUPPORTED_PACKAGES;
-import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig;
-
-import android.Manifest;
-import android.annotation.MainThread;
-import android.annotation.Nullable;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.os.IBinder;
-import android.os.RemoteCallback;
-import android.os.RemoteException;
-import android.os.UserHandle;
-import android.service.watchdog.ExplicitHealthCheckService;
-import android.service.watchdog.IExplicitHealthCheckService;
-import android.text.TextUtils;
-import android.util.ArraySet;
-import android.util.Slog;
-
-import com.android.internal.annotations.GuardedBy;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Objects;
-import java.util.Set;
-import java.util.function.Consumer;
-
-// TODO(b/120598832): Add tests
-/**
- * Controls the connections with {@link ExplicitHealthCheckService}.
- */
-class ExplicitHealthCheckController {
- private static final String TAG = "ExplicitHealthCheckController";
- private final Object mLock = new Object();
- private final Context mContext;
-
- // Called everytime a package passes the health check, so the watchdog is notified of the
- // passing check. In practice, should never be null after it has been #setEnabled.
- // To prevent deadlocks between the controller and watchdog threads, we have
- // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class.
- // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer.
- @GuardedBy("mLock") @Nullable private Consumer<String> mPassedConsumer;
- // Called everytime after a successful #syncRequest call, so the watchdog can receive packages
- // supporting health checks and update its internal state. In practice, should never be null
- // after it has been #setEnabled.
- // To prevent deadlocks between the controller and watchdog threads, we have
- // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class.
- // It's easier to just NOT hold #mLock when calling into watchdog code on this consumer.
- @GuardedBy("mLock") @Nullable private Consumer<List<PackageConfig>> mSupportedConsumer;
- // Called everytime we need to notify the watchdog to sync requests between itself and the
- // health check service. In practice, should never be null after it has been #setEnabled.
- // To prevent deadlocks between the controller and watchdog threads, we have
- // a lock invariant to ALWAYS acquire the PackageWatchdog#mLock before #mLock in this class.
- // It's easier to just NOT hold #mLock when calling into watchdog code on this runnable.
- @GuardedBy("mLock") @Nullable private Runnable mNotifySyncRunnable;
- // Actual binder object to the explicit health check service.
- @GuardedBy("mLock") @Nullable private IExplicitHealthCheckService mRemoteService;
- // Connection to the explicit health check service, necessary to unbind.
- // We should only try to bind if mConnection is null, non-null indicates we
- // are connected or at least connecting.
- @GuardedBy("mLock") @Nullable private ServiceConnection mConnection;
- // Bind state of the explicit health check service.
- @GuardedBy("mLock") private boolean mEnabled;
-
- ExplicitHealthCheckController(Context context) {
- mContext = context;
- }
-
- /** Enables or disables explicit health checks. */
- public void setEnabled(boolean enabled) {
- synchronized (mLock) {
- Slog.i(TAG, "Explicit health checks " + (enabled ? "enabled." : "disabled."));
- mEnabled = enabled;
- }
- }
-
- /**
- * Sets callbacks to listen to important events from the controller.
- *
- * <p> Should be called once at initialization before any other calls to the controller to
- * ensure a happens-before relationship of the set parameters and visibility on other threads.
- */
- public void setCallbacks(Consumer<String> passedConsumer,
- Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable) {
- synchronized (mLock) {
- if (mPassedConsumer != null || mSupportedConsumer != null
- || mNotifySyncRunnable != null) {
- Slog.wtf(TAG, "Resetting health check controller callbacks");
- }
-
- mPassedConsumer = Objects.requireNonNull(passedConsumer);
- mSupportedConsumer = Objects.requireNonNull(supportedConsumer);
- mNotifySyncRunnable = Objects.requireNonNull(notifySyncRunnable);
- }
- }
-
- /**
- * Calls the health check service to request or cancel packages based on
- * {@code newRequestedPackages}.
- *
- * <p> Supported packages in {@code newRequestedPackages} that have not been previously
- * requested will be requested while supported packages not in {@code newRequestedPackages}
- * but were previously requested will be cancelled.
- *
- * <p> This handles binding and unbinding to the health check service as required.
- *
- * <p> Note, calling this may modify {@code newRequestedPackages}.
- *
- * <p> Note, this method is not thread safe, all calls should be serialized.
- */
- public void syncRequests(Set<String> newRequestedPackages) {
- boolean enabled;
- synchronized (mLock) {
- enabled = mEnabled;
- }
-
- if (!enabled) {
- Slog.i(TAG, "Health checks disabled, no supported packages");
- // Call outside lock
- mSupportedConsumer.accept(Collections.emptyList());
- return;
- }
-
- getSupportedPackages(supportedPackageConfigs -> {
- // Notify the watchdog without lock held
- mSupportedConsumer.accept(supportedPackageConfigs);
- getRequestedPackages(previousRequestedPackages -> {
- synchronized (mLock) {
- // Hold lock so requests and cancellations are sent atomically.
- // It is important we don't mix requests from multiple threads.
-
- Set<String> supportedPackages = new ArraySet<>();
- for (PackageConfig config : supportedPackageConfigs) {
- supportedPackages.add(config.getPackageName());
- }
- // Note, this may modify newRequestedPackages
- newRequestedPackages.retainAll(supportedPackages);
-
- // Cancel packages no longer requested
- actOnDifference(previousRequestedPackages,
- newRequestedPackages, p -> cancel(p));
- // Request packages not yet requested
- actOnDifference(newRequestedPackages,
- previousRequestedPackages, p -> request(p));
-
- if (newRequestedPackages.isEmpty()) {
- Slog.i(TAG, "No more health check requests, unbinding...");
- unbindService();
- return;
- }
- }
- });
- });
- }
-
- private void actOnDifference(Collection<String> collection1, Collection<String> collection2,
- Consumer<String> action) {
- Iterator<String> iterator = collection1.iterator();
- while (iterator.hasNext()) {
- String packageName = iterator.next();
- if (!collection2.contains(packageName)) {
- action.accept(packageName);
- }
- }
- }
-
- /**
- * Requests an explicit health check for {@code packageName}.
- * After this request, the callback registered on {@link #setCallbacks} can receive explicit
- * health check passed results.
- */
- private void request(String packageName) {
- synchronized (mLock) {
- if (!prepareServiceLocked("request health check for " + packageName)) {
- return;
- }
-
- Slog.i(TAG, "Requesting health check for package " + packageName);
- try {
- mRemoteService.request(packageName);
- } catch (RemoteException e) {
- Slog.w(TAG, "Failed to request health check for package " + packageName, e);
- }
- }
- }
-
- /**
- * Cancels all explicit health checks for {@code packageName}.
- * After this request, the callback registered on {@link #setCallbacks} can no longer receive
- * explicit health check passed results.
- */
- private void cancel(String packageName) {
- synchronized (mLock) {
- if (!prepareServiceLocked("cancel health check for " + packageName)) {
- return;
- }
-
- Slog.i(TAG, "Cancelling health check for package " + packageName);
- try {
- mRemoteService.cancel(packageName);
- } catch (RemoteException e) {
- // Do nothing, if the service is down, when it comes up, we will sync requests,
- // if there's some other error, retrying wouldn't fix anyways.
- Slog.w(TAG, "Failed to cancel health check for package " + packageName, e);
- }
- }
- }
-
- /**
- * Returns the packages that we can request explicit health checks for.
- * The packages will be returned to the {@code consumer}.
- */
- private void getSupportedPackages(Consumer<List<PackageConfig>> consumer) {
- synchronized (mLock) {
- if (!prepareServiceLocked("get health check supported packages")) {
- return;
- }
-
- Slog.d(TAG, "Getting health check supported packages");
- try {
- mRemoteService.getSupportedPackages(new RemoteCallback(result -> {
- List<PackageConfig> packages =
- result.getParcelableArrayList(EXTRA_SUPPORTED_PACKAGES, android.service.watchdog.ExplicitHealthCheckService.PackageConfig.class);
- Slog.i(TAG, "Explicit health check supported packages " + packages);
- consumer.accept(packages);
- }));
- } catch (RemoteException e) {
- // Request failed, treat as if all observed packages are supported, if any packages
- // expire during this period, we may incorrectly treat it as failing health checks
- // even if we don't support health checks for the package.
- Slog.w(TAG, "Failed to get health check supported packages", e);
- }
- }
- }
-
- /**
- * Returns the packages for which health checks are currently in progress.
- * The packages will be returned to the {@code consumer}.
- */
- private void getRequestedPackages(Consumer<List<String>> consumer) {
- synchronized (mLock) {
- if (!prepareServiceLocked("get health check requested packages")) {
- return;
- }
-
- Slog.d(TAG, "Getting health check requested packages");
- try {
- mRemoteService.getRequestedPackages(new RemoteCallback(result -> {
- List<String> packages = result.getStringArrayList(EXTRA_REQUESTED_PACKAGES);
- Slog.i(TAG, "Explicit health check requested packages " + packages);
- consumer.accept(packages);
- }));
- } catch (RemoteException e) {
- // Request failed, treat as if we haven't requested any packages, if any packages
- // were actually requested, they will not be cancelled now. May be cancelled later
- Slog.w(TAG, "Failed to get health check requested packages", e);
- }
- }
- }
-
- /**
- * Binds to the explicit health check service if the controller is enabled and
- * not already bound.
- */
- private void bindService() {
- synchronized (mLock) {
- if (!mEnabled || mConnection != null || mRemoteService != null) {
- if (!mEnabled) {
- Slog.i(TAG, "Not binding to service, service disabled");
- } else if (mRemoteService != null) {
- Slog.i(TAG, "Not binding to service, service already connected");
- } else {
- Slog.i(TAG, "Not binding to service, service already connecting");
- }
- return;
- }
- ComponentName component = getServiceComponentNameLocked();
- if (component == null) {
- Slog.wtf(TAG, "Explicit health check service not found");
- return;
- }
-
- Intent intent = new Intent();
- intent.setComponent(component);
- mConnection = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- Slog.i(TAG, "Explicit health check service is connected " + name);
- initState(service);
- }
-
- @Override
- @MainThread
- public void onServiceDisconnected(ComponentName name) {
- // Service crashed or process was killed, #onServiceConnected will be called.
- // Don't need to re-bind.
- Slog.i(TAG, "Explicit health check service is disconnected " + name);
- synchronized (mLock) {
- mRemoteService = null;
- }
- }
-
- @Override
- public void onBindingDied(ComponentName name) {
- // Application hosting service probably got updated
- // Need to re-bind.
- Slog.i(TAG, "Explicit health check service binding is dead. Rebind: " + name);
- unbindService();
- bindService();
- }
-
- @Override
- public void onNullBinding(ComponentName name) {
- // Should never happen. Service returned null from #onBind.
- Slog.wtf(TAG, "Explicit health check service binding is null?? " + name);
- }
- };
-
- mContext.bindServiceAsUser(intent, mConnection,
- Context.BIND_AUTO_CREATE, UserHandle.SYSTEM);
- Slog.i(TAG, "Explicit health check service is bound");
- }
- }
-
- /** Unbinds the explicit health check service. */
- private void unbindService() {
- synchronized (mLock) {
- if (mRemoteService != null) {
- mContext.unbindService(mConnection);
- mRemoteService = null;
- mConnection = null;
- }
- Slog.i(TAG, "Explicit health check service is unbound");
- }
- }
-
- @GuardedBy("mLock")
- @Nullable
- private ServiceInfo getServiceInfoLocked() {
- final Intent intent = new Intent(ExplicitHealthCheckService.SERVICE_INTERFACE);
- final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
- PackageManager.GET_SERVICES | PackageManager.GET_META_DATA
- | PackageManager.MATCH_SYSTEM_ONLY);
- if (resolveInfo == null || resolveInfo.serviceInfo == null) {
- Slog.w(TAG, "No valid components found.");
- return null;
- }
- return resolveInfo.serviceInfo;
- }
-
- @GuardedBy("mLock")
- @Nullable
- private ComponentName getServiceComponentNameLocked() {
- final ServiceInfo serviceInfo = getServiceInfoLocked();
- if (serviceInfo == null) {
- return null;
- }
-
- final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name);
- if (!Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE
- .equals(serviceInfo.permission)) {
- Slog.w(TAG, name.flattenToShortString() + " does not require permission "
- + Manifest.permission.BIND_EXPLICIT_HEALTH_CHECK_SERVICE);
- return null;
- }
- return name;
- }
-
- private void initState(IBinder service) {
- synchronized (mLock) {
- if (!mEnabled) {
- Slog.w(TAG, "Attempting to connect disabled service?? Unbinding...");
- // Very unlikely, but we disabled the service after binding but before we connected
- unbindService();
- return;
- }
- mRemoteService = IExplicitHealthCheckService.Stub.asInterface(service);
- try {
- mRemoteService.setCallback(new RemoteCallback(result -> {
- String packageName = result.getString(EXTRA_HEALTH_CHECK_PASSED_PACKAGE);
- if (!TextUtils.isEmpty(packageName)) {
- if (mPassedConsumer == null) {
- Slog.wtf(TAG, "Health check passed for package " + packageName
- + "but no consumer registered.");
- } else {
- // Call without lock held
- mPassedConsumer.accept(packageName);
- }
- } else {
- Slog.wtf(TAG, "Empty package passed explicit health check?");
- }
- }));
- Slog.i(TAG, "Service initialized, syncing requests");
- } catch (RemoteException e) {
- Slog.wtf(TAG, "Could not setCallback on explicit health check service");
- }
- }
- // Calling outside lock
- mNotifySyncRunnable.run();
- }
-
- /**
- * Prepares the health check service to receive requests.
- *
- * @return {@code true} if it is ready and we can proceed with a request,
- * {@code false} otherwise. If it is not ready, and the service is enabled,
- * we will bind and the request should be automatically attempted later.
- */
- @GuardedBy("mLock")
- private boolean prepareServiceLocked(String action) {
- if (mRemoteService != null && mEnabled) {
- return true;
- }
- Slog.i(TAG, "Service not ready to " + action
- + (mEnabled ? ". Binding..." : ". Disabled"));
- if (mEnabled) {
- bindService();
- }
- return false;
- }
-}
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java
deleted file mode 100644
index e4f07f9fc213..000000000000
--- a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java
+++ /dev/null
@@ -1,2253 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server;
-
-import static android.content.Intent.ACTION_REBOOT;
-import static android.content.Intent.ACTION_SHUTDOWN;
-import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig;
-import static android.util.Xml.Encoding.UTF_8;
-
-import static com.android.server.crashrecovery.CrashRecoveryUtils.dumpCrashRecoveryEvents;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import android.annotation.CallbackExecutor;
-import android.annotation.FlaggedApi;
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.SuppressLint;
-import android.annotation.SystemApi;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.VersionedPackage;
-import android.crashrecovery.flags.Flags;
-import android.os.Environment;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Process;
-import android.os.SystemProperties;
-import android.provider.DeviceConfig;
-import android.sysprop.CrashRecoveryProperties;
-import android.text.TextUtils;
-import android.util.ArrayMap;
-import android.util.ArraySet;
-import android.util.AtomicFile;
-import android.util.EventLog;
-import android.util.IndentingPrintWriter;
-import android.util.LongArrayQueue;
-import android.util.Slog;
-import android.util.Xml;
-import android.util.XmlUtils;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.FastXmlSerializer;
-import com.android.modules.utils.BackgroundThread;
-
-import libcore.io.IoUtils;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlSerializer;
-
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.PrintWriter;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.NoSuchElementException;
-import java.util.Set;
-import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Monitors the health of packages on the system and notifies interested observers when packages
- * fail. On failure, the registered observer with the least user impacting mitigation will
- * be notified.
- * @hide
- */
-@FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY)
-@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
-public class PackageWatchdog {
- private static final String TAG = "PackageWatchdog";
-
- static final String PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS =
- "watchdog_trigger_failure_duration_millis";
- static final String PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT =
- "watchdog_trigger_failure_count";
- static final String PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED =
- "watchdog_explicit_health_check_enabled";
-
- // TODO: make the following values configurable via DeviceConfig
- private static final long NATIVE_CRASH_POLLING_INTERVAL_MILLIS =
- TimeUnit.SECONDS.toMillis(30);
- private static final long NUMBER_OF_NATIVE_CRASH_POLLS = 10;
-
-
- /** Reason for package failure could not be determined. */
- public static final int FAILURE_REASON_UNKNOWN = 0;
-
- /** The package had a native crash. */
- public static final int FAILURE_REASON_NATIVE_CRASH = 1;
-
- /** The package failed an explicit health check. */
- public static final int FAILURE_REASON_EXPLICIT_HEALTH_CHECK = 2;
-
- /** The app crashed. */
- public static final int FAILURE_REASON_APP_CRASH = 3;
-
- /** The app was not responding. */
- public static final int FAILURE_REASON_APP_NOT_RESPONDING = 4;
-
- /** The device was boot looping. */
- public static final int FAILURE_REASON_BOOT_LOOP = 5;
-
- /** @hide */
- @IntDef(prefix = { "FAILURE_REASON_" }, value = {
- FAILURE_REASON_UNKNOWN,
- FAILURE_REASON_NATIVE_CRASH,
- FAILURE_REASON_EXPLICIT_HEALTH_CHECK,
- FAILURE_REASON_APP_CRASH,
- FAILURE_REASON_APP_NOT_RESPONDING,
- FAILURE_REASON_BOOT_LOOP
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface FailureReasons {}
-
- // Duration to count package failures before it resets to 0
- @VisibleForTesting
- static final int DEFAULT_TRIGGER_FAILURE_DURATION_MS =
- (int) TimeUnit.MINUTES.toMillis(1);
- // Number of package failures within the duration above before we notify observers
- @VisibleForTesting
- static final int DEFAULT_TRIGGER_FAILURE_COUNT = 5;
- @VisibleForTesting
- static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2);
- // Sliding window for tracking how many mitigation calls were made for a package.
- @VisibleForTesting
- static final long DEFAULT_DEESCALATION_WINDOW_MS = TimeUnit.HOURS.toMillis(1);
- // Whether explicit health checks are enabled or not
- private static final boolean DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED = true;
-
- @VisibleForTesting
- static final int DEFAULT_BOOT_LOOP_TRIGGER_COUNT = 5;
-
- static final long DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS = TimeUnit.MINUTES.toMillis(10);
-
- // Time needed to apply mitigation
- private static final String MITIGATION_WINDOW_MS =
- "persist.device_config.configuration.mitigation_window_ms";
- @VisibleForTesting
- static final long DEFAULT_MITIGATION_WINDOW_MS = TimeUnit.SECONDS.toMillis(5);
-
- // Threshold level at which or above user might experience significant disruption.
- private static final String MAJOR_USER_IMPACT_LEVEL_THRESHOLD =
- "persist.device_config.configuration.major_user_impact_level_threshold";
- private static final int DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD =
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_71;
-
- // Comma separated list of all packages exempt from user impact level threshold. If a package
- // in the list is crash looping, all the mitigations including factory reset will be performed.
- private static final String PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD =
- "persist.device_config.configuration.packages_exempt_from_impact_level_threshold";
-
- // Comma separated list of default packages exempt from user impact level threshold.
- private static final String DEFAULT_PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD =
- "com.android.systemui";
-
- private long mNumberOfNativeCrashPollsRemaining;
-
- private static final int DB_VERSION = 1;
- private static final String TAG_PACKAGE_WATCHDOG = "package-watchdog";
- private static final String TAG_PACKAGE = "package";
- private static final String TAG_OBSERVER = "observer";
- private static final String ATTR_VERSION = "version";
- private static final String ATTR_NAME = "name";
- private static final String ATTR_DURATION = "duration";
- private static final String ATTR_EXPLICIT_HEALTH_CHECK_DURATION = "health-check-duration";
- private static final String ATTR_PASSED_HEALTH_CHECK = "passed-health-check";
- private static final String ATTR_MITIGATION_CALLS = "mitigation-calls";
- private static final String ATTR_MITIGATION_COUNT = "mitigation-count";
-
- // A file containing information about the current mitigation count in the case of a boot loop.
- // This allows boot loop information to persist in the case of an fs-checkpoint being
- // aborted.
- private static final String METADATA_FILE = "/metadata/watchdog/mitigation_count.txt";
-
- /**
- * EventLog tags used when logging into the event log. Note the values must be sync with
- * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct
- * name translation.
- */
- private static final int LOG_TAG_RESCUE_NOTE = 2900;
-
- private static final Object sPackageWatchdogLock = new Object();
- @GuardedBy("sPackageWatchdogLock")
- private static PackageWatchdog sPackageWatchdog;
-
- private static final Object sLock = new Object();
- // System server context
- private final Context mContext;
- // Handler to run short running tasks
- private final Handler mShortTaskHandler;
- // Handler for processing IO and long running tasks
- private final Handler mLongTaskHandler;
- // Contains (observer-name -> observer-handle) that have ever been registered from
- // previous boots. Observers with all packages expired are periodically pruned.
- // It is saved to disk on system shutdown and repouplated on startup so it survives reboots.
- @GuardedBy("sLock")
- private final ArrayMap<String, ObserverInternal> mAllObservers = new ArrayMap<>();
- // File containing the XML data of monitored packages /data/system/package-watchdog.xml
- private final AtomicFile mPolicyFile;
- private final ExplicitHealthCheckController mHealthCheckController;
- private final Runnable mSyncRequests = this::syncRequests;
- private final Runnable mSyncStateWithScheduledReason = this::syncStateWithScheduledReason;
- private final Runnable mSaveToFile = this::saveToFile;
- private final SystemClock mSystemClock;
- private final BootThreshold mBootThreshold;
- private final DeviceConfig.OnPropertiesChangedListener
- mOnPropertyChangedListener = this::onPropertyChanged;
-
- private final Set<String> mPackagesExemptFromImpactLevelThreshold = new ArraySet<>();
-
- // The set of packages that have been synced with the ExplicitHealthCheckController
- @GuardedBy("sLock")
- private Set<String> mRequestedHealthCheckPackages = new ArraySet<>();
- @GuardedBy("sLock")
- private boolean mIsPackagesReady;
- // Flag to control whether explicit health checks are supported or not
- @GuardedBy("sLock")
- private boolean mIsHealthCheckEnabled = DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED;
- @GuardedBy("sLock")
- private int mTriggerFailureDurationMs = DEFAULT_TRIGGER_FAILURE_DURATION_MS;
- @GuardedBy("sLock")
- private int mTriggerFailureCount = DEFAULT_TRIGGER_FAILURE_COUNT;
- // SystemClock#uptimeMillis when we last executed #syncState
- // 0 if no prune is scheduled.
- @GuardedBy("sLock")
- private long mUptimeAtLastStateSync;
- // If true, sync explicit health check packages with the ExplicitHealthCheckController.
- @GuardedBy("sLock")
- private boolean mSyncRequired = false;
-
- @GuardedBy("sLock")
- private long mLastMitigation = -1000000;
-
- @FunctionalInterface
- @VisibleForTesting
- interface SystemClock {
- long uptimeMillis();
- }
-
- private PackageWatchdog(Context context) {
- // Needs to be constructed inline
- this(context, new AtomicFile(
- new File(new File(Environment.getDataDirectory(), "system"),
- "package-watchdog.xml")),
- new Handler(Looper.myLooper()), BackgroundThread.getHandler(),
- new ExplicitHealthCheckController(context),
- android.os.SystemClock::uptimeMillis);
- }
-
- /**
- * Creates a PackageWatchdog that allows injecting dependencies.
- */
- @VisibleForTesting
- PackageWatchdog(Context context, AtomicFile policyFile, Handler shortTaskHandler,
- Handler longTaskHandler, ExplicitHealthCheckController controller,
- SystemClock clock) {
- mContext = context;
- mPolicyFile = policyFile;
- mShortTaskHandler = shortTaskHandler;
- mLongTaskHandler = longTaskHandler;
- mHealthCheckController = controller;
- mSystemClock = clock;
- mNumberOfNativeCrashPollsRemaining = NUMBER_OF_NATIVE_CRASH_POLLS;
- mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT,
- DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS);
-
- loadFromFile();
- sPackageWatchdog = this;
- }
-
- /**
- * Creates or gets singleton instance of PackageWatchdog.
- *
- * @param context The system server context.
- */
- public static @NonNull PackageWatchdog getInstance(@NonNull Context context) {
- synchronized (sPackageWatchdogLock) {
- if (sPackageWatchdog == null) {
- new PackageWatchdog(context);
- }
- return sPackageWatchdog;
- }
- }
-
- /**
- * Called during boot to notify when packages are ready on the device so we can start
- * binding.
- * @hide
- */
- public void onPackagesReady() {
- synchronized (sLock) {
- mIsPackagesReady = true;
- mHealthCheckController.setCallbacks(packageName -> onHealthCheckPassed(packageName),
- packages -> onSupportedPackages(packages),
- this::onSyncRequestNotified);
- setPropertyChangedListenerLocked();
- updateConfigs();
- }
- }
-
- /**
- * Registers {@code observer} to listen for package failures. Add a new ObserverInternal for
- * this observer if it does not already exist.
- * For executing mitigations observers will receive callback on the given executor.
- *
- * <p>Observers are expected to call this on boot. It does not specify any packages but
- * it will resume observing any packages requested from a previous boot.
- *
- * @param observer instance of {@link PackageHealthObserver} for observing package failures
- * and boot loops.
- * @param executor Executor for the thread on which observers would receive callbacks
- */
- public void registerHealthObserver(@NonNull @CallbackExecutor Executor executor,
- @NonNull PackageHealthObserver observer) {
- synchronized (sLock) {
- ObserverInternal internalObserver = mAllObservers.get(observer.getUniqueIdentifier());
- if (internalObserver != null) {
- internalObserver.registeredObserver = observer;
- internalObserver.observerExecutor = executor;
- } else {
- internalObserver = new ObserverInternal(observer.getUniqueIdentifier(),
- new ArrayList<>());
- internalObserver.registeredObserver = observer;
- internalObserver.observerExecutor = executor;
- mAllObservers.put(observer.getUniqueIdentifier(), internalObserver);
- syncState("added new observer");
- }
- }
- }
-
- /**
- * Starts observing the health of the {@code packages} for {@code observer}.
- * Note: Observer needs to be registered with {@link #registerHealthObserver} before calling
- * this API.
- *
- * <p>If monitoring a package supporting explicit health check, at the end of the monitoring
- * duration if {@link #onHealthCheckPassed} was never called,
- * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} will be called as if the
- * package failed.
- *
- * <p>If {@code observer} is already monitoring a package in {@code packageNames},
- * the monitoring window of that package will be reset to {@code durationMs} and the health
- * check state will be reset to a default.
- *
- * <p>The {@code observer} must be registered with {@link #registerHealthObserver} before
- * calling this method.
- *
- * @param packageNames The list of packages to check. If this is empty, the call will be a
- * no-op.
- *
- * @param timeoutMs The timeout after which Explicit Health Checks would not run. If this is
- * less than 1, a default monitoring duration 2 days will be used.
- *
- * @throws IllegalStateException if the observer was not previously registered
- */
- public void startExplicitHealthCheck(@NonNull List<String> packageNames, long timeoutMs,
- @NonNull PackageHealthObserver observer) {
- synchronized (sLock) {
- if (!mAllObservers.containsKey(observer.getUniqueIdentifier())) {
- Slog.wtf(TAG, "No observer found, need to register the observer: "
- + observer.getUniqueIdentifier());
- throw new IllegalStateException("Observer not registered");
- }
- }
- if (packageNames.isEmpty()) {
- Slog.wtf(TAG, "No packages to observe, " + observer.getUniqueIdentifier());
- return;
- }
- if (timeoutMs < 1) {
- Slog.wtf(TAG, "Invalid duration " + timeoutMs + "ms for observer "
- + observer.getUniqueIdentifier() + ". Not observing packages " + packageNames);
- timeoutMs = DEFAULT_OBSERVING_DURATION_MS;
- }
-
- List<MonitoredPackage> packages = new ArrayList<>();
- for (int i = 0; i < packageNames.size(); i++) {
- // Health checks not available yet so health check state will start INACTIVE
- MonitoredPackage pkg = newMonitoredPackage(packageNames.get(i), timeoutMs, false);
- if (pkg != null) {
- packages.add(pkg);
- } else {
- Slog.w(TAG, "Failed to create MonitoredPackage for pkg=" + packageNames.get(i));
- }
- }
-
- if (packages.isEmpty()) {
- return;
- }
-
- // Sync before we add the new packages to the observers. This will #pruneObservers,
- // causing any elapsed time to be deducted from all existing packages before we add new
- // packages. This maintains the invariant that the elapsed time for ALL (new and existing)
- // packages is the same.
- mLongTaskHandler.post(() -> {
- syncState("observing new packages");
-
- synchronized (sLock) {
- ObserverInternal oldObserver = mAllObservers.get(observer.getUniqueIdentifier());
- if (oldObserver == null) {
- Slog.d(TAG, observer.getUniqueIdentifier() + " started monitoring health "
- + "of packages " + packageNames);
- mAllObservers.put(observer.getUniqueIdentifier(),
- new ObserverInternal(observer.getUniqueIdentifier(), packages));
- } else {
- Slog.d(TAG, observer.getUniqueIdentifier() + " added the following "
- + "packages to monitor " + packageNames);
- oldObserver.updatePackagesLocked(packages);
- }
- }
-
- // Sync after we add the new packages to the observers. We may have received packges
- // requiring an earlier schedule than we are currently scheduled for.
- syncState("updated observers");
- });
-
- }
-
- /**
- * Unregisters {@code observer} from listening to package failure.
- * Additionally, this stops observing any packages that may have previously been observed
- * even from a previous boot.
- */
- public void unregisterHealthObserver(@NonNull PackageHealthObserver observer) {
- mLongTaskHandler.post(() -> {
- synchronized (sLock) {
- mAllObservers.remove(observer.getUniqueIdentifier());
- }
- syncState("unregistering observer: " + observer.getUniqueIdentifier());
- });
- }
-
- /**
- * Called when a process fails due to a crash, ANR or explicit health check.
- *
- * <p>For each package contained in the process, one registered observer with the least user
- * impact will be notified for mitigation.
- *
- * <p>This method could be called frequently if there is a severe problem on the device.
- */
- public void notifyPackageFailure(@NonNull List<VersionedPackage> packages,
- @FailureReasons int failureReason) {
- if (packages == null) {
- Slog.w(TAG, "Could not resolve a list of failing packages");
- return;
- }
- synchronized (sLock) {
- final long now = mSystemClock.uptimeMillis();
- if (Flags.recoverabilityDetection()) {
- if (now >= mLastMitigation
- && (now - mLastMitigation) < getMitigationWindowMs()) {
- Slog.i(TAG, "Skipping notifyPackageFailure mitigation");
- return;
- }
- }
- }
- mLongTaskHandler.post(() -> {
- synchronized (sLock) {
- if (mAllObservers.isEmpty()) {
- return;
- }
- boolean requiresImmediateAction = (failureReason == FAILURE_REASON_NATIVE_CRASH
- || failureReason == FAILURE_REASON_EXPLICIT_HEALTH_CHECK);
- if (requiresImmediateAction) {
- handleFailureImmediately(packages, failureReason);
- } else {
- for (int pIndex = 0; pIndex < packages.size(); pIndex++) {
- VersionedPackage versionedPackage = packages.get(pIndex);
- // Observer that will receive failure for versionedPackage
- ObserverInternal currentObserverToNotify = null;
- int currentObserverImpact = Integer.MAX_VALUE;
- MonitoredPackage currentMonitoredPackage = null;
-
- // Find observer with least user impact
- for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) {
- ObserverInternal observer = mAllObservers.valueAt(oIndex);
- PackageHealthObserver registeredObserver = observer.registeredObserver;
- if (registeredObserver != null
- && observer.notifyPackageFailureLocked(
- versionedPackage.getPackageName())) {
- MonitoredPackage p = observer.getMonitoredPackage(
- versionedPackage.getPackageName());
- int mitigationCount = 1;
- if (p != null) {
- mitigationCount = p.getMitigationCountLocked() + 1;
- }
- int impact = registeredObserver.onHealthCheckFailed(
- versionedPackage, failureReason, mitigationCount);
- if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0
- && impact < currentObserverImpact) {
- currentObserverToNotify = observer;
- currentObserverImpact = impact;
- currentMonitoredPackage = p;
- }
- }
- }
-
- // Execute action with least user impact
- if (currentObserverToNotify != null) {
- int mitigationCount;
- if (currentMonitoredPackage != null) {
- currentMonitoredPackage.noteMitigationCallLocked();
- mitigationCount =
- currentMonitoredPackage.getMitigationCountLocked();
- } else {
- mitigationCount = 1;
- }
- if (Flags.recoverabilityDetection()) {
- maybeExecute(currentObserverToNotify, versionedPackage,
- failureReason, currentObserverImpact, mitigationCount);
- } else {
- PackageHealthObserver registeredObserver =
- currentObserverToNotify.registeredObserver;
- currentObserverToNotify.observerExecutor.execute(() ->
- registeredObserver.onExecuteHealthCheckMitigation(
- versionedPackage, failureReason, mitigationCount));
- }
- }
- }
- }
- }
- });
- }
-
- /**
- * For native crashes or explicit health check failures, call directly into each observer to
- * mitigate the error without going through failure threshold logic.
- */
- @GuardedBy("sLock")
- private void handleFailureImmediately(List<VersionedPackage> packages,
- @FailureReasons int failureReason) {
- VersionedPackage failingPackage = packages.size() > 0 ? packages.get(0) : null;
- ObserverInternal currentObserverToNotify = null;
- int currentObserverImpact = Integer.MAX_VALUE;
- for (ObserverInternal observer: mAllObservers.values()) {
- PackageHealthObserver registeredObserver = observer.registeredObserver;
- if (registeredObserver != null) {
- int impact = registeredObserver.onHealthCheckFailed(
- failingPackage, failureReason, 1);
- if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0
- && impact < currentObserverImpact) {
- currentObserverToNotify = observer;
- currentObserverImpact = impact;
- }
- }
- }
- if (currentObserverToNotify != null) {
- if (Flags.recoverabilityDetection()) {
- maybeExecute(currentObserverToNotify, failingPackage, failureReason,
- currentObserverImpact, /*mitigationCount=*/ 1);
- } else {
- PackageHealthObserver registeredObserver =
- currentObserverToNotify.registeredObserver;
- currentObserverToNotify.observerExecutor.execute(() ->
- registeredObserver.onExecuteHealthCheckMitigation(failingPackage,
- failureReason, 1));
-
- }
- }
- }
-
- private void maybeExecute(ObserverInternal currentObserverToNotify,
- VersionedPackage versionedPackage,
- @FailureReasons int failureReason,
- int currentObserverImpact,
- int mitigationCount) {
- if (allowMitigations(currentObserverImpact, versionedPackage)) {
- PackageHealthObserver registeredObserver;
- synchronized (sLock) {
- mLastMitigation = mSystemClock.uptimeMillis();
- registeredObserver = currentObserverToNotify.registeredObserver;
- }
- currentObserverToNotify.observerExecutor.execute(() ->
- registeredObserver.onExecuteHealthCheckMitigation(versionedPackage,
- failureReason, mitigationCount));
- }
- }
-
- private boolean allowMitigations(int currentObserverImpact,
- VersionedPackage versionedPackage) {
- return currentObserverImpact < getUserImpactLevelLimit()
- || getPackagesExemptFromImpactLevelThreshold().contains(
- versionedPackage.getPackageName());
- }
-
- private long getMitigationWindowMs() {
- return SystemProperties.getLong(MITIGATION_WINDOW_MS, DEFAULT_MITIGATION_WINDOW_MS);
- }
-
-
- /**
- * Called when the system server boots. If the system server is detected to be in a boot loop,
- * query each observer and perform the mitigation action with the lowest user impact.
- *
- * Note: PackageWatchdog considers system_server restart loop as bootloop. Full reboots
- * are not counted in bootloop.
- * @hide
- */
- @SuppressWarnings("GuardedBy")
- public void noteBoot() {
- synchronized (sLock) {
- // if boot count has reached threshold, start mitigation.
- // We wait until threshold number of restarts only for the first time. Perform
- // mitigations for every restart after that.
- boolean mitigate = mBootThreshold.incrementAndTest();
- if (mitigate) {
- if (!Flags.recoverabilityDetection()) {
- mBootThreshold.reset();
- }
- int mitigationCount = mBootThreshold.getMitigationCount() + 1;
- ObserverInternal currentObserverToNotify = null;
- int currentObserverImpact = Integer.MAX_VALUE;
- for (int i = 0; i < mAllObservers.size(); i++) {
- final ObserverInternal observer = mAllObservers.valueAt(i);
- PackageHealthObserver registeredObserver = observer.registeredObserver;
- if (registeredObserver != null) {
- int impact = Flags.recoverabilityDetection()
- ? registeredObserver.onBootLoop(
- observer.getBootMitigationCount() + 1)
- : registeredObserver.onBootLoop(mitigationCount);
- if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0
- && impact < currentObserverImpact) {
- currentObserverToNotify = observer;
- currentObserverImpact = impact;
- }
- }
- }
-
- if (currentObserverToNotify != null) {
- PackageHealthObserver registeredObserver =
- currentObserverToNotify.registeredObserver;
- if (Flags.recoverabilityDetection()) {
- int currentObserverMitigationCount =
- currentObserverToNotify.getBootMitigationCount() + 1;
- currentObserverToNotify.setBootMitigationCount(
- currentObserverMitigationCount);
- saveAllObserversBootMitigationCountToMetadata(METADATA_FILE);
- currentObserverToNotify.observerExecutor
- .execute(() -> registeredObserver.onExecuteBootLoopMitigation(
- currentObserverMitigationCount));
- } else {
- mBootThreshold.setMitigationCount(mitigationCount);
- mBootThreshold.saveMitigationCountToMetadata();
- currentObserverToNotify.observerExecutor
- .execute(() -> registeredObserver.onExecuteBootLoopMitigation(
- mitigationCount));
-
- }
- }
- }
- }
- }
-
- // TODO(b/120598832): Optimize write? Maybe only write a separate smaller file? Also
- // avoid holding lock?
- // This currently adds about 7ms extra to shutdown thread
- /** @hide Writes the package information to file during shutdown. */
- public void writeNow() {
- synchronized (sLock) {
- // Must only run synchronous tasks as this runs on the ShutdownThread and no other
- // thread is guaranteed to run during shutdown.
- if (!mAllObservers.isEmpty()) {
- mLongTaskHandler.removeCallbacks(mSaveToFile);
- pruneObserversLocked();
- saveToFile();
- Slog.i(TAG, "Last write to update package durations");
- }
- }
- }
-
- /**
- * Enables or disables explicit health checks.
- * <p> If explicit health checks are enabled, the health check service is started.
- * <p> If explicit health checks are disabled, pending explicit health check requests are
- * passed and the health check service is stopped.
- */
- private void setExplicitHealthCheckEnabled(boolean enabled) {
- synchronized (sLock) {
- mIsHealthCheckEnabled = enabled;
- mHealthCheckController.setEnabled(enabled);
- mSyncRequired = true;
- // Prune to update internal state whenever health check is enabled/disabled
- syncState("health check state " + (enabled ? "enabled" : "disabled"));
- }
- }
-
- /**
- * This method should be only called on mShortTaskHandler, since it modifies
- * {@link #mNumberOfNativeCrashPollsRemaining}.
- */
- private void checkAndMitigateNativeCrashes() {
- mNumberOfNativeCrashPollsRemaining--;
- // Check if native watchdog reported a crash
- if ("1".equals(SystemProperties.get("sys.init.updatable_crashing"))) {
- // We rollback all available low impact rollbacks when crash is unattributable
- notifyPackageFailure(Collections.EMPTY_LIST, FAILURE_REASON_NATIVE_CRASH);
- // we stop polling after an attempt to execute rollback, regardless of whether the
- // attempt succeeds or not
- } else {
- if (mNumberOfNativeCrashPollsRemaining > 0) {
- mShortTaskHandler.postDelayed(() -> checkAndMitigateNativeCrashes(),
- NATIVE_CRASH_POLLING_INTERVAL_MILLIS);
- }
- }
- }
-
- /**
- * Since this method can eventually trigger a rollback, it should be called
- * only once boot has completed {@code onBootCompleted} and not earlier, because the install
- * session must be entirely completed before we try to rollback.
- * @hide
- */
- public void scheduleCheckAndMitigateNativeCrashes() {
- Slog.i(TAG, "Scheduling " + mNumberOfNativeCrashPollsRemaining + " polls to check "
- + "and mitigate native crashes");
- mShortTaskHandler.post(()->checkAndMitigateNativeCrashes());
- }
-
- private int getUserImpactLevelLimit() {
- return SystemProperties.getInt(MAJOR_USER_IMPACT_LEVEL_THRESHOLD,
- DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD);
- }
-
- private Set<String> getPackagesExemptFromImpactLevelThreshold() {
- if (mPackagesExemptFromImpactLevelThreshold.isEmpty()) {
- String packageNames = SystemProperties.get(PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD,
- DEFAULT_PACKAGES_EXEMPT_FROM_IMPACT_LEVEL_THRESHOLD);
- return Set.of(packageNames.split("\\s*,\\s*"));
- }
- return mPackagesExemptFromImpactLevelThreshold;
- }
-
- /**
- * Indicates that a mitigation was successfully triggered or executed during
- * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} or
- * {@link PackageHealthObserver#onExecuteBootLoopMitigation}.
- */
- public static final int MITIGATION_RESULT_SUCCESS =
- ObserverMitigationResult.MITIGATION_RESULT_SUCCESS;
-
- /**
- * Indicates that a mitigation executed during
- * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} or
- * {@link PackageHealthObserver#onExecuteBootLoopMitigation} was skipped.
- */
- public static final int MITIGATION_RESULT_SKIPPED =
- ObserverMitigationResult.MITIGATION_RESULT_SKIPPED;
-
-
- /**
- * Possible return values of the for mitigations executed during
- * {@link PackageHealthObserver#onExecuteHealthCheckMitigation} and
- * {@link PackageHealthObserver#onExecuteBootLoopMitigation}.
- * @hide
- */
- @Retention(SOURCE)
- @IntDef(prefix = "MITIGATION_RESULT_", value = {
- ObserverMitigationResult.MITIGATION_RESULT_SUCCESS,
- ObserverMitigationResult.MITIGATION_RESULT_SKIPPED,
- })
- public @interface ObserverMitigationResult {
- int MITIGATION_RESULT_SUCCESS = 1;
- int MITIGATION_RESULT_SKIPPED = 2;
- }
-
- /**
- * The minimum value that can be returned by any observer.
- * It represents that no mitigations were available.
- */
- public static final int USER_IMPACT_THRESHOLD_NONE =
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
-
- /**
- * The mitigation impact beyond which the user will start noticing the mitigations.
- */
- public static final int USER_IMPACT_THRESHOLD_MEDIUM =
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_20;
-
- /**
- * The mitigation impact beyond which the user impact is severely high.
- */
- public static final int USER_IMPACT_THRESHOLD_HIGH =
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_71;
-
- /**
- * Possible severity values of the user impact of a
- * {@link PackageHealthObserver#onExecuteHealthCheckMitigation}.
- * @hide
- */
- @Retention(SOURCE)
- @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0,
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_10,
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_20,
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_30,
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_40,
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_50,
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_70,
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_71,
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_75,
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_80,
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_90,
- PackageHealthObserverImpact.USER_IMPACT_LEVEL_100})
- public @interface PackageHealthObserverImpact {
- /** No action to take. */
- int USER_IMPACT_LEVEL_0 = 0;
- /* Action has low user impact, user of a device will barely notice. */
- int USER_IMPACT_LEVEL_10 = 10;
- /* Actions having medium user impact, user of a device will likely notice. */
- int USER_IMPACT_LEVEL_20 = 20;
- int USER_IMPACT_LEVEL_30 = 30;
- int USER_IMPACT_LEVEL_40 = 40;
- int USER_IMPACT_LEVEL_50 = 50;
- int USER_IMPACT_LEVEL_70 = 70;
- /* Action has high user impact, a last resort, user of a device will be very frustrated. */
- int USER_IMPACT_LEVEL_71 = 71;
- int USER_IMPACT_LEVEL_75 = 75;
- int USER_IMPACT_LEVEL_80 = 80;
- int USER_IMPACT_LEVEL_90 = 90;
- int USER_IMPACT_LEVEL_100 = 100;
- }
-
- /** Register instances of this interface to receive notifications on package failure. */
- @SuppressLint({"CallbackName"})
- public interface PackageHealthObserver {
- /**
- * Called when health check fails for the {@code versionedPackage}.
- * Note: if the returned user impact is higher than {@link #USER_IMPACT_THRESHOLD_HIGH},
- * then {@link #onExecuteHealthCheckMitigation} would be called only in severe device
- * conditions like boot-loop or network failure.
- *
- * @param versionedPackage the package that is failing. This may be null if a native
- * service is crashing.
- * @param failureReason the type of failure that is occurring.
- * @param mitigationCount the number of times mitigation has been called for this package
- * (including this time).
- *
- * @return any value greater than {@link #USER_IMPACT_THRESHOLD_NONE} to express
- * the impact of mitigation on the user in {@link #onExecuteHealthCheckMitigation}.
- * Returning {@link #USER_IMPACT_THRESHOLD_NONE} would indicate no mitigations available.
- */
- @PackageHealthObserverImpact int onHealthCheckFailed(
- @Nullable VersionedPackage versionedPackage,
- @FailureReasons int failureReason,
- int mitigationCount);
-
- /**
- * This would be called after {@link #onHealthCheckFailed}.
- * This is called only if current observer returned least impact mitigation for failed
- * health check.
- *
- * @param versionedPackage the package that is failing. This may be null if a native
- * service is crashing.
- * @param failureReason the type of failure that is occurring.
- * @param mitigationCount the number of times mitigation has been called for this package
- * (including this time).
- * @return {@link #MITIGATION_RESULT_SUCCESS} if the mitigation was successful,
- * or {@link #MITIGATION_RESULT_SKIPPED} if the mitigation was skipped.
- */
- @ObserverMitigationResult int onExecuteHealthCheckMitigation(
- @Nullable VersionedPackage versionedPackage,
- @FailureReasons int failureReason, int mitigationCount);
-
-
- /**
- * Called when the system server has booted several times within a window of time, defined
- * by {@link #mBootThreshold}
- *
- * @param mitigationCount the number of times mitigation has been attempted for this
- * boot loop (including this time).
- *
- * @return any value greater than {@link #USER_IMPACT_THRESHOLD_NONE} to express
- * the impact of mitigation on the user in {@link #onExecuteBootLoopMitigation}.
- * Returning {@link #USER_IMPACT_THRESHOLD_NONE} would indicate no mitigations available.
- */
- default @PackageHealthObserverImpact int onBootLoop(int mitigationCount) {
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
- }
-
- /**
- * This would be called after {@link #onBootLoop}.
- * This is called only if current observer returned least impact mitigation for fixing
- * boot loop.
- *
- * @param mitigationCount the number of times mitigation has been attempted for this
- * boot loop (including this time).
- *
- * @return {@link #MITIGATION_RESULT_SUCCESS} if the mitigation was successful,
- * or {@link #MITIGATION_RESULT_SKIPPED} if the mitigation was skipped.
- */
- default @ObserverMitigationResult int onExecuteBootLoopMitigation(int mitigationCount) {
- return ObserverMitigationResult.MITIGATION_RESULT_SKIPPED;
- }
-
- // TODO(b/120598832): Ensure uniqueness?
- /**
- * Identifier for the observer, should not change across device updates otherwise the
- * watchdog may drop observing packages with the old name.
- */
- @NonNull String getUniqueIdentifier();
-
- /**
- * An observer will not be pruned if this is set, even if the observer is not explicitly
- * monitoring any packages.
- */
- default boolean isPersistent() {
- return false;
- }
-
- /**
- * Returns {@code true} if this observer wishes to observe the given package, {@code false}
- * otherwise.
- * Any failing package can be passed on to the observer. Currently the packages that have
- * ANRs and perform {@link android.service.watchdog.ExplicitHealthCheckService} are being
- * passed to observers in these API.
- *
- * <p> A persistent observer may choose to start observing certain failing packages, even if
- * it has not explicitly asked to watch the package with {@link #startExplicitHealthCheck}.
- */
- default boolean mayObservePackage(@NonNull String packageName) {
- return false;
- }
- }
-
- @VisibleForTesting
- long getTriggerFailureCount() {
- synchronized (sLock) {
- return mTriggerFailureCount;
- }
- }
-
- @VisibleForTesting
- long getTriggerFailureDurationMs() {
- synchronized (sLock) {
- return mTriggerFailureDurationMs;
- }
- }
-
- /**
- * Serializes and syncs health check requests with the {@link ExplicitHealthCheckController}.
- */
- private void syncRequestsAsync() {
- mShortTaskHandler.removeCallbacks(mSyncRequests);
- mShortTaskHandler.post(mSyncRequests);
- }
-
- /**
- * Syncs health check requests with the {@link ExplicitHealthCheckController}.
- * Calls to this must be serialized.
- *
- * @see #syncRequestsAsync
- */
- private void syncRequests() {
- boolean syncRequired = false;
- synchronized (sLock) {
- if (mIsPackagesReady) {
- Set<String> packages = getPackagesPendingHealthChecksLocked();
- if (mSyncRequired || !packages.equals(mRequestedHealthCheckPackages)
- || packages.isEmpty()) {
- syncRequired = true;
- mRequestedHealthCheckPackages = packages;
- }
- } // else, we will sync requests when packages become ready
- }
-
- // Call outside lock to avoid holding lock when calling into the controller.
- if (syncRequired) {
- Slog.i(TAG, "Syncing health check requests for packages: "
- + mRequestedHealthCheckPackages);
- mHealthCheckController.syncRequests(mRequestedHealthCheckPackages);
- mSyncRequired = false;
- }
- }
-
- /**
- * Updates the observers monitoring {@code packageName} that explicit health check has passed.
- *
- * <p> This update is strictly for registered observers at the time of the call
- * Observers that register after this signal will have no knowledge of prior signals and will
- * effectively behave as if the explicit health check hasn't passed for {@code packageName}.
- *
- * <p> {@code packageName} can still be considered failed if reported by
- * {@link #notifyPackageFailureLocked} before the package expires.
- *
- * <p> Triggered by components outside the system server when they are fully functional after an
- * update.
- */
- private void onHealthCheckPassed(String packageName) {
- Slog.i(TAG, "Health check passed for package: " + packageName);
- boolean isStateChanged = false;
-
- synchronized (sLock) {
- for (int observerIdx = 0; observerIdx < mAllObservers.size(); observerIdx++) {
- ObserverInternal observer = mAllObservers.valueAt(observerIdx);
- MonitoredPackage monitoredPackage = observer.getMonitoredPackage(packageName);
-
- if (monitoredPackage != null) {
- int oldState = monitoredPackage.getHealthCheckStateLocked();
- int newState = monitoredPackage.tryPassHealthCheckLocked();
- isStateChanged |= oldState != newState;
- }
- }
- }
-
- if (isStateChanged) {
- syncState("health check passed for " + packageName);
- }
- }
-
- private void onSupportedPackages(List<PackageConfig> supportedPackages) {
- boolean isStateChanged = false;
-
- Map<String, Long> supportedPackageTimeouts = new ArrayMap<>();
- Iterator<PackageConfig> it = supportedPackages.iterator();
- while (it.hasNext()) {
- PackageConfig info = it.next();
- supportedPackageTimeouts.put(info.getPackageName(), info.getHealthCheckTimeoutMillis());
- }
-
- synchronized (sLock) {
- Slog.d(TAG, "Received supported packages " + supportedPackages);
- Iterator<ObserverInternal> oit = mAllObservers.values().iterator();
- while (oit.hasNext()) {
- Iterator<MonitoredPackage> pit = oit.next().getMonitoredPackages()
- .values().iterator();
- while (pit.hasNext()) {
- MonitoredPackage monitoredPackage = pit.next();
- String packageName = monitoredPackage.getName();
- int oldState = monitoredPackage.getHealthCheckStateLocked();
- int newState;
-
- if (supportedPackageTimeouts.containsKey(packageName)) {
- // Supported packages become ACTIVE if currently INACTIVE
- newState = monitoredPackage.setHealthCheckActiveLocked(
- supportedPackageTimeouts.get(packageName));
- } else {
- // Unsupported packages are marked as PASSED unless already FAILED
- newState = monitoredPackage.tryPassHealthCheckLocked();
- }
- isStateChanged |= oldState != newState;
- }
- }
- }
-
- if (isStateChanged) {
- syncState("updated health check supported packages " + supportedPackages);
- }
- }
-
- private void onSyncRequestNotified() {
- synchronized (sLock) {
- mSyncRequired = true;
- syncRequestsAsync();
- }
- }
-
- @GuardedBy("sLock")
- private Set<String> getPackagesPendingHealthChecksLocked() {
- Set<String> packages = new ArraySet<>();
- Iterator<ObserverInternal> oit = mAllObservers.values().iterator();
- while (oit.hasNext()) {
- ObserverInternal observer = oit.next();
- Iterator<MonitoredPackage> pit =
- observer.getMonitoredPackages().values().iterator();
- while (pit.hasNext()) {
- MonitoredPackage monitoredPackage = pit.next();
- String packageName = monitoredPackage.getName();
- if (monitoredPackage.isPendingHealthChecksLocked()) {
- packages.add(packageName);
- }
- }
- }
- return packages;
- }
-
- /**
- * Syncs the state of the observers.
- *
- * <p> Prunes all observers, saves new state to disk, syncs health check requests with the
- * health check service and schedules the next state sync.
- */
- private void syncState(String reason) {
- synchronized (sLock) {
- Slog.i(TAG, "Syncing state, reason: " + reason);
- pruneObserversLocked();
-
- saveToFileAsync();
- syncRequestsAsync();
-
- // Done syncing state, schedule the next state sync
- scheduleNextSyncStateLocked();
- }
- }
-
- private void syncStateWithScheduledReason() {
- syncState("scheduled");
- }
-
- @GuardedBy("sLock")
- private void scheduleNextSyncStateLocked() {
- long durationMs = getNextStateSyncMillisLocked();
- mShortTaskHandler.removeCallbacks(mSyncStateWithScheduledReason);
- if (durationMs == Long.MAX_VALUE) {
- Slog.i(TAG, "Cancelling state sync, nothing to sync");
- mUptimeAtLastStateSync = 0;
- } else {
- mUptimeAtLastStateSync = mSystemClock.uptimeMillis();
- mShortTaskHandler.postDelayed(mSyncStateWithScheduledReason, durationMs);
- }
- }
-
- /**
- * Returns the next duration in millis to sync the watchdog state.
- *
- * @returns Long#MAX_VALUE if there are no observed packages.
- */
- @GuardedBy("sLock")
- private long getNextStateSyncMillisLocked() {
- long shortestDurationMs = Long.MAX_VALUE;
- for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) {
- ArrayMap<String, MonitoredPackage> packages = mAllObservers.valueAt(oIndex)
- .getMonitoredPackages();
- for (int pIndex = 0; pIndex < packages.size(); pIndex++) {
- MonitoredPackage mp = packages.valueAt(pIndex);
- long duration = mp.getShortestScheduleDurationMsLocked();
- if (duration < shortestDurationMs) {
- shortestDurationMs = duration;
- }
- }
- }
- return shortestDurationMs;
- }
-
- /**
- * Removes {@code elapsedMs} milliseconds from all durations on monitored packages
- * and updates other internal state.
- */
- @GuardedBy("sLock")
- private void pruneObserversLocked() {
- long elapsedMs = mUptimeAtLastStateSync == 0
- ? 0 : mSystemClock.uptimeMillis() - mUptimeAtLastStateSync;
- if (elapsedMs <= 0) {
- Slog.i(TAG, "Not pruning observers, elapsed time: " + elapsedMs + "ms");
- return;
- }
-
- Iterator<ObserverInternal> it = mAllObservers.values().iterator();
- while (it.hasNext()) {
- ObserverInternal observer = it.next();
- Set<MonitoredPackage> failedPackages =
- observer.prunePackagesLocked(elapsedMs);
- if (!failedPackages.isEmpty()) {
- onHealthCheckFailed(observer, failedPackages);
- }
- if (observer.getMonitoredPackages().isEmpty() && (observer.registeredObserver == null
- || !observer.registeredObserver.isPersistent())) {
- Slog.i(TAG, "Discarding observer " + observer.name + ". All packages expired");
- it.remove();
- }
- }
- }
-
- private void onHealthCheckFailed(ObserverInternal observer,
- Set<MonitoredPackage> failedPackages) {
- mLongTaskHandler.post(() -> {
- synchronized (sLock) {
- PackageHealthObserver registeredObserver = observer.registeredObserver;
- if (registeredObserver != null) {
- Iterator<MonitoredPackage> it = failedPackages.iterator();
- while (it.hasNext()) {
- VersionedPackage versionedPkg = getVersionedPackage(it.next().getName());
- if (versionedPkg != null) {
- Slog.i(TAG,
- "Explicit health check failed for package " + versionedPkg);
- observer.observerExecutor.execute(() ->
- registeredObserver.onExecuteHealthCheckMitigation(versionedPkg,
- PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK,
- 1));
- }
- }
- }
- }
- });
- }
-
- /**
- * Gets PackageInfo for the given package. Matches any user and apex.
- *
- * @throws PackageManager.NameNotFoundException if no such package is installed.
- */
- private PackageInfo getPackageInfo(String packageName)
- throws PackageManager.NameNotFoundException {
- PackageManager pm = mContext.getPackageManager();
- try {
- // The MATCH_ANY_USER flag doesn't mix well with the MATCH_APEX
- // flag, so make two separate attempts to get the package info.
- // We don't need both flags at the same time because we assume
- // apex files are always installed for all users.
- return pm.getPackageInfo(packageName, PackageManager.MATCH_ANY_USER);
- } catch (PackageManager.NameNotFoundException e) {
- return pm.getPackageInfo(packageName, PackageManager.MATCH_APEX);
- }
- }
-
- @Nullable
- private VersionedPackage getVersionedPackage(String packageName) {
- final PackageManager pm = mContext.getPackageManager();
- if (pm == null || TextUtils.isEmpty(packageName)) {
- return null;
- }
- try {
- final long versionCode = getPackageInfo(packageName).getLongVersionCode();
- return new VersionedPackage(packageName, versionCode);
- } catch (PackageManager.NameNotFoundException e) {
- return null;
- }
- }
-
- /**
- * Loads mAllObservers from file.
- *
- * <p>Note that this is <b>not</b> thread safe and should only called be called
- * from the constructor.
- */
- private void loadFromFile() {
- InputStream infile = null;
- mAllObservers.clear();
- try {
- infile = mPolicyFile.openRead();
- final XmlPullParser parser = Xml.newPullParser();
- parser.setInput(infile, UTF_8.name());
- XmlUtils.beginDocument(parser, TAG_PACKAGE_WATCHDOG);
- int outerDepth = parser.getDepth();
- while (XmlUtils.nextElementWithin(parser, outerDepth)) {
- ObserverInternal observer = ObserverInternal.read(parser, this);
- if (observer != null) {
- mAllObservers.put(observer.name, observer);
- }
- }
- } catch (FileNotFoundException e) {
- // Nothing to monitor
- } catch (Exception e) {
- Slog.wtf(TAG, "Unable to read monitored packages, deleting file", e);
- mPolicyFile.delete();
- } finally {
- IoUtils.closeQuietly(infile);
- }
- }
-
- private void onPropertyChanged(DeviceConfig.Properties properties) {
- try {
- updateConfigs();
- } catch (Exception ignore) {
- Slog.w(TAG, "Failed to reload device config changes");
- }
- }
-
- /** Adds a {@link DeviceConfig#OnPropertiesChangedListener}. */
- private void setPropertyChangedListenerLocked() {
- DeviceConfig.addOnPropertiesChangedListener(
- DeviceConfig.NAMESPACE_ROLLBACK,
- mContext.getMainExecutor(),
- mOnPropertyChangedListener);
- }
-
- @VisibleForTesting
- void removePropertyChangedListener() {
- DeviceConfig.removeOnPropertiesChangedListener(mOnPropertyChangedListener);
- }
-
- /**
- * Health check is enabled or disabled after reading the flags
- * from DeviceConfig.
- */
- @VisibleForTesting
- void updateConfigs() {
- synchronized (sLock) {
- mTriggerFailureCount = DeviceConfig.getInt(
- DeviceConfig.NAMESPACE_ROLLBACK,
- PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT,
- DEFAULT_TRIGGER_FAILURE_COUNT);
- if (mTriggerFailureCount <= 0) {
- mTriggerFailureCount = DEFAULT_TRIGGER_FAILURE_COUNT;
- }
-
- mTriggerFailureDurationMs = DeviceConfig.getInt(
- DeviceConfig.NAMESPACE_ROLLBACK,
- PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS,
- DEFAULT_TRIGGER_FAILURE_DURATION_MS);
- if (mTriggerFailureDurationMs <= 0) {
- mTriggerFailureDurationMs = DEFAULT_TRIGGER_FAILURE_DURATION_MS;
- }
-
- setExplicitHealthCheckEnabled(DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_ROLLBACK,
- PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED,
- DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED));
- }
- }
-
- /**
- * Persists mAllObservers to file. Threshold information is ignored.
- */
- private boolean saveToFile() {
- Slog.i(TAG, "Saving observer state to file");
- synchronized (sLock) {
- FileOutputStream stream;
- try {
- stream = mPolicyFile.startWrite();
- } catch (IOException e) {
- Slog.w(TAG, "Cannot update monitored packages", e);
- return false;
- }
-
- try {
- XmlSerializer out = new FastXmlSerializer();
- out.setOutput(stream, UTF_8.name());
- out.startDocument(null, true);
- out.startTag(null, TAG_PACKAGE_WATCHDOG);
- out.attribute(null, ATTR_VERSION, Integer.toString(DB_VERSION));
- for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) {
- mAllObservers.valueAt(oIndex).writeLocked(out);
- }
- out.endTag(null, TAG_PACKAGE_WATCHDOG);
- out.endDocument();
- mPolicyFile.finishWrite(stream);
- return true;
- } catch (IOException e) {
- Slog.w(TAG, "Failed to save monitored packages, restoring backup", e);
- mPolicyFile.failWrite(stream);
- return false;
- }
- }
- }
-
- private void saveToFileAsync() {
- if (!mLongTaskHandler.hasCallbacks(mSaveToFile)) {
- mLongTaskHandler.post(mSaveToFile);
- }
- }
-
- /** @hide Convert a {@code LongArrayQueue} to a String of comma-separated values. */
- public static String longArrayQueueToString(LongArrayQueue queue) {
- if (queue.size() > 0) {
- StringBuilder sb = new StringBuilder();
- sb.append(queue.get(0));
- for (int i = 1; i < queue.size(); i++) {
- sb.append(",");
- sb.append(queue.get(i));
- }
- return sb.toString();
- }
- return "";
- }
-
- /** @hide Parse a comma-separated String of longs into a LongArrayQueue. */
- public static LongArrayQueue parseLongArrayQueue(String commaSeparatedValues) {
- LongArrayQueue result = new LongArrayQueue();
- if (!TextUtils.isEmpty(commaSeparatedValues)) {
- String[] values = commaSeparatedValues.split(",");
- for (String value : values) {
- result.addLast(Long.parseLong(value));
- }
- }
- return result;
- }
-
-
- /** Dump status of every observer in mAllObservers. */
- public void dump(@NonNull PrintWriter pw) {
- if (Flags.synchronousRebootInRescueParty() && isRecoveryTriggeredReboot()) {
- dumpInternal(pw);
- } else {
- synchronized (sLock) {
- dumpInternal(pw);
- }
- }
- }
-
- /**
- * Check if we're currently attempting to reboot during mitigation. This method must return
- * true if triggered reboot early during a boot loop, since the device will not be fully booted
- * at this time.
- */
- public static boolean isRecoveryTriggeredReboot() {
- return isFactoryResetPropertySet() || isRebootPropertySet();
- }
-
- private static boolean isFactoryResetPropertySet() {
- return CrashRecoveryProperties.attemptingFactoryReset().orElse(false);
- }
-
- private static boolean isRebootPropertySet() {
- return CrashRecoveryProperties.attemptingReboot().orElse(false);
- }
-
- private void dumpInternal(@NonNull PrintWriter pw) {
- IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
- ipw.println("Package Watchdog status");
- ipw.increaseIndent();
- synchronized (sLock) {
- for (String observerName : mAllObservers.keySet()) {
- ipw.println("Observer name: " + observerName);
- ipw.increaseIndent();
- ObserverInternal observerInternal = mAllObservers.get(observerName);
- observerInternal.dump(ipw);
- ipw.decreaseIndent();
- }
- }
- ipw.decreaseIndent();
- dumpCrashRecoveryEvents(ipw);
- }
-
- @VisibleForTesting
- @GuardedBy("sLock")
- void registerObserverInternal(ObserverInternal observerInternal) {
- mAllObservers.put(observerInternal.name, observerInternal);
- }
-
- /**
- * Represents an observer monitoring a set of packages along with the failure thresholds for
- * each package.
- *
- * <p> Note, the PackageWatchdog#sLock must always be held when reading or writing
- * instances of this class.
- */
- static class ObserverInternal {
- public final String name;
- @GuardedBy("sLock")
- private final ArrayMap<String, MonitoredPackage> mPackages = new ArrayMap<>();
- @Nullable
- @GuardedBy("sLock")
- public PackageHealthObserver registeredObserver;
- public Executor observerExecutor;
- private int mMitigationCount;
-
- ObserverInternal(String name, List<MonitoredPackage> packages) {
- this(name, packages, /*mitigationCount=*/ 0);
- }
-
- ObserverInternal(String name, List<MonitoredPackage> packages, int mitigationCount) {
- this.name = name;
- updatePackagesLocked(packages);
- this.mMitigationCount = mitigationCount;
- }
-
- /**
- * Writes important {@link MonitoredPackage} details for this observer to file.
- * Does not persist any package failure thresholds.
- */
- @GuardedBy("sLock")
- public boolean writeLocked(XmlSerializer out) {
- try {
- out.startTag(null, TAG_OBSERVER);
- out.attribute(null, ATTR_NAME, name);
- if (Flags.recoverabilityDetection()) {
- out.attribute(null, ATTR_MITIGATION_COUNT, Integer.toString(mMitigationCount));
- }
- for (int i = 0; i < mPackages.size(); i++) {
- MonitoredPackage p = mPackages.valueAt(i);
- p.writeLocked(out);
- }
- out.endTag(null, TAG_OBSERVER);
- return true;
- } catch (IOException e) {
- Slog.w(TAG, "Cannot save observer", e);
- return false;
- }
- }
-
- public int getBootMitigationCount() {
- return mMitigationCount;
- }
-
- public void setBootMitigationCount(int mitigationCount) {
- mMitigationCount = mitigationCount;
- }
-
- @GuardedBy("sLock")
- public void updatePackagesLocked(List<MonitoredPackage> packages) {
- for (int pIndex = 0; pIndex < packages.size(); pIndex++) {
- MonitoredPackage p = packages.get(pIndex);
- MonitoredPackage existingPackage = getMonitoredPackage(p.getName());
- if (existingPackage != null) {
- existingPackage.updateHealthCheckDuration(p.mDurationMs);
- } else {
- putMonitoredPackage(p);
- }
- }
- }
-
- /**
- * Reduces the monitoring durations of all packages observed by this observer by
- * {@code elapsedMs}. If any duration is less than 0, the package is removed from
- * observation. If any health check duration is less than 0, the health check result
- * is evaluated.
- *
- * @return a {@link Set} of packages that were removed from the observer without explicit
- * health check passing, or an empty list if no package expired for which an explicit health
- * check was still pending
- */
- @GuardedBy("sLock")
- private Set<MonitoredPackage> prunePackagesLocked(long elapsedMs) {
- Set<MonitoredPackage> failedPackages = new ArraySet<>();
- Iterator<MonitoredPackage> it = mPackages.values().iterator();
- while (it.hasNext()) {
- MonitoredPackage p = it.next();
- int oldState = p.getHealthCheckStateLocked();
- int newState = p.handleElapsedTimeLocked(elapsedMs);
- if (oldState != HealthCheckState.FAILED
- && newState == HealthCheckState.FAILED) {
- Slog.i(TAG, "Package " + p.getName() + " failed health check");
- failedPackages.add(p);
- }
- if (p.isExpiredLocked()) {
- it.remove();
- }
- }
- return failedPackages;
- }
-
- /**
- * Increments failure counts of {@code packageName}.
- * @returns {@code true} if failure threshold is exceeded, {@code false} otherwise
- * @hide
- */
- @GuardedBy("sLock")
- public boolean notifyPackageFailureLocked(String packageName) {
- if (getMonitoredPackage(packageName) == null && registeredObserver.isPersistent()
- && registeredObserver.mayObservePackage(packageName)) {
- putMonitoredPackage(sPackageWatchdog.newMonitoredPackage(
- packageName, DEFAULT_OBSERVING_DURATION_MS, false));
- }
- MonitoredPackage p = getMonitoredPackage(packageName);
- if (p != null) {
- return p.onFailureLocked();
- }
- return false;
- }
-
- /**
- * Returns the map of packages monitored by this observer.
- *
- * @return a mapping of package names to {@link MonitoredPackage} objects.
- */
- @GuardedBy("sLock")
- public ArrayMap<String, MonitoredPackage> getMonitoredPackages() {
- return mPackages;
- }
-
- /**
- * Returns the {@link MonitoredPackage} associated with a given package name if the
- * package is being monitored by this observer.
- *
- * @param packageName: the name of the package.
- * @return the {@link MonitoredPackage} object associated with the package name if one
- * exists, {@code null} otherwise.
- */
- @GuardedBy("sLock")
- @Nullable
- public MonitoredPackage getMonitoredPackage(String packageName) {
- return mPackages.get(packageName);
- }
-
- /**
- * Associates a {@link MonitoredPackage} with the observer.
- *
- * @param p: the {@link MonitoredPackage} to store.
- */
- @GuardedBy("sLock")
- public void putMonitoredPackage(MonitoredPackage p) {
- mPackages.put(p.getName(), p);
- }
-
- /**
- * Returns one ObserverInternal from the {@code parser} and advances its state.
- *
- * <p>Note that this method is <b>not</b> thread safe. It should only be called from
- * #loadFromFile which in turn is only called on construction of the
- * singleton PackageWatchdog.
- **/
- public static ObserverInternal read(XmlPullParser parser, PackageWatchdog watchdog) {
- String observerName = null;
- int observerMitigationCount = 0;
- if (TAG_OBSERVER.equals(parser.getName())) {
- observerName = parser.getAttributeValue(null, ATTR_NAME);
- if (TextUtils.isEmpty(observerName)) {
- Slog.wtf(TAG, "Unable to read observer name");
- return null;
- }
- }
- List<MonitoredPackage> packages = new ArrayList<>();
- int innerDepth = parser.getDepth();
- try {
- if (Flags.recoverabilityDetection()) {
- try {
- observerMitigationCount = Integer.parseInt(
- parser.getAttributeValue(null, ATTR_MITIGATION_COUNT));
- } catch (Exception e) {
- Slog.i(
- TAG,
- "ObserverInternal mitigation count was not present.");
- }
- }
- while (XmlUtils.nextElementWithin(parser, innerDepth)) {
- if (TAG_PACKAGE.equals(parser.getName())) {
- try {
- MonitoredPackage pkg = watchdog.parseMonitoredPackage(parser);
- if (pkg != null) {
- packages.add(pkg);
- }
- } catch (NumberFormatException e) {
- Slog.wtf(TAG, "Skipping package for observer " + observerName, e);
- continue;
- }
- }
- }
- } catch (XmlPullParserException | IOException e) {
- Slog.wtf(TAG, "Unable to read observer " + observerName, e);
- return null;
- }
- if (packages.isEmpty()) {
- return null;
- }
- return new ObserverInternal(observerName, packages, observerMitigationCount);
- }
-
- /** Dumps information about this observer and the packages it watches. */
- public void dump(IndentingPrintWriter pw) {
- boolean isPersistent = registeredObserver != null && registeredObserver.isPersistent();
- pw.println("Persistent: " + isPersistent);
- for (String packageName : mPackages.keySet()) {
- MonitoredPackage p = getMonitoredPackage(packageName);
- pw.println(packageName + ": ");
- pw.increaseIndent();
- pw.println("# Failures: " + p.mFailureHistory.size());
- pw.println("Monitoring duration remaining: " + p.mDurationMs + "ms");
- pw.println("Explicit health check duration: " + p.mHealthCheckDurationMs + "ms");
- pw.println("Health check state: " + p.toString(p.mHealthCheckState));
- pw.decreaseIndent();
- }
- }
- }
-
- /** @hide */
- @Retention(SOURCE)
- @IntDef(value = {
- HealthCheckState.ACTIVE,
- HealthCheckState.INACTIVE,
- HealthCheckState.PASSED,
- HealthCheckState.FAILED})
- public @interface HealthCheckState {
- // The package has not passed health check but has requested a health check
- int ACTIVE = 0;
- // The package has not passed health check and has not requested a health check
- int INACTIVE = 1;
- // The package has passed health check
- int PASSED = 2;
- // The package has failed health check
- int FAILED = 3;
- }
-
- MonitoredPackage newMonitoredPackage(
- String name, long durationMs, boolean hasPassedHealthCheck) {
- return newMonitoredPackage(name, durationMs, Long.MAX_VALUE, hasPassedHealthCheck,
- new LongArrayQueue());
- }
-
- MonitoredPackage newMonitoredPackage(String name, long durationMs, long healthCheckDurationMs,
- boolean hasPassedHealthCheck, LongArrayQueue mitigationCalls) {
- return new MonitoredPackage(name, durationMs, healthCheckDurationMs,
- hasPassedHealthCheck, mitigationCalls);
- }
-
- MonitoredPackage parseMonitoredPackage(XmlPullParser parser)
- throws XmlPullParserException {
- String packageName = parser.getAttributeValue(null, ATTR_NAME);
- long duration = Long.parseLong(parser.getAttributeValue(null, ATTR_DURATION));
- long healthCheckDuration = Long.parseLong(parser.getAttributeValue(null,
- ATTR_EXPLICIT_HEALTH_CHECK_DURATION));
- boolean hasPassedHealthCheck = Boolean.parseBoolean(parser.getAttributeValue(null,
- ATTR_PASSED_HEALTH_CHECK));
- LongArrayQueue mitigationCalls = parseLongArrayQueue(
- parser.getAttributeValue(null, ATTR_MITIGATION_CALLS));
- return newMonitoredPackage(packageName,
- duration, healthCheckDuration, hasPassedHealthCheck, mitigationCalls);
- }
-
- /**
- * Represents a package and its health check state along with the time
- * it should be monitored for.
- *
- * <p> Note, the PackageWatchdog#sLock must always be held when reading or writing
- * instances of this class.
- */
- class MonitoredPackage {
- private final String mPackageName;
- // Times when package failures happen sorted in ascending order
- @GuardedBy("sLock")
- private final LongArrayQueue mFailureHistory = new LongArrayQueue();
- // Times when an observer was called to mitigate this package's failure. Sorted in
- // ascending order.
- @GuardedBy("sLock")
- private final LongArrayQueue mMitigationCalls;
- // One of STATE_[ACTIVE|INACTIVE|PASSED|FAILED]. Updated on construction and after
- // methods that could change the health check state: handleElapsedTimeLocked and
- // tryPassHealthCheckLocked
- private int mHealthCheckState = HealthCheckState.INACTIVE;
- // Whether an explicit health check has passed.
- // This value in addition with mHealthCheckDurationMs determines the health check state
- // of the package, see #getHealthCheckStateLocked
- @GuardedBy("sLock")
- private boolean mHasPassedHealthCheck;
- // System uptime duration to monitor package.
- @GuardedBy("sLock")
- private long mDurationMs;
- // System uptime duration to check the result of an explicit health check
- // Initially, MAX_VALUE until we get a value from the health check service
- // and request health checks.
- // This value in addition with mHasPassedHealthCheck determines the health check state
- // of the package, see #getHealthCheckStateLocked
- @GuardedBy("sLock")
- private long mHealthCheckDurationMs = Long.MAX_VALUE;
-
- MonitoredPackage(String packageName, long durationMs,
- long healthCheckDurationMs, boolean hasPassedHealthCheck,
- LongArrayQueue mitigationCalls) {
- mPackageName = packageName;
- mDurationMs = durationMs;
- mHealthCheckDurationMs = healthCheckDurationMs;
- mHasPassedHealthCheck = hasPassedHealthCheck;
- mMitigationCalls = mitigationCalls;
- updateHealthCheckStateLocked();
- }
-
- /** Writes the salient fields to disk using {@code out}.
- * @hide
- */
- @GuardedBy("sLock")
- public void writeLocked(XmlSerializer out) throws IOException {
- out.startTag(null, TAG_PACKAGE);
- out.attribute(null, ATTR_NAME, getName());
- out.attribute(null, ATTR_DURATION, Long.toString(mDurationMs));
- out.attribute(null, ATTR_EXPLICIT_HEALTH_CHECK_DURATION,
- Long.toString(mHealthCheckDurationMs));
- out.attribute(null, ATTR_PASSED_HEALTH_CHECK, Boolean.toString(mHasPassedHealthCheck));
- LongArrayQueue normalizedCalls = normalizeMitigationCalls();
- out.attribute(null, ATTR_MITIGATION_CALLS, longArrayQueueToString(normalizedCalls));
- out.endTag(null, TAG_PACKAGE);
- }
-
- /**
- * Increment package failures or resets failure count depending on the last package failure.
- *
- * @return {@code true} if failure count exceeds a threshold, {@code false} otherwise
- */
- @GuardedBy("sLock")
- public boolean onFailureLocked() {
- // Sliding window algorithm: find out if there exists a window containing failures >=
- // mTriggerFailureCount.
- final long now = mSystemClock.uptimeMillis();
- mFailureHistory.addLast(now);
- while (now - mFailureHistory.peekFirst() > mTriggerFailureDurationMs) {
- // Prune values falling out of the window
- mFailureHistory.removeFirst();
- }
- boolean failed = mFailureHistory.size() >= mTriggerFailureCount;
- if (failed) {
- mFailureHistory.clear();
- }
- return failed;
- }
-
- /**
- * Notes the timestamp of a mitigation call into the observer.
- */
- @GuardedBy("sLock")
- public void noteMitigationCallLocked() {
- mMitigationCalls.addLast(mSystemClock.uptimeMillis());
- }
-
- /**
- * Prunes any mitigation calls outside of the de-escalation window, and returns the
- * number of calls that are in the window afterwards.
- *
- * @return the number of mitigation calls made in the de-escalation window.
- */
- @GuardedBy("sLock")
- public int getMitigationCountLocked() {
- try {
- final long now = mSystemClock.uptimeMillis();
- while (now - mMitigationCalls.peekFirst() > DEFAULT_DEESCALATION_WINDOW_MS) {
- mMitigationCalls.removeFirst();
- }
- } catch (NoSuchElementException ignore) {
- }
-
- return mMitigationCalls.size();
- }
-
- /**
- * Before writing to disk, make the mitigation call timestamps relative to the current
- * system uptime. This is because they need to be relative to the uptime which will reset
- * at the next boot.
- *
- * @return a LongArrayQueue of the mitigation calls relative to the current system uptime.
- */
- @GuardedBy("sLock")
- public LongArrayQueue normalizeMitigationCalls() {
- LongArrayQueue normalized = new LongArrayQueue();
- final long now = mSystemClock.uptimeMillis();
- for (int i = 0; i < mMitigationCalls.size(); i++) {
- normalized.addLast(mMitigationCalls.get(i) - now);
- }
- return normalized;
- }
-
- /**
- * Sets the initial health check duration.
- *
- * @return the new health check state
- */
- @GuardedBy("sLock")
- public int setHealthCheckActiveLocked(long initialHealthCheckDurationMs) {
- if (initialHealthCheckDurationMs <= 0) {
- Slog.wtf(TAG, "Cannot set non-positive health check duration "
- + initialHealthCheckDurationMs + "ms for package " + getName()
- + ". Using total duration " + mDurationMs + "ms instead");
- initialHealthCheckDurationMs = mDurationMs;
- }
- if (mHealthCheckState == HealthCheckState.INACTIVE) {
- // Transitions to ACTIVE
- mHealthCheckDurationMs = initialHealthCheckDurationMs;
- }
- return updateHealthCheckStateLocked();
- }
-
- /**
- * Updates the monitoring durations of the package.
- *
- * @return the new health check state
- */
- @GuardedBy("sLock")
- public int handleElapsedTimeLocked(long elapsedMs) {
- if (elapsedMs <= 0) {
- Slog.w(TAG, "Cannot handle non-positive elapsed time for package " + getName());
- return mHealthCheckState;
- }
- // Transitions to FAILED if now <= 0 and health check not passed
- mDurationMs -= elapsedMs;
- if (mHealthCheckState == HealthCheckState.ACTIVE) {
- // We only update health check durations if we have #setHealthCheckActiveLocked
- // This ensures we don't leave the INACTIVE state for an unexpected elapsed time
- // Transitions to FAILED if now <= 0 and health check not passed
- mHealthCheckDurationMs -= elapsedMs;
- }
- return updateHealthCheckStateLocked();
- }
-
- /** Explicitly update the monitoring duration of the package. */
- @GuardedBy("sLock")
- public void updateHealthCheckDuration(long newDurationMs) {
- mDurationMs = newDurationMs;
- }
-
- /**
- * Marks the health check as passed and transitions to {@link HealthCheckState.PASSED}
- * if not yet {@link HealthCheckState.FAILED}.
- *
- * @return the new {@link HealthCheckState health check state}
- */
- @GuardedBy("sLock")
- @HealthCheckState
- public int tryPassHealthCheckLocked() {
- if (mHealthCheckState != HealthCheckState.FAILED) {
- // FAILED is a final state so only pass if we haven't failed
- // Transition to PASSED
- mHasPassedHealthCheck = true;
- }
- return updateHealthCheckStateLocked();
- }
-
- /** Returns the monitored package name. */
- private String getName() {
- return mPackageName;
- }
-
- /**
- * Returns the current {@link HealthCheckState health check state}.
- */
- @GuardedBy("sLock")
- @HealthCheckState
- public int getHealthCheckStateLocked() {
- return mHealthCheckState;
- }
-
- /**
- * Returns the shortest duration before the package should be scheduled for a prune.
- *
- * @return the duration or {@link Long#MAX_VALUE} if the package should not be scheduled
- */
- @GuardedBy("sLock")
- public long getShortestScheduleDurationMsLocked() {
- // Consider health check duration only if #isPendingHealthChecksLocked is true
- return Math.min(toPositive(mDurationMs),
- isPendingHealthChecksLocked()
- ? toPositive(mHealthCheckDurationMs) : Long.MAX_VALUE);
- }
-
- /**
- * Returns {@code true} if the total duration left to monitor the package is less than or
- * equal to 0 {@code false} otherwise.
- */
- @GuardedBy("sLock")
- public boolean isExpiredLocked() {
- return mDurationMs <= 0;
- }
-
- /**
- * Returns {@code true} if the package, {@link #getName} is expecting health check results
- * {@code false} otherwise.
- */
- @GuardedBy("sLock")
- public boolean isPendingHealthChecksLocked() {
- return mHealthCheckState == HealthCheckState.ACTIVE
- || mHealthCheckState == HealthCheckState.INACTIVE;
- }
-
- /**
- * Updates the health check state based on {@link #mHasPassedHealthCheck}
- * and {@link #mHealthCheckDurationMs}.
- *
- * @return the new {@link HealthCheckState health check state}
- */
- @GuardedBy("sLock")
- @HealthCheckState
- private int updateHealthCheckStateLocked() {
- int oldState = mHealthCheckState;
- if (mHasPassedHealthCheck) {
- // Set final state first to avoid ambiguity
- mHealthCheckState = HealthCheckState.PASSED;
- } else if (mHealthCheckDurationMs <= 0 || mDurationMs <= 0) {
- // Set final state first to avoid ambiguity
- mHealthCheckState = HealthCheckState.FAILED;
- } else if (mHealthCheckDurationMs == Long.MAX_VALUE) {
- mHealthCheckState = HealthCheckState.INACTIVE;
- } else {
- mHealthCheckState = HealthCheckState.ACTIVE;
- }
-
- if (oldState != mHealthCheckState) {
- Slog.i(TAG, "Updated health check state for package " + getName() + ": "
- + toString(oldState) + " -> " + toString(mHealthCheckState));
- }
- return mHealthCheckState;
- }
-
- /** Returns a {@link String} representation of the current health check state. */
- private String toString(@HealthCheckState int state) {
- switch (state) {
- case HealthCheckState.ACTIVE:
- return "ACTIVE";
- case HealthCheckState.INACTIVE:
- return "INACTIVE";
- case HealthCheckState.PASSED:
- return "PASSED";
- case HealthCheckState.FAILED:
- return "FAILED";
- default:
- return "UNKNOWN";
- }
- }
-
- /** Returns {@code value} if it is greater than 0 or {@link Long#MAX_VALUE} otherwise. */
- private long toPositive(long value) {
- return value > 0 ? value : Long.MAX_VALUE;
- }
-
- /** Compares the equality of this object with another {@link MonitoredPackage}. */
- @VisibleForTesting
- boolean isEqualTo(MonitoredPackage pkg) {
- return (getName().equals(pkg.getName()))
- && mDurationMs == pkg.mDurationMs
- && mHasPassedHealthCheck == pkg.mHasPassedHealthCheck
- && mHealthCheckDurationMs == pkg.mHealthCheckDurationMs
- && (mMitigationCalls.toString()).equals(pkg.mMitigationCalls.toString());
- }
- }
-
- @GuardedBy("sLock")
- @SuppressWarnings("GuardedBy")
- void saveAllObserversBootMitigationCountToMetadata(String filePath) {
- HashMap<String, Integer> bootMitigationCounts = new HashMap<>();
- for (int i = 0; i < mAllObservers.size(); i++) {
- final ObserverInternal observer = mAllObservers.valueAt(i);
- bootMitigationCounts.put(observer.name, observer.getBootMitigationCount());
- }
-
- FileOutputStream fileStream = null;
- ObjectOutputStream objectStream = null;
- try {
- fileStream = new FileOutputStream(new File(filePath));
- objectStream = new ObjectOutputStream(fileStream);
- objectStream.writeObject(bootMitigationCounts);
- objectStream.flush();
- } catch (Exception e) {
- Slog.i(TAG, "Could not save observers metadata to file: " + e);
- return;
- } finally {
- IoUtils.closeQuietly(objectStream);
- IoUtils.closeQuietly(fileStream);
- }
- }
-
- /**
- * Handles the thresholding logic for system server boots.
- */
- class BootThreshold {
-
- private final int mBootTriggerCount;
- private final long mTriggerWindow;
-
- BootThreshold(int bootTriggerCount, long triggerWindow) {
- this.mBootTriggerCount = bootTriggerCount;
- this.mTriggerWindow = triggerWindow;
- }
-
- public void reset() {
- setStart(0);
- setCount(0);
- }
-
- protected int getCount() {
- return CrashRecoveryProperties.rescueBootCount().orElse(0);
- }
-
- protected void setCount(int count) {
- CrashRecoveryProperties.rescueBootCount(count);
- }
-
- public long getStart() {
- return CrashRecoveryProperties.rescueBootStart().orElse(0L);
- }
-
- public int getMitigationCount() {
- return CrashRecoveryProperties.bootMitigationCount().orElse(0);
- }
-
- public void setStart(long start) {
- CrashRecoveryProperties.rescueBootStart(getStartTime(start));
- }
-
- public void setMitigationStart(long start) {
- CrashRecoveryProperties.bootMitigationStart(getStartTime(start));
- }
-
- public long getMitigationStart() {
- return CrashRecoveryProperties.bootMitigationStart().orElse(0L);
- }
-
- public void setMitigationCount(int count) {
- CrashRecoveryProperties.bootMitigationCount(count);
- }
-
- private static long constrain(long amount, long low, long high) {
- return amount < low ? low : (amount > high ? high : amount);
- }
-
- public long getStartTime(long start) {
- final long now = mSystemClock.uptimeMillis();
- return constrain(start, 0, now);
- }
-
- public void saveMitigationCountToMetadata() {
- try (BufferedWriter writer = new BufferedWriter(new FileWriter(METADATA_FILE))) {
- writer.write(String.valueOf(getMitigationCount()));
- } catch (Exception e) {
- Slog.e(TAG, "Could not save metadata to file: " + e);
- }
- }
-
- public void readMitigationCountFromMetadataIfNecessary() {
- File bootPropsFile = new File(METADATA_FILE);
- if (bootPropsFile.exists()) {
- try (BufferedReader reader = new BufferedReader(new FileReader(METADATA_FILE))) {
- String mitigationCount = reader.readLine();
- setMitigationCount(Integer.parseInt(mitigationCount));
- bootPropsFile.delete();
- } catch (Exception e) {
- Slog.i(TAG, "Could not read metadata file: " + e);
- }
- }
- }
-
-
- /** Increments the boot counter, and returns whether the device is bootlooping. */
- @GuardedBy("sLock")
- public boolean incrementAndTest() {
- if (Flags.recoverabilityDetection()) {
- readAllObserversBootMitigationCountIfNecessary(METADATA_FILE);
- } else {
- readMitigationCountFromMetadataIfNecessary();
- }
-
- final long now = mSystemClock.uptimeMillis();
- if (now - getStart() < 0) {
- Slog.e(TAG, "Window was less than zero. Resetting start to current time.");
- setStart(now);
- setMitigationStart(now);
- }
- if (now - getMitigationStart() > DEFAULT_DEESCALATION_WINDOW_MS) {
- setMitigationStart(now);
- if (Flags.recoverabilityDetection()) {
- resetAllObserversBootMitigationCount();
- } else {
- setMitigationCount(0);
- }
- }
- final long window = now - getStart();
- if (window >= mTriggerWindow) {
- setCount(1);
- setStart(now);
- return false;
- } else {
- int count = getCount() + 1;
- setCount(count);
- EventLog.writeEvent(LOG_TAG_RESCUE_NOTE, Process.ROOT_UID, count, window);
- if (Flags.recoverabilityDetection()) {
- // After a reboot (e.g. by WARM_REBOOT or mainline rollback) we apply
- // mitigations without waiting for DEFAULT_BOOT_LOOP_TRIGGER_COUNT.
- return (count >= mBootTriggerCount)
- || (performedMitigationsDuringWindow() && count > 1);
- }
- return count >= mBootTriggerCount;
- }
- }
-
- @GuardedBy("sLock")
- private boolean performedMitigationsDuringWindow() {
- for (ObserverInternal observerInternal: mAllObservers.values()) {
- if (observerInternal.getBootMitigationCount() > 0) {
- return true;
- }
- }
- return false;
- }
-
- @GuardedBy("sLock")
- private void resetAllObserversBootMitigationCount() {
- for (int i = 0; i < mAllObservers.size(); i++) {
- final ObserverInternal observer = mAllObservers.valueAt(i);
- observer.setBootMitigationCount(0);
- }
- saveAllObserversBootMitigationCountToMetadata(METADATA_FILE);
- }
-
- @GuardedBy("sLock")
- @SuppressWarnings("GuardedBy")
- void readAllObserversBootMitigationCountIfNecessary(String filePath) {
- File metadataFile = new File(filePath);
- if (metadataFile.exists()) {
- FileInputStream fileStream = null;
- ObjectInputStream objectStream = null;
- HashMap<String, Integer> bootMitigationCounts = null;
- try {
- fileStream = new FileInputStream(metadataFile);
- objectStream = new ObjectInputStream(fileStream);
- bootMitigationCounts =
- (HashMap<String, Integer>) objectStream.readObject();
- } catch (Exception e) {
- Slog.i(TAG, "Could not read observer metadata file: " + e);
- return;
- } finally {
- IoUtils.closeQuietly(objectStream);
- IoUtils.closeQuietly(fileStream);
- }
-
- if (bootMitigationCounts == null || bootMitigationCounts.isEmpty()) {
- Slog.i(TAG, "No observer in metadata file");
- return;
- }
- for (int i = 0; i < mAllObservers.size(); i++) {
- final ObserverInternal observer = mAllObservers.valueAt(i);
- if (bootMitigationCounts.containsKey(observer.name)) {
- observer.setBootMitigationCount(
- bootMitigationCounts.get(observer.name));
- }
- }
- }
- }
- }
-
- /**
- * Register broadcast receiver for shutdown.
- * We would save the observer state to persist across boots.
- *
- * @hide
- */
- public void registerShutdownBroadcastReceiver() {
- BroadcastReceiver shutdownEventReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- // Only write if intent is relevant to device reboot or shutdown.
- String intentAction = intent.getAction();
- if (ACTION_REBOOT.equals(intentAction)
- || ACTION_SHUTDOWN.equals(intentAction)) {
- writeNow();
- }
- }
- };
-
- // Setup receiver for device reboots or shutdowns.
- IntentFilter filter = new IntentFilter(ACTION_REBOOT);
- filter.addAction(ACTION_SHUTDOWN);
- mContext.registerReceiverForAllUsers(shutdownEventReceiver, filter, null,
- /* run on main thread */ null);
- }
-}
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java
deleted file mode 100644
index 846da194b3c3..000000000000
--- a/packages/CrashRecovery/services/module/java/com/android/server/RescueParty.java
+++ /dev/null
@@ -1,861 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server;
-
-import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SKIPPED;
-import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SUCCESS;
-import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.VersionedPackage;
-import android.crashrecovery.flags.Flags;
-import android.os.Build;
-import android.os.PowerManager;
-import android.os.RecoverySystem;
-import android.os.SystemClock;
-import android.os.SystemProperties;
-import android.provider.Settings;
-import android.sysprop.CrashRecoveryProperties;
-import android.text.TextUtils;
-import android.util.EventLog;
-import android.util.FileUtils;
-import android.util.Log;
-import android.util.Slog;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.PackageWatchdog.FailureReasons;
-import com.android.server.PackageWatchdog.PackageHealthObserver;
-import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
-import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog;
-
-import java.io.File;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Utilities to help rescue the system from crash loops. Callers are expected to
- * report boot events and persistent app crashes, and if they happen frequently
- * enough this class will slowly escalate through several rescue operations
- * before finally rebooting and prompting the user if they want to wipe data as
- * a last resort.
- *
- * @hide
- */
-public class RescueParty {
- @VisibleForTesting
- static final String PROP_ENABLE_RESCUE = "persist.sys.enable_rescue";
- @VisibleForTesting
- static final int LEVEL_NONE = 0;
- @VisibleForTesting
- static final int LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 1;
- @VisibleForTesting
- static final int LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 2;
- @VisibleForTesting
- static final int LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 3;
- @VisibleForTesting
- static final int LEVEL_WARM_REBOOT = 4;
- @VisibleForTesting
- static final int LEVEL_FACTORY_RESET = 5;
- @VisibleForTesting
- static final int RESCUE_LEVEL_NONE = 0;
- @VisibleForTesting
- static final int RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET = 1;
- @VisibleForTesting
- static final int RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET = 2;
- @VisibleForTesting
- static final int RESCUE_LEVEL_WARM_REBOOT = 3;
- @VisibleForTesting
- static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 4;
- @VisibleForTesting
- static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 5;
- @VisibleForTesting
- static final int RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 6;
- @VisibleForTesting
- static final int RESCUE_LEVEL_FACTORY_RESET = 7;
-
- @IntDef(prefix = { "RESCUE_LEVEL_" }, value = {
- RESCUE_LEVEL_NONE,
- RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET,
- RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET,
- RESCUE_LEVEL_WARM_REBOOT,
- RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS,
- RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES,
- RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS,
- RESCUE_LEVEL_FACTORY_RESET
- })
- @Retention(RetentionPolicy.SOURCE)
- @interface RescueLevels {}
-
- @VisibleForTesting
- static final String RESCUE_NON_REBOOT_LEVEL_LIMIT = "persist.sys.rescue_non_reboot_level_limit";
- @VisibleForTesting
- static final int DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT = RESCUE_LEVEL_WARM_REBOOT - 1;
- @VisibleForTesting
- static final String TAG = "RescueParty";
- @VisibleForTesting
- static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2);
- @VisibleForTesting
- static final int DEVICE_CONFIG_RESET_MODE = Settings.RESET_MODE_TRUSTED_DEFAULTS;
- // The DeviceConfig namespace containing all RescueParty switches.
- @VisibleForTesting
- static final String NAMESPACE_CONFIGURATION = "configuration";
- @VisibleForTesting
- static final String NAMESPACE_TO_PACKAGE_MAPPING_FLAG =
- "namespace_to_package_mapping";
- @VisibleForTesting
- static final long DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN = 1440;
-
- private static final String NAME = "rescue-party-observer";
-
- private static final String PROP_DISABLE_RESCUE = "persist.sys.disable_rescue";
- private static final String PROP_VIRTUAL_DEVICE = "ro.hardware.virtual_device";
- private static final String PROP_DEVICE_CONFIG_DISABLE_FLAG =
- "persist.device_config.configuration.disable_rescue_party";
- private static final String PROP_DISABLE_FACTORY_RESET_FLAG =
- "persist.device_config.configuration.disable_rescue_party_factory_reset";
- private static final String PROP_THROTTLE_DURATION_MIN_FLAG =
- "persist.device_config.configuration.rescue_party_throttle_duration_min";
-
- private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT
- | ApplicationInfo.FLAG_SYSTEM;
-
- /**
- * EventLog tags used when logging into the event log. Note the values must be sync with
- * frameworks/base/services/core/java/com/android/server/EventLogTags.logtags to get correct
- * name translation.
- */
- private static final int LOG_TAG_RESCUE_SUCCESS = 2902;
- private static final int LOG_TAG_RESCUE_FAILURE = 2903;
-
- /** Register the Rescue Party observer as a Package Watchdog health observer */
- public static void registerHealthObserver(Context context) {
- PackageWatchdog.getInstance(context).registerHealthObserver(
- context.getMainExecutor(), RescuePartyObserver.getInstance(context));
- }
-
- private static boolean isDisabled() {
- // Check if we're explicitly enabled for testing
- if (SystemProperties.getBoolean(PROP_ENABLE_RESCUE, false)) {
- return false;
- }
-
- // We're disabled if the DeviceConfig disable flag is set to true.
- // This is in case that an emergency rollback of the feature is needed.
- if (SystemProperties.getBoolean(PROP_DEVICE_CONFIG_DISABLE_FLAG, false)) {
- Slog.v(TAG, "Disabled because of DeviceConfig flag");
- return true;
- }
-
- // We're disabled on all engineering devices
- if (Build.TYPE.equals("eng")) {
- Slog.v(TAG, "Disabled because of eng build");
- return true;
- }
-
- // We're disabled on userdebug devices connected over USB, since that's
- // a decent signal that someone is actively trying to debug the device,
- // or that it's in a lab environment.
- if (Build.TYPE.equals("userdebug") && isUsbActive()) {
- Slog.v(TAG, "Disabled because of active USB connection");
- return true;
- }
-
- // One last-ditch check
- if (SystemProperties.getBoolean(PROP_DISABLE_RESCUE, false)) {
- Slog.v(TAG, "Disabled because of manual property");
- return true;
- }
-
- return false;
- }
-
- /**
- * Check if we're currently attempting to reboot for a factory reset. This method must
- * return true if RescueParty tries to reboot early during a boot loop, since the device
- * will not be fully booted at this time.
- */
- public static boolean isRecoveryTriggeredReboot() {
- return isFactoryResetPropertySet() || isRebootPropertySet();
- }
-
- static boolean isFactoryResetPropertySet() {
- return CrashRecoveryProperties.attemptingFactoryReset().orElse(false);
- }
-
- static boolean isRebootPropertySet() {
- return CrashRecoveryProperties.attemptingReboot().orElse(false);
- }
-
- protected static long getLastFactoryResetTimeMs() {
- return CrashRecoveryProperties.lastFactoryResetTimeMs().orElse(0L);
- }
-
- protected static int getMaxRescueLevelAttempted() {
- return CrashRecoveryProperties.maxRescueLevelAttempted().orElse(LEVEL_NONE);
- }
-
- protected static void setFactoryResetProperty(boolean value) {
- CrashRecoveryProperties.attemptingFactoryReset(value);
- }
- protected static void setRebootProperty(boolean value) {
- CrashRecoveryProperties.attemptingReboot(value);
- }
-
- protected static void setLastFactoryResetTimeMs(long value) {
- CrashRecoveryProperties.lastFactoryResetTimeMs(value);
- }
-
- protected static void setMaxRescueLevelAttempted(int level) {
- CrashRecoveryProperties.maxRescueLevelAttempted(level);
- }
-
- @VisibleForTesting
- static long getElapsedRealtime() {
- return SystemClock.elapsedRealtime();
- }
-
- private static int getMaxRescueLevel(boolean mayPerformReboot) {
- if (Flags.recoverabilityDetection()) {
- if (!mayPerformReboot
- || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
- return SystemProperties.getInt(RESCUE_NON_REBOOT_LEVEL_LIMIT,
- DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT);
- }
- return RESCUE_LEVEL_FACTORY_RESET;
- } else {
- if (!mayPerformReboot
- || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
- return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS;
- }
- return LEVEL_FACTORY_RESET;
- }
- }
-
- private static int getMaxRescueLevel() {
- if (!SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) {
- return Level.factoryReset();
- }
- return Level.reboot();
- }
-
- /**
- * Get the rescue level to perform if this is the n-th attempt at mitigating failure.
- *
- * @param mitigationCount: the mitigation attempt number (1 = first attempt etc.)
- * @param mayPerformReboot: whether or not a reboot and factory reset may be performed
- * for the given failure.
- * @return the rescue level for the n-th mitigation attempt.
- */
- private static int getRescueLevel(int mitigationCount, boolean mayPerformReboot) {
- if (!Flags.deprecateFlagsAndSettingsResets()) {
- if (mitigationCount == 1) {
- return LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS;
- } else if (mitigationCount == 2) {
- return LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES;
- } else if (mitigationCount == 3) {
- return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS;
- } else if (mitigationCount == 4) {
- return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_WARM_REBOOT);
- } else if (mitigationCount >= 5) {
- return Math.min(getMaxRescueLevel(mayPerformReboot), LEVEL_FACTORY_RESET);
- } else {
- Slog.w(TAG, "Expected positive mitigation count, was " + mitigationCount);
- return LEVEL_NONE;
- }
- } else {
- if (mitigationCount == 1) {
- return Level.reboot();
- } else if (mitigationCount >= 2) {
- return Math.min(getMaxRescueLevel(), Level.factoryReset());
- } else {
- Slog.w(TAG, "Expected positive mitigation count, was " + mitigationCount);
- return LEVEL_NONE;
- }
- }
- }
-
- /**
- * Get the rescue level to perform if this is the n-th attempt at mitigating failure.
- * When failedPackage is null then 1st and 2nd mitigation counts are redundant (scoped and
- * all device config reset). Behaves as if one mitigation attempt was already done.
- *
- * @param mitigationCount the mitigation attempt number (1 = first attempt etc.).
- * @param mayPerformReboot whether or not a reboot and factory reset may be performed
- * for the given failure.
- * @param failedPackage in case of bootloop this is null.
- * @return the rescue level for the n-th mitigation attempt.
- */
- private static @RescueLevels int getRescueLevel(int mitigationCount, boolean mayPerformReboot,
- @Nullable VersionedPackage failedPackage) {
- // Skipping RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET since it's not defined without a failed
- // package.
- if (failedPackage == null && mitigationCount > 0) {
- mitigationCount += 1;
- }
- if (mitigationCount == 1) {
- return RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET;
- } else if (mitigationCount == 2) {
- return RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET;
- } else if (mitigationCount == 3) {
- return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_WARM_REBOOT);
- } else if (mitigationCount == 4) {
- return Math.min(getMaxRescueLevel(mayPerformReboot),
- RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS);
- } else if (mitigationCount == 5) {
- return Math.min(getMaxRescueLevel(mayPerformReboot),
- RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES);
- } else if (mitigationCount == 6) {
- return Math.min(getMaxRescueLevel(mayPerformReboot),
- RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS);
- } else if (mitigationCount >= 7) {
- return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_FACTORY_RESET);
- } else {
- return RESCUE_LEVEL_NONE;
- }
- }
-
- /**
- * Get the rescue level to perform if this is the n-th attempt at mitigating failure.
- *
- * @param mitigationCount the mitigation attempt number (1 = first attempt etc.).
- * @return the rescue level for the n-th mitigation attempt.
- */
- private static @RescueLevels int getRescueLevel(int mitigationCount) {
- if (mitigationCount == 1) {
- return Level.reboot();
- } else if (mitigationCount >= 2) {
- return Math.min(getMaxRescueLevel(), Level.factoryReset());
- } else {
- return Level.none();
- }
- }
-
- private static void executeRescueLevel(Context context, @Nullable String failedPackage,
- int level) {
- Slog.w(TAG, "Attempting rescue level " + levelToString(level));
- try {
- executeRescueLevelInternal(context, level, failedPackage);
- EventLog.writeEvent(LOG_TAG_RESCUE_SUCCESS, level);
- String successMsg = "Finished rescue level " + levelToString(level);
- if (!TextUtils.isEmpty(failedPackage)) {
- successMsg += " for package " + failedPackage;
- }
- logCrashRecoveryEvent(Log.DEBUG, successMsg);
- } catch (Throwable t) {
- logRescueException(level, failedPackage, t);
- }
- }
-
- private static void executeRescueLevelInternal(Context context, int level, @Nullable
- String failedPackage) throws Exception {
- if (Flags.recoverabilityDetection()) {
- executeRescueLevelInternalNew(context, level, failedPackage);
- } else {
- executeRescueLevelInternalOld(context, level, failedPackage);
- }
- }
-
- private static void executeRescueLevelInternalOld(Context context, int level, @Nullable
- String failedPackage) throws Exception {
- CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED,
- level, levelToString(level));
- // Try our best to reset all settings possible, and once finished
- // rethrow any exception that we encountered
- Exception res = null;
- switch (level) {
- case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
- break;
- case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
- break;
- case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
- break;
- case LEVEL_WARM_REBOOT:
- executeWarmReboot(context, level, failedPackage);
- break;
- case LEVEL_FACTORY_RESET:
- // Before the completion of Reboot, if any crash happens then PackageWatchdog
- // escalates to next level i.e. factory reset, as they happen in separate threads.
- // Adding a check to prevent factory reset to execute before above reboot completes.
- // Note: this reboot property is not persistent resets after reboot is completed.
- if (isRebootPropertySet()) {
- return;
- }
- executeFactoryReset(context, level, failedPackage);
- break;
- }
-
- if (res != null) {
- throw res;
- }
- }
-
- private static void executeRescueLevelInternalNew(Context context, @RescueLevels int level,
- @Nullable String failedPackage) throws Exception {
- CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED,
- level, levelToString(level));
- switch (level) {
- case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
- break;
- case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
- break;
- case RESCUE_LEVEL_WARM_REBOOT:
- executeWarmReboot(context, level, failedPackage);
- break;
- case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
- // do nothing
- break;
- case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
- // do nothing
- break;
- case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
- // do nothing
- break;
- case RESCUE_LEVEL_FACTORY_RESET:
- // Before the completion of Reboot, if any crash happens then PackageWatchdog
- // escalates to next level i.e. factory reset, as they happen in separate threads.
- // Adding a check to prevent factory reset to execute before above reboot completes.
- // Note: this reboot property is not persistent resets after reboot is completed.
- if (isRebootPropertySet()) {
- return;
- }
- executeFactoryReset(context, level, failedPackage);
- break;
- }
- }
-
- private static void executeWarmReboot(Context context, int level,
- @Nullable String failedPackage) {
- if (Flags.deprecateFlagsAndSettingsResets()) {
- if (shouldThrottleReboot()) {
- return;
- }
- }
-
- // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog
- // when device shutting down.
- setRebootProperty(true);
-
- if (Flags.synchronousRebootInRescueParty()) {
- try {
- PowerManager pm = context.getSystemService(PowerManager.class);
- if (pm != null) {
- pm.reboot(TAG);
- }
- } catch (Throwable t) {
- logRescueException(level, failedPackage, t);
- }
- } else {
- Runnable runnable = () -> {
- try {
- PowerManager pm = context.getSystemService(PowerManager.class);
- if (pm != null) {
- pm.reboot(TAG);
- }
- } catch (Throwable t) {
- logRescueException(level, failedPackage, t);
- }
- };
- Thread thread = new Thread(runnable);
- thread.start();
- }
- }
-
- private static void executeFactoryReset(Context context, int level,
- @Nullable String failedPackage) {
- if (Flags.deprecateFlagsAndSettingsResets()) {
- if (shouldThrottleReboot()) {
- return;
- }
- }
- setFactoryResetProperty(true);
- long now = System.currentTimeMillis();
- setLastFactoryResetTimeMs(now);
-
- if (Flags.synchronousRebootInRescueParty()) {
- try {
- RecoverySystem.rebootPromptAndWipeUserData(context, TAG + "," + failedPackage);
- } catch (Throwable t) {
- logRescueException(level, failedPackage, t);
- }
- } else {
- Runnable runnable = new Runnable() {
- @Override
- public void run() {
- try {
- RecoverySystem.rebootPromptAndWipeUserData(context,
- TAG + "," + failedPackage);
- } catch (Throwable t) {
- logRescueException(level, failedPackage, t);
- }
- }
- };
- Thread thread = new Thread(runnable);
- thread.start();
- }
- }
-
-
- private static String getCompleteMessage(Throwable t) {
- final StringBuilder builder = new StringBuilder();
- builder.append(t.getMessage());
- while ((t = t.getCause()) != null) {
- builder.append(": ").append(t.getMessage());
- }
- return builder.toString();
- }
-
- private static void logRescueException(int level, @Nullable String failedPackageName,
- Throwable t) {
- final String msg = getCompleteMessage(t);
- EventLog.writeEvent(LOG_TAG_RESCUE_FAILURE, level, msg);
- String failureMsg = "Failed rescue level " + levelToString(level);
- if (!TextUtils.isEmpty(failedPackageName)) {
- failureMsg += " for package " + failedPackageName;
- }
- logCrashRecoveryEvent(Log.ERROR, failureMsg + ": " + msg);
- }
-
- private static int mapRescueLevelToUserImpact(int rescueLevel) {
- if (Flags.recoverabilityDetection()) {
- switch (rescueLevel) {
- case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10;
- case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_40;
- case RESCUE_LEVEL_WARM_REBOOT:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
- case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_71;
- case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_75;
- case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_80;
- case RESCUE_LEVEL_FACTORY_RESET:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100;
- default:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
- }
- } else {
- switch (rescueLevel) {
- case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
- case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10;
- case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
- case LEVEL_WARM_REBOOT:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50;
- case LEVEL_FACTORY_RESET:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100;
- default:
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
- }
- }
- }
-
- /**
- * Handle mitigation action for package failures. This observer will be register to Package
- * Watchdog and will receive calls about package failures. This observer is persistent so it
- * may choose to mitigate failures for packages it has not explicitly asked to observe.
- */
- public static class RescuePartyObserver implements PackageHealthObserver {
-
- private final Context mContext;
- private final Map<String, Set<String>> mCallingPackageNamespaceSetMap = new HashMap<>();
- private final Map<String, Set<String>> mNamespaceCallingPackageSetMap = new HashMap<>();
-
- @GuardedBy("RescuePartyObserver.class")
- static RescuePartyObserver sRescuePartyObserver;
-
- private RescuePartyObserver(Context context) {
- mContext = context;
- }
-
- /** Creates or gets singleton instance of RescueParty. */
- public static RescuePartyObserver getInstance(Context context) {
- synchronized (RescuePartyObserver.class) {
- if (sRescuePartyObserver == null) {
- sRescuePartyObserver = new RescuePartyObserver(context);
- }
- return sRescuePartyObserver;
- }
- }
-
- /** Gets singleton instance. It returns null if the instance is not created yet.*/
- @Nullable
- public static RescuePartyObserver getInstanceIfCreated() {
- synchronized (RescuePartyObserver.class) {
- return sRescuePartyObserver;
- }
- }
-
- @VisibleForTesting
- static void reset() {
- synchronized (RescuePartyObserver.class) {
- sRescuePartyObserver = null;
- }
- }
-
- @Override
- public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage,
- @FailureReasons int failureReason, int mitigationCount) {
- int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
- if (!isDisabled() && (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH
- || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING)) {
- if (Flags.recoverabilityDetection()) {
- if (!Flags.deprecateFlagsAndSettingsResets()) {
- impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
- mayPerformReboot(failedPackage), failedPackage));
- } else {
- impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount));
- }
- } else {
- impact = mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
- mayPerformReboot(failedPackage)));
- }
- }
-
- Slog.i(TAG, "Checking available remediations for health check failure."
- + " failedPackage: "
- + (failedPackage == null ? null : failedPackage.getPackageName())
- + " failureReason: " + failureReason
- + " available impact: " + impact);
- return impact;
- }
-
- @Override
- public int onExecuteHealthCheckMitigation(@Nullable VersionedPackage failedPackage,
- @FailureReasons int failureReason, int mitigationCount) {
- if (isDisabled()) {
- return MITIGATION_RESULT_SKIPPED;
- }
- Slog.i(TAG, "Executing remediation."
- + " failedPackage: "
- + (failedPackage == null ? null : failedPackage.getPackageName())
- + " failureReason: " + failureReason
- + " mitigationCount: " + mitigationCount);
- if (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH
- || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING) {
- final int level;
- if (Flags.recoverabilityDetection()) {
- if (!Flags.deprecateFlagsAndSettingsResets()) {
- level = getRescueLevel(mitigationCount, mayPerformReboot(failedPackage),
- failedPackage);
- } else {
- level = getRescueLevel(mitigationCount);
- }
- } else {
- level = getRescueLevel(mitigationCount, mayPerformReboot(failedPackage));
- }
- executeRescueLevel(mContext,
- failedPackage == null ? null : failedPackage.getPackageName(), level);
- return MITIGATION_RESULT_SUCCESS;
- } else {
- return MITIGATION_RESULT_SKIPPED;
- }
- }
-
- @Override
- public boolean isPersistent() {
- return true;
- }
-
- @Override
- public boolean mayObservePackage(String packageName) {
- PackageManager pm = mContext.getPackageManager();
- try {
- // A package is a module if this is non-null
- if (pm.getModuleInfo(packageName, 0) != null) {
- return true;
- }
- } catch (PackageManager.NameNotFoundException | IllegalStateException ignore) {
- }
-
- return isPersistentSystemApp(packageName);
- }
-
- @Override
- public int onBootLoop(int mitigationCount) {
- if (isDisabled()) {
- return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
- }
- if (Flags.recoverabilityDetection()) {
- if (!Flags.deprecateFlagsAndSettingsResets()) {
- return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount,
- true, /*failedPackage=*/ null));
- } else {
- return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount));
- }
- } else {
- return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true));
- }
- }
-
- @Override
- public int onExecuteBootLoopMitigation(int mitigationCount) {
- if (isDisabled()) {
- return MITIGATION_RESULT_SKIPPED;
- }
- boolean mayPerformReboot = !shouldThrottleReboot();
- final int level;
- if (Flags.recoverabilityDetection()) {
- if (!Flags.deprecateFlagsAndSettingsResets()) {
- level = getRescueLevel(mitigationCount, mayPerformReboot,
- /*failedPackage=*/ null);
- } else {
- level = getRescueLevel(mitigationCount);
- }
- } else {
- level = getRescueLevel(mitigationCount, mayPerformReboot);
- }
- executeRescueLevel(mContext, /*failedPackage=*/ null, level);
- return MITIGATION_RESULT_SUCCESS;
- }
-
- @Override
- public String getUniqueIdentifier() {
- return NAME;
- }
-
- /**
- * Returns {@code true} if the failing package is non-null and performing a reboot or
- * prompting a factory reset is an acceptable mitigation strategy for the package's
- * failure, {@code false} otherwise.
- */
- private boolean mayPerformReboot(@Nullable VersionedPackage failingPackage) {
- if (failingPackage == null) {
- return false;
- }
- if (shouldThrottleReboot()) {
- return false;
- }
-
- return isPersistentSystemApp(failingPackage.getPackageName());
- }
-
- private boolean isPersistentSystemApp(@NonNull String packageName) {
- PackageManager pm = mContext.getPackageManager();
- try {
- ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
- return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK;
- } catch (PackageManager.NameNotFoundException e) {
- return false;
- }
- }
-
- private synchronized Set<String> getCallingPackagesSet(String namespace) {
- return mNamespaceCallingPackageSetMap.get(namespace);
- }
- }
-
- /**
- * Returns {@code true} if Rescue Party is allowed to attempt a reboot or factory reset.
- * Will return {@code false} if a factory reset was already offered recently.
- */
- private static boolean shouldThrottleReboot() {
- Long lastResetTime = getLastFactoryResetTimeMs();
- long now = System.currentTimeMillis();
- long throttleDurationMin = SystemProperties.getLong(PROP_THROTTLE_DURATION_MIN_FLAG,
- DEFAULT_FACTORY_RESET_THROTTLE_DURATION_MIN);
- return now < lastResetTime + TimeUnit.MINUTES.toMillis(throttleDurationMin);
- }
-
- /**
- * Hacky test to check if the device has an active USB connection, which is
- * a good proxy for someone doing local development work.
- */
- private static boolean isUsbActive() {
- if (SystemProperties.getBoolean(PROP_VIRTUAL_DEVICE, false)) {
- Slog.v(TAG, "Assuming virtual device is connected over USB");
- return true;
- }
- try {
- final String state = FileUtils
- .readTextFile(new File("/sys/class/android_usb/android0/state"), 128, "");
- return "CONFIGURED".equals(state.trim());
- } catch (Throwable t) {
- Slog.w(TAG, "Failed to determine if device was on USB", t);
- return false;
- }
- }
-
- private static class Level {
- static int none() {
- return Flags.recoverabilityDetection() ? RESCUE_LEVEL_NONE : LEVEL_NONE;
- }
-
- static int reboot() {
- return Flags.recoverabilityDetection() ? RESCUE_LEVEL_WARM_REBOOT : LEVEL_WARM_REBOOT;
- }
-
- static int factoryReset() {
- return Flags.recoverabilityDetection()
- ? RESCUE_LEVEL_FACTORY_RESET
- : LEVEL_FACTORY_RESET;
- }
- }
-
- private static String levelToString(int level) {
- if (Flags.recoverabilityDetection()) {
- switch (level) {
- case RESCUE_LEVEL_NONE:
- return "NONE";
- case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET:
- return "SCOPED_DEVICE_CONFIG_RESET";
- case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET:
- return "ALL_DEVICE_CONFIG_RESET";
- case RESCUE_LEVEL_WARM_REBOOT:
- return "WARM_REBOOT";
- case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
- return "RESET_SETTINGS_UNTRUSTED_DEFAULTS";
- case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
- return "RESET_SETTINGS_UNTRUSTED_CHANGES";
- case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
- return "RESET_SETTINGS_TRUSTED_DEFAULTS";
- case RESCUE_LEVEL_FACTORY_RESET:
- return "FACTORY_RESET";
- default:
- return Integer.toString(level);
- }
- } else {
- switch (level) {
- case LEVEL_NONE:
- return "NONE";
- case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS:
- return "RESET_SETTINGS_UNTRUSTED_DEFAULTS";
- case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES:
- return "RESET_SETTINGS_UNTRUSTED_CHANGES";
- case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS:
- return "RESET_SETTINGS_TRUSTED_DEFAULTS";
- case LEVEL_WARM_REBOOT:
- return "WARM_REBOOT";
- case LEVEL_FACTORY_RESET:
- return "FACTORY_RESET";
- default:
- return Integer.toString(level);
- }
- }
- }
-}
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java
deleted file mode 100644
index 8a81aaa1e636..000000000000
--- a/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryModule.java
+++ /dev/null
@@ -1,58 +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.server.crashrecovery;
-
-import android.content.Context;
-
-import com.android.server.PackageWatchdog;
-import com.android.server.RescueParty;
-import com.android.server.SystemService;
-
-
-/** This class encapsulate the lifecycle methods of CrashRecovery module.
- *
- * @hide
- */
-public class CrashRecoveryModule {
- private static final String TAG = "CrashRecoveryModule";
-
- /** Lifecycle definition for CrashRecovery module. */
- public static class Lifecycle extends SystemService {
- private Context mSystemContext;
- private PackageWatchdog mPackageWatchdog;
-
- public Lifecycle(Context context) {
- super(context);
- mSystemContext = context;
- mPackageWatchdog = PackageWatchdog.getInstance(context);
- }
-
- @Override
- public void onStart() {
- RescueParty.registerHealthObserver(mSystemContext);
- mPackageWatchdog.registerShutdownBroadcastReceiver();
- mPackageWatchdog.noteBoot();
- }
-
- @Override
- public void onBootPhase(int phase) {
- if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
- mPackageWatchdog.onPackagesReady();
- }
- }
- }
-}
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java b/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
deleted file mode 100644
index 2e2a93776f9d..000000000000
--- a/packages/CrashRecovery/services/module/java/com/android/server/crashrecovery/CrashRecoveryUtils.java
+++ /dev/null
@@ -1,85 +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.server.crashrecovery;
-
-import android.os.Environment;
-import android.util.IndentingPrintWriter;
-import android.util.Log;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-
-/**
- * Class containing helper methods for the CrashRecoveryModule.
- *
- * @hide
- */
-public class CrashRecoveryUtils {
- private static final String TAG = "CrashRecoveryUtils";
- private static final long MAX_CRITICAL_INFO_DUMP_SIZE = 1000 * 1000; // ~1MB
- private static final Object sFileLock = new Object();
-
- /** Persist recovery related events in crashrecovery events file.**/
- public static void logCrashRecoveryEvent(int priority, String msg) {
- Log.println(priority, TAG, msg);
- try {
- File fname = getCrashRecoveryEventsFile();
- synchronized (sFileLock) {
- FileOutputStream out = new FileOutputStream(fname, true);
- PrintWriter pw = new PrintWriter(out);
- String dateString = LocalDateTime.now(ZoneId.systemDefault()).toString();
- pw.println(dateString + ": " + msg);
- pw.close();
- }
- } catch (IOException e) {
- Log.e(TAG, "Unable to log CrashRecoveryEvents " + e.getMessage());
- }
- }
-
- /** Dump recovery related events from crashrecovery events file.**/
- public static void dumpCrashRecoveryEvents(IndentingPrintWriter pw) {
- pw.println("CrashRecovery Events: ");
- pw.increaseIndent();
- final File file = getCrashRecoveryEventsFile();
- final long skipSize = file.length() - MAX_CRITICAL_INFO_DUMP_SIZE;
- synchronized (sFileLock) {
- try (BufferedReader in = new BufferedReader(new FileReader(file))) {
- if (skipSize > 0) {
- in.skip(skipSize);
- }
- String line;
- while ((line = in.readLine()) != null) {
- pw.println(line);
- }
- } catch (IOException e) {
- Log.e(TAG, "Unable to dump CrashRecoveryEvents " + e.getMessage());
- }
- }
- pw.decreaseIndent();
- }
-
- private static File getCrashRecoveryEventsFile() {
- File systemDir = new File(Environment.getDataDirectory(), "system");
- return new File(systemDir, "crashrecovery-events.txt");
- }
-}
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java
deleted file mode 100644
index 4978df491c62..000000000000
--- a/packages/CrashRecovery/services/module/java/com/android/server/rollback/RollbackPackageHealthObserver.java
+++ /dev/null
@@ -1,785 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.rollback;
-
-import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SKIPPED;
-import static com.android.server.PackageWatchdog.MITIGATION_RESULT_SUCCESS;
-import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent;
-
-import android.annotation.AnyThread;
-import android.annotation.FlaggedApi;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.SuppressLint;
-import android.annotation.SystemApi;
-import android.annotation.WorkerThread;
-import android.app.PendingIntent;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.VersionedPackage;
-import android.content.rollback.PackageRollbackInfo;
-import android.content.rollback.RollbackInfo;
-import android.content.rollback.RollbackManager;
-import android.crashrecovery.flags.Flags;
-import android.os.Environment;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.PowerManager;
-import android.os.SystemProperties;
-import android.sysprop.CrashRecoveryProperties;
-import android.util.ArraySet;
-import android.util.FileUtils;
-import android.util.Log;
-import android.util.Slog;
-import android.util.SparseArray;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.Preconditions;
-import com.android.server.PackageWatchdog;
-import com.android.server.PackageWatchdog.FailureReasons;
-import com.android.server.PackageWatchdog.PackageHealthObserver;
-import com.android.server.PackageWatchdog.PackageHealthObserverImpact;
-import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.FileReader;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Set;
-import java.util.function.Consumer;
-
-/**
- * {@link PackageHealthObserver} for {@link RollbackManagerService}.
- * This class monitors crashes and triggers RollbackManager rollback accordingly.
- * It also monitors native crashes for some short while after boot.
- *
- * @hide
- */
-@FlaggedApi(Flags.FLAG_ENABLE_CRASHRECOVERY)
-@SuppressLint({"CallbackName"})
-@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
-public final class RollbackPackageHealthObserver implements PackageHealthObserver {
- private static final String TAG = "RollbackPackageHealthObserver";
- private static final String NAME = "rollback-observer";
- private static final String CLASS_NAME = RollbackPackageHealthObserver.class.getName();
-
- private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT
- | ApplicationInfo.FLAG_SYSTEM;
-
- private static final String PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG =
- "persist.device_config.configuration.disable_high_impact_rollback";
-
- private final Context mContext;
- private final Handler mHandler;
- private final File mLastStagedRollbackIdsFile;
- private final File mTwoPhaseRollbackEnabledFile;
- // Staged rollback ids that have been committed but their session is not yet ready
- private final Set<Integer> mPendingStagedRollbackIds = new ArraySet<>();
- // True if needing to roll back only rebootless apexes when native crash happens
- private boolean mTwoPhaseRollbackEnabled;
-
- @VisibleForTesting
- public RollbackPackageHealthObserver(@NonNull Context context) {
- mContext = context;
- HandlerThread handlerThread = new HandlerThread("RollbackPackageHealthObserver");
- handlerThread.start();
- mHandler = new Handler(handlerThread.getLooper());
- File dataDir = new File(Environment.getDataDirectory(), "rollback-observer");
- dataDir.mkdirs();
- mLastStagedRollbackIdsFile = new File(dataDir, "last-staged-rollback-ids");
- mTwoPhaseRollbackEnabledFile = new File(dataDir, "two-phase-rollback-enabled");
- PackageWatchdog.getInstance(mContext).registerHealthObserver(context.getMainExecutor(),
- this);
-
- if (SystemProperties.getBoolean("sys.boot_completed", false)) {
- // Load the value from the file if system server has crashed and restarted
- mTwoPhaseRollbackEnabled = readBoolean(mTwoPhaseRollbackEnabledFile);
- } else {
- // Disable two-phase rollback for a normal reboot. We assume the rebootless apex
- // installed before reboot is stable if native crash didn't happen.
- mTwoPhaseRollbackEnabled = false;
- writeBoolean(mTwoPhaseRollbackEnabledFile, false);
- }
- }
-
- @Override
- public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage,
- @FailureReasons int failureReason, int mitigationCount) {
- int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
- if (Flags.recoverabilityDetection()) {
- List<RollbackInfo> availableRollbacks = getAvailableRollbacks();
- List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel(
- availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_LOW);
- if (!lowImpactRollbacks.isEmpty()) {
- if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) {
- // For native crashes, we will directly roll back any available rollbacks at low
- // impact level
- impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
- } else if (getRollbackForPackage(failedPackage, lowImpactRollbacks) != null) {
- // Rollback is available for crashing low impact package
- impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
- } else {
- impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70;
- }
- }
- } else {
- boolean anyRollbackAvailable = !mContext.getSystemService(RollbackManager.class)
- .getAvailableRollbacks().isEmpty();
-
- if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH
- && anyRollbackAvailable) {
- // For native crashes, we will directly roll back any available rollbacks
- // Note: For non-native crashes the rollback-all step has higher impact
- impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
- } else if (getAvailableRollback(failedPackage) != null) {
- // Rollback is available, we may get a callback into #onExecuteHealthCheckMitigation
- impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30;
- } else if (anyRollbackAvailable) {
- // If any rollbacks are available, we will commit them
- impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70;
- }
- }
-
- Slog.i(TAG, "Checking available remediations for health check failure."
- + " failedPackage: "
- + (failedPackage == null ? null : failedPackage.getPackageName())
- + " failureReason: " + failureReason
- + " available impact: " + impact);
- return impact;
- }
-
- @Override
- public int onExecuteHealthCheckMitigation(@Nullable VersionedPackage failedPackage,
- @FailureReasons int rollbackReason, int mitigationCount) {
- Slog.i(TAG, "Executing remediation."
- + " failedPackage: "
- + (failedPackage == null ? null : failedPackage.getPackageName())
- + " rollbackReason: " + rollbackReason
- + " mitigationCount: " + mitigationCount);
- if (Flags.recoverabilityDetection()) {
- List<RollbackInfo> availableRollbacks = getAvailableRollbacks();
- if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) {
- mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason));
- return MITIGATION_RESULT_SUCCESS;
- }
-
- List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel(
- availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_LOW);
- RollbackInfo rollback = getRollbackForPackage(failedPackage, lowImpactRollbacks);
- if (rollback != null) {
- mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason));
- } else if (!lowImpactRollbacks.isEmpty()) {
- // Apply all available low impact rollbacks.
- mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason));
- }
- } else {
- if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) {
- mHandler.post(() -> rollbackAll(rollbackReason));
- return MITIGATION_RESULT_SUCCESS;
- }
-
- RollbackInfo rollback = getAvailableRollback(failedPackage);
- if (rollback != null) {
- mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason));
- } else {
- mHandler.post(() -> rollbackAll(rollbackReason));
- }
- }
-
- // Assume rollbacks executed successfully
- return MITIGATION_RESULT_SUCCESS;
- }
-
- @Override
- public int onBootLoop(int mitigationCount) {
- int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
- if (Flags.recoverabilityDetection()) {
- List<RollbackInfo> availableRollbacks = getAvailableRollbacks();
- if (!availableRollbacks.isEmpty()) {
- impact = getUserImpactBasedOnRollbackImpactLevel(availableRollbacks);
- }
- }
- return impact;
- }
-
- @Override
- public int onExecuteBootLoopMitigation(int mitigationCount) {
- if (Flags.recoverabilityDetection()) {
- List<RollbackInfo> availableRollbacks = getAvailableRollbacks();
-
- triggerLeastImpactLevelRollback(availableRollbacks,
- PackageWatchdog.FAILURE_REASON_BOOT_LOOP);
- return MITIGATION_RESULT_SUCCESS;
- }
- return MITIGATION_RESULT_SKIPPED;
- }
-
- @Override
- @NonNull
- public String getUniqueIdentifier() {
- return NAME;
- }
-
- @Override
- public boolean isPersistent() {
- return true;
- }
-
- @Override
- public boolean mayObservePackage(@NonNull String packageName) {
- if (getAvailableRollbacks().isEmpty()) {
- return false;
- }
- return isPersistentSystemApp(packageName);
- }
-
- private List<RollbackInfo> getAvailableRollbacks() {
- return mContext.getSystemService(RollbackManager.class).getAvailableRollbacks();
- }
-
- private boolean isPersistentSystemApp(@NonNull String packageName) {
- PackageManager pm = mContext.getPackageManager();
- try {
- ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
- return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK;
- } catch (PackageManager.NameNotFoundException e) {
- return false;
- }
- }
-
- private void assertInWorkerThread() {
- Preconditions.checkState(mHandler.getLooper().isCurrentThread());
- }
-
- @AnyThread
- @NonNull
- public void notifyRollbackAvailable(@NonNull RollbackInfo rollback) {
- mHandler.post(() -> {
- // Enable two-phase rollback when a rebootless apex rollback is made available.
- // We assume the rebootless apex is stable and is less likely to be the cause
- // if native crash doesn't happen before reboot. So we will clear the flag and disable
- // two-phase rollback after reboot.
- if (isRebootlessApex(rollback)) {
- mTwoPhaseRollbackEnabled = true;
- writeBoolean(mTwoPhaseRollbackEnabledFile, true);
- }
- });
- }
-
- private static boolean isRebootlessApex(RollbackInfo rollback) {
- if (!rollback.isStaged()) {
- for (PackageRollbackInfo info : rollback.getPackages()) {
- if (info.isApex()) {
- return true;
- }
- }
- }
- return false;
- }
-
- /** Verifies the rollback state after a reboot and schedules polling for sometime after reboot
- * to check for native crashes and mitigate them if needed.
- */
- @AnyThread
- public void onBootCompletedAsync() {
- mHandler.post(()->onBootCompleted());
- }
-
- @WorkerThread
- private void onBootCompleted() {
- assertInWorkerThread();
-
- RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class);
- if (!rollbackManager.getAvailableRollbacks().isEmpty()) {
- // TODO(gavincorkery): Call into Package Watchdog from outside the observer
- PackageWatchdog.getInstance(mContext).scheduleCheckAndMitigateNativeCrashes();
- }
-
- SparseArray<String> rollbackIds = popLastStagedRollbackIds();
- for (int i = 0; i < rollbackIds.size(); i++) {
- WatchdogRollbackLogger.logRollbackStatusOnBoot(mContext,
- rollbackIds.keyAt(i), rollbackIds.valueAt(i),
- rollbackManager.getRecentlyCommittedRollbacks());
- }
- }
-
- @AnyThread
- private RollbackInfo getAvailableRollback(VersionedPackage failedPackage) {
- RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class);
- for (RollbackInfo rollback : rollbackManager.getAvailableRollbacks()) {
- for (PackageRollbackInfo packageRollback : rollback.getPackages()) {
- if (packageRollback.getVersionRolledBackFrom().equals(failedPackage)) {
- return rollback;
- }
- // TODO(b/147666157): Extract version number of apk-in-apex so that we don't have
- // to rely on complicated reasoning as below
-
- // Due to b/147666157, for apk in apex, we do not know the version we are rolling
- // back from. But if a package X is embedded in apex A exclusively (not embedded in
- // any other apex), which is not guaranteed, then it is sufficient to check only
- // package names here, as the version of failedPackage and the PackageRollbackInfo
- // can't be different. If failedPackage has a higher version, then it must have
- // been updated somehow. There are two ways: it was updated by an update of apex A
- // or updated directly as apk. In both cases, this rollback would have gotten
- // expired when onPackageReplaced() was called. Since the rollback exists, it has
- // same version as failedPackage.
- if (packageRollback.isApkInApex()
- && packageRollback.getVersionRolledBackFrom().getPackageName()
- .equals(failedPackage.getPackageName())) {
- return rollback;
- }
- }
- }
- return null;
- }
-
- @AnyThread
- private RollbackInfo getRollbackForPackage(@Nullable VersionedPackage failedPackage,
- List<RollbackInfo> availableRollbacks) {
- if (failedPackage == null) {
- return null;
- }
-
- for (RollbackInfo rollback : availableRollbacks) {
- for (PackageRollbackInfo packageRollback : rollback.getPackages()) {
- if (packageRollback.getVersionRolledBackFrom().equals(failedPackage)) {
- return rollback;
- }
- // TODO(b/147666157): Extract version number of apk-in-apex so that we don't have
- // to rely on complicated reasoning as below
-
- // Due to b/147666157, for apk in apex, we do not know the version we are rolling
- // back from. But if a package X is embedded in apex A exclusively (not embedded in
- // any other apex), which is not guaranteed, then it is sufficient to check only
- // package names here, as the version of failedPackage and the PackageRollbackInfo
- // can't be different. If failedPackage has a higher version, then it must have
- // been updated somehow. There are two ways: it was updated by an update of apex A
- // or updated directly as apk. In both cases, this rollback would have gotten
- // expired when onPackageReplaced() was called. Since the rollback exists, it has
- // same version as failedPackage.
- if (packageRollback.isApkInApex()
- && packageRollback.getVersionRolledBackFrom().getPackageName()
- .equals(failedPackage.getPackageName())) {
- return rollback;
- }
- }
- }
- return null;
- }
-
- /**
- * Returns {@code true} if staged session associated with {@code rollbackId} was marked
- * as handled, {@code false} if already handled.
- */
- @WorkerThread
- private boolean markStagedSessionHandled(int rollbackId) {
- assertInWorkerThread();
- return mPendingStagedRollbackIds.remove(rollbackId);
- }
-
- /**
- * Returns {@code true} if all pending staged rollback sessions were marked as handled,
- * {@code false} if there is any left.
- */
- @WorkerThread
- private boolean isPendingStagedSessionsEmpty() {
- assertInWorkerThread();
- return mPendingStagedRollbackIds.isEmpty();
- }
-
- private static boolean readBoolean(File file) {
- try (FileInputStream fis = new FileInputStream(file)) {
- return fis.read() == 1;
- } catch (IOException ignore) {
- return false;
- }
- }
-
- private static void writeBoolean(File file, boolean value) {
- try (FileOutputStream fos = new FileOutputStream(file)) {
- fos.write(value ? 1 : 0);
- fos.flush();
- FileUtils.sync(fos);
- } catch (IOException ignore) {
- }
- }
-
- @WorkerThread
- private void saveStagedRollbackId(int stagedRollbackId, @Nullable VersionedPackage logPackage) {
- assertInWorkerThread();
- writeStagedRollbackId(mLastStagedRollbackIdsFile, stagedRollbackId, logPackage);
- }
-
- static void writeStagedRollbackId(File file, int stagedRollbackId,
- @Nullable VersionedPackage logPackage) {
- try {
- FileOutputStream fos = new FileOutputStream(file, true);
- PrintWriter pw = new PrintWriter(fos);
- String logPackageName = logPackage != null ? logPackage.getPackageName() : "";
- pw.append(String.valueOf(stagedRollbackId)).append(",").append(logPackageName);
- pw.println();
- pw.flush();
- FileUtils.sync(fos);
- pw.close();
- } catch (IOException e) {
- Slog.e(TAG, "Failed to save last staged rollback id", e);
- file.delete();
- }
- }
-
- @WorkerThread
- private SparseArray<String> popLastStagedRollbackIds() {
- assertInWorkerThread();
- try {
- return readStagedRollbackIds(mLastStagedRollbackIdsFile);
- } finally {
- mLastStagedRollbackIdsFile.delete();
- }
- }
-
- static SparseArray<String> readStagedRollbackIds(File file) {
- SparseArray<String> result = new SparseArray<>();
- try {
- String line;
- BufferedReader reader = new BufferedReader(new FileReader(file));
- while ((line = reader.readLine()) != null) {
- // Each line is of the format: "id,logging_package"
- String[] values = line.trim().split(",");
- String rollbackId = values[0];
- String logPackageName = "";
- if (values.length > 1) {
- logPackageName = values[1];
- }
- result.put(Integer.parseInt(rollbackId), logPackageName);
- }
- } catch (Exception ignore) {
- return new SparseArray<>();
- }
- return result;
- }
-
-
- /**
- * Returns true if the package name is the name of a module.
- */
- @AnyThread
- private boolean isModule(String packageName) {
- // Check if the package is listed among the system modules or is an
- // APK inside an updatable APEX.
- try {
- PackageManager pm = mContext.getPackageManager();
- final PackageInfo pkg = pm.getPackageInfo(packageName, 0 /* flags */);
- String apexPackageName = pkg.getApexPackageName();
- if (apexPackageName != null) {
- packageName = apexPackageName;
- }
-
- return pm.getModuleInfo(packageName, 0 /* flags */) != null;
- } catch (PackageManager.NameNotFoundException e) {
- return false;
- }
- }
-
- /**
- * Rolls back the session that owns {@code failedPackage}
- *
- * @param rollback {@code rollbackInfo} of the {@code failedPackage}
- * @param failedPackage the package that needs to be rolled back
- */
- @WorkerThread
- private void rollbackPackage(RollbackInfo rollback, VersionedPackage failedPackage,
- @FailureReasons int rollbackReason) {
- assertInWorkerThread();
- String failedPackageName = (failedPackage == null ? null : failedPackage.getPackageName());
-
- Slog.i(TAG, "Rolling back package. RollbackId: " + rollback.getRollbackId()
- + " failedPackage: " + failedPackageName
- + " rollbackReason: " + rollbackReason);
- logCrashRecoveryEvent(Log.DEBUG, String.format("Rolling back %s. Reason: %s",
- failedPackageName, rollbackReason));
- final RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class);
- int reasonToLog = WatchdogRollbackLogger.mapFailureReasonToMetric(rollbackReason);
- final String failedPackageToLog;
- if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) {
- failedPackageToLog = SystemProperties.get(
- "sys.init.updatable_crashing_process_name", "");
- } else {
- failedPackageToLog = failedPackage.getPackageName();
- }
- VersionedPackage logPackageTemp = null;
- if (isModule(failedPackage.getPackageName())) {
- logPackageTemp = WatchdogRollbackLogger.getLogPackage(mContext, failedPackage);
- }
-
- final VersionedPackage logPackage = logPackageTemp;
- WatchdogRollbackLogger.logEvent(logPackage,
- CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE,
- reasonToLog, failedPackageToLog);
-
- Consumer<Intent> onResult = result -> {
- assertInWorkerThread();
- int status = result.getIntExtra(RollbackManager.EXTRA_STATUS,
- RollbackManager.STATUS_FAILURE);
- if (status == RollbackManager.STATUS_SUCCESS) {
- if (rollback.isStaged()) {
- int rollbackId = rollback.getRollbackId();
- saveStagedRollbackId(rollbackId, logPackage);
- WatchdogRollbackLogger.logEvent(logPackage,
- CrashRecoveryStatsLog
- .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED,
- reasonToLog, failedPackageToLog);
-
- } else {
- WatchdogRollbackLogger.logEvent(logPackage,
- CrashRecoveryStatsLog
- .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS,
- reasonToLog, failedPackageToLog);
- }
- } else {
- WatchdogRollbackLogger.logEvent(logPackage,
- CrashRecoveryStatsLog
- .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE,
- reasonToLog, failedPackageToLog);
- }
- if (rollback.isStaged()) {
- markStagedSessionHandled(rollback.getRollbackId());
- // Wait for all pending staged sessions to get handled before rebooting.
- if (isPendingStagedSessionsEmpty()) {
- CrashRecoveryProperties.attemptingReboot(true);
- mContext.getSystemService(PowerManager.class).reboot("Rollback staged install");
- }
- }
- };
-
- // Define a BroadcastReceiver to handle the result
- BroadcastReceiver rollbackReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent result) {
- mHandler.post(() -> onResult.accept(result));
- }
- };
-
- String intentActionName = CLASS_NAME + rollback.getRollbackId();
- // Register the BroadcastReceiver
- mContext.registerReceiver(rollbackReceiver,
- new IntentFilter(intentActionName),
- Context.RECEIVER_NOT_EXPORTED);
-
- Intent intentReceiver = new Intent(intentActionName);
- intentReceiver.putExtra("rollbackId", rollback.getRollbackId());
- intentReceiver.setPackage(mContext.getPackageName());
- intentReceiver.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
-
- PendingIntent rollbackPendingIntent = PendingIntent.getBroadcast(mContext,
- rollback.getRollbackId(),
- intentReceiver,
- PendingIntent.FLAG_MUTABLE);
-
- rollbackManager.commitRollback(rollback.getRollbackId(),
- Collections.singletonList(failedPackage),
- rollbackPendingIntent.getIntentSender());
- }
-
- /**
- * Two-phase rollback:
- * 1. roll back rebootless apexes first
- * 2. roll back all remaining rollbacks if native crash doesn't stop after (1) is done
- *
- * This approach gives us a better chance to correctly attribute native crash to rebootless
- * apex update without rolling back Mainline updates which might contains critical security
- * fixes.
- */
- @WorkerThread
- private boolean useTwoPhaseRollback(List<RollbackInfo> rollbacks) {
- assertInWorkerThread();
- if (!mTwoPhaseRollbackEnabled) {
- return false;
- }
-
- Slog.i(TAG, "Rolling back all rebootless APEX rollbacks");
- boolean found = false;
- for (RollbackInfo rollback : rollbacks) {
- if (isRebootlessApex(rollback)) {
- VersionedPackage firstRollback =
- rollback.getPackages().get(0).getVersionRolledBackFrom();
- rollbackPackage(rollback, firstRollback,
- PackageWatchdog.FAILURE_REASON_NATIVE_CRASH);
- found = true;
- }
- }
- return found;
- }
-
- /**
- * Rollback the package that has minimum rollback impact level.
- * @param availableRollbacks all available rollbacks
- * @param rollbackReason reason to rollback
- */
- private void triggerLeastImpactLevelRollback(List<RollbackInfo> availableRollbacks,
- @FailureReasons int rollbackReason) {
- int minRollbackImpactLevel = getMinRollbackImpactLevel(availableRollbacks);
-
- if (minRollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_LOW) {
- // Apply all available low impact rollbacks.
- mHandler.post(() -> rollbackAllLowImpact(availableRollbacks, rollbackReason));
- } else if (minRollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_HIGH) {
- // Check disable_high_impact_rollback device config before performing rollback
- if (SystemProperties.getBoolean(PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG, false)) {
- return;
- }
- // Rollback one package at a time. If that doesn't resolve the issue, rollback
- // next with same impact level.
- mHandler.post(() -> rollbackHighImpact(availableRollbacks, rollbackReason));
- }
- }
-
- /**
- * sort the available high impact rollbacks by first package name to have a deterministic order.
- * Apply the first available rollback.
- * @param availableRollbacks all available rollbacks
- * @param rollbackReason reason to rollback
- */
- @WorkerThread
- private void rollbackHighImpact(List<RollbackInfo> availableRollbacks,
- @FailureReasons int rollbackReason) {
- assertInWorkerThread();
- List<RollbackInfo> highImpactRollbacks =
- getRollbacksAvailableForImpactLevel(
- availableRollbacks, PackageManager.ROLLBACK_USER_IMPACT_HIGH);
-
- // sort rollbacks based on package name of the first package. This is to have a
- // deterministic order of rollbacks.
- List<RollbackInfo> sortedHighImpactRollbacks = highImpactRollbacks.stream().sorted(
- Comparator.comparing(a -> a.getPackages().get(0).getPackageName())).toList();
- VersionedPackage firstRollback =
- sortedHighImpactRollbacks
- .get(0)
- .getPackages()
- .get(0)
- .getVersionRolledBackFrom();
- Slog.i(TAG, "Rolling back high impact rollback for package: "
- + firstRollback.getPackageName());
- rollbackPackage(sortedHighImpactRollbacks.get(0), firstRollback, rollbackReason);
- }
-
- @WorkerThread
- private void rollbackAll(@FailureReasons int rollbackReason) {
- assertInWorkerThread();
- RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class);
- List<RollbackInfo> rollbacks = rollbackManager.getAvailableRollbacks();
- if (useTwoPhaseRollback(rollbacks)) {
- return;
- }
-
- Slog.i(TAG, "Rolling back all available rollbacks");
- // Add all rollback ids to mPendingStagedRollbackIds, so that we do not reboot before all
- // pending staged rollbacks are handled.
- for (RollbackInfo rollback : rollbacks) {
- if (rollback.isStaged()) {
- mPendingStagedRollbackIds.add(rollback.getRollbackId());
- }
- }
-
- for (RollbackInfo rollback : rollbacks) {
- VersionedPackage firstRollback =
- rollback.getPackages().get(0).getVersionRolledBackFrom();
- rollbackPackage(rollback, firstRollback, rollbackReason);
- }
- }
-
- /**
- * Rollback all available low impact rollbacks
- * @param availableRollbacks all available rollbacks
- * @param rollbackReason reason to rollbacks
- */
- @WorkerThread
- private void rollbackAllLowImpact(
- List<RollbackInfo> availableRollbacks, @FailureReasons int rollbackReason) {
- assertInWorkerThread();
-
- List<RollbackInfo> lowImpactRollbacks = getRollbacksAvailableForImpactLevel(
- availableRollbacks,
- PackageManager.ROLLBACK_USER_IMPACT_LOW);
- if (useTwoPhaseRollback(lowImpactRollbacks)) {
- return;
- }
-
- Slog.i(TAG, "Rolling back all available low impact rollbacks");
- logCrashRecoveryEvent(Log.DEBUG, "Rolling back all available. Reason: " + rollbackReason);
- // Add all rollback ids to mPendingStagedRollbackIds, so that we do not reboot before all
- // pending staged rollbacks are handled.
- for (RollbackInfo rollback : lowImpactRollbacks) {
- if (rollback.isStaged()) {
- mPendingStagedRollbackIds.add(rollback.getRollbackId());
- }
- }
-
- for (RollbackInfo rollback : lowImpactRollbacks) {
- VersionedPackage firstRollback =
- rollback.getPackages().get(0).getVersionRolledBackFrom();
- rollbackPackage(rollback, firstRollback, rollbackReason);
- }
- }
-
- private List<RollbackInfo> getRollbacksAvailableForImpactLevel(
- List<RollbackInfo> availableRollbacks, int impactLevel) {
- return availableRollbacks.stream()
- .filter(rollbackInfo -> rollbackInfo.getRollbackImpactLevel() == impactLevel)
- .toList();
- }
-
- private int getMinRollbackImpactLevel(List<RollbackInfo> availableRollbacks) {
- return availableRollbacks.stream()
- .mapToInt(RollbackInfo::getRollbackImpactLevel)
- .min()
- .orElse(-1);
- }
-
- private int getUserImpactBasedOnRollbackImpactLevel(List<RollbackInfo> availableRollbacks) {
- int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
- int minImpact = getMinRollbackImpactLevel(availableRollbacks);
- switch (minImpact) {
- case PackageManager.ROLLBACK_USER_IMPACT_LOW:
- impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70;
- break;
- case PackageManager.ROLLBACK_USER_IMPACT_HIGH:
- if (!SystemProperties.getBoolean(PROP_DISABLE_HIGH_IMPACT_ROLLBACK_FLAG, false)) {
- impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_90;
- }
- break;
- default:
- impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0;
- }
- return impact;
- }
-
- @VisibleForTesting
- Handler getHandler() {
- return mHandler;
- }
-}
diff --git a/packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java b/packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java
deleted file mode 100644
index 9cfed02f9355..000000000000
--- a/packages/CrashRecovery/services/module/java/com/android/server/rollback/WatchdogRollbackLogger.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.rollback;
-
-import static com.android.server.crashrecovery.CrashRecoveryUtils.logCrashRecoveryEvent;
-import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH;
-import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING;
-import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING;
-import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK;
-import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH;
-import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH_DURING_BOOT;
-import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN;
-import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED;
-import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE;
-import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE;
-import static com.android.server.crashrecovery.proto.CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInstaller;
-import android.content.pm.PackageManager;
-import android.content.pm.VersionedPackage;
-import android.content.rollback.PackageRollbackInfo;
-import android.content.rollback.RollbackInfo;
-import android.os.SystemProperties;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.Slog;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.PackageWatchdog;
-import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog;
-
-import java.util.List;
-
-/**
- * This class handles the logic for logging Watchdog-triggered rollback events.
- * @hide
- */
-public final class WatchdogRollbackLogger {
- private static final String TAG = "WatchdogRollbackLogger";
-
- private static final String LOGGING_PARENT_KEY = "android.content.pm.LOGGING_PARENT";
-
- private WatchdogRollbackLogger() {
- }
-
- @Nullable
- private static String getLoggingParentName(Context context, @NonNull String packageName) {
- PackageManager packageManager = context.getPackageManager();
- try {
- int flags = PackageManager.MATCH_APEX | PackageManager.GET_META_DATA;
- ApplicationInfo ai = packageManager.getPackageInfo(packageName, flags).applicationInfo;
- if (ai.metaData == null) {
- return null;
- }
- return ai.metaData.getString(LOGGING_PARENT_KEY);
- } catch (Exception e) {
- Slog.w(TAG, "Unable to discover logging parent package: " + packageName, e);
- return null;
- }
- }
-
- /**
- * Returns the logging parent of a given package if it exists, {@code null} otherwise.
- *
- * The logging parent is defined by the {@code android.content.pm.LOGGING_PARENT} field in the
- * metadata of a package's AndroidManifest.xml.
- */
- @VisibleForTesting
- @Nullable
- static VersionedPackage getLogPackage(Context context,
- @NonNull VersionedPackage failingPackage) {
- String logPackageName;
- VersionedPackage loggingParent;
- logPackageName = getLoggingParentName(context, failingPackage.getPackageName());
- if (logPackageName == null) {
- return null;
- }
- try {
- loggingParent = new VersionedPackage(logPackageName, context.getPackageManager()
- .getPackageInfo(logPackageName, 0 /* flags */).getLongVersionCode());
- } catch (PackageManager.NameNotFoundException e) {
- return null;
- }
- return loggingParent;
- }
-
- static void logRollbackStatusOnBoot(Context context, int rollbackId, String logPackageName,
- List<RollbackInfo> recentlyCommittedRollbacks) {
- PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
-
- RollbackInfo rollback = null;
- for (RollbackInfo info : recentlyCommittedRollbacks) {
- if (rollbackId == info.getRollbackId()) {
- rollback = info;
- break;
- }
- }
-
- if (rollback == null) {
- Slog.e(TAG, "rollback info not found for last staged rollback: " + rollbackId);
- return;
- }
-
- // Use the version of the logging parent that was installed before
- // we rolled back for logging purposes.
- VersionedPackage oldLoggingPackage = null;
- if (!TextUtils.isEmpty(logPackageName)) {
- for (PackageRollbackInfo packageRollback : rollback.getPackages()) {
- if (logPackageName.equals(packageRollback.getPackageName())) {
- oldLoggingPackage = packageRollback.getVersionRolledBackFrom();
- break;
- }
- }
- }
-
- int sessionId = rollback.getCommittedSessionId();
- PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId);
- if (sessionInfo == null) {
- Slog.e(TAG, "On boot completed, could not load session id " + sessionId);
- return;
- }
-
- if (sessionInfo.isStagedSessionApplied()) {
- logEvent(oldLoggingPackage,
- WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS,
- WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN, "");
- } else if (sessionInfo.isStagedSessionFailed()) {
- logEvent(oldLoggingPackage,
- WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE,
- WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN, "");
- }
- }
-
- /**
- * Log a Watchdog rollback event to statsd.
- *
- * @param logPackage the package to associate the rollback with.
- * @param type the state of the rollback.
- * @param rollbackReason the reason Watchdog triggered a rollback, if known.
- * @param failingPackageName the failing package or process which triggered the rollback.
- */
- public static void logEvent(@Nullable VersionedPackage logPackage, int type,
- int rollbackReason, @NonNull String failingPackageName) {
- String logMsg = "Watchdog event occurred with type: " + rollbackTypeToString(type)
- + " logPackage: " + logPackage
- + " rollbackReason: " + rollbackReasonToString(rollbackReason)
- + " failedPackageName: " + failingPackageName;
- Slog.i(TAG, logMsg);
- if (logPackage != null) {
- CrashRecoveryStatsLog.write(
- CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED,
- type,
- logPackage.getPackageName(),
- logPackage.getVersionCode(),
- rollbackReason,
- failingPackageName,
- new byte[]{});
- } else {
- // In the case that the log package is null, still log an empty string as an
- // indication that retrieving the logging parent failed.
- CrashRecoveryStatsLog.write(
- CrashRecoveryStatsLog.WATCHDOG_ROLLBACK_OCCURRED,
- type,
- "",
- 0,
- rollbackReason,
- failingPackageName,
- new byte[]{});
- }
-
- logTestProperties(logMsg);
- }
-
- /**
- * Writes properties which will be used by rollback tests to check if particular rollback
- * events have occurred.
- */
- private static void logTestProperties(String logMsg) {
- // This property should be on only during the tests
- if (!SystemProperties.getBoolean("persist.sys.rollbacktest.enabled", false)) {
- return;
- }
- logCrashRecoveryEvent(Log.DEBUG, logMsg);
- }
-
- @VisibleForTesting
- static int mapFailureReasonToMetric(@PackageWatchdog.FailureReasons int failureReason) {
- switch (failureReason) {
- case PackageWatchdog.FAILURE_REASON_NATIVE_CRASH:
- return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH;
- case PackageWatchdog.FAILURE_REASON_EXPLICIT_HEALTH_CHECK:
- return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK;
- case PackageWatchdog.FAILURE_REASON_APP_CRASH:
- return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH;
- case PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING:
- return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING;
- case PackageWatchdog.FAILURE_REASON_BOOT_LOOP:
- return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING;
- default:
- return WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_UNKNOWN;
- }
- }
-
- private static String rollbackTypeToString(int type) {
- switch (type) {
- case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE:
- return "ROLLBACK_INITIATE";
- case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS:
- return "ROLLBACK_SUCCESS";
- case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE:
- return "ROLLBACK_FAILURE";
- case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED:
- return "ROLLBACK_BOOT_TRIGGERED";
- default:
- return "UNKNOWN";
- }
- }
-
- private static String rollbackReasonToString(int reason) {
- switch (reason) {
- case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH:
- return "REASON_NATIVE_CRASH";
- case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_EXPLICIT_HEALTH_CHECK:
- return "REASON_EXPLICIT_HEALTH_CHECK";
- case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_CRASH:
- return "REASON_APP_CRASH";
- case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_APP_NOT_RESPONDING:
- return "REASON_APP_NOT_RESPONDING";
- case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_NATIVE_CRASH_DURING_BOOT:
- return "REASON_NATIVE_CRASH_DURING_BOOT";
- case WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_REASON__REASON_BOOT_LOOPING:
- return "REASON_BOOT_LOOP";
- default:
- return "UNKNOWN";
- }
- }
-}
diff --git a/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java
deleted file mode 100644
index 29ff7cced897..000000000000
--- a/packages/CrashRecovery/services/module/java/com/android/util/ArrayUtils.java
+++ /dev/null
@@ -1,42 +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 android.util;
-
-import android.annotation.Nullable;
-
-/**
- * Copied over from frameworks/base/core/java/com/android/internal/util/ArrayUtils.java
- *
- * @hide
- */
-public class ArrayUtils {
- private ArrayUtils() { /* cannot be instantiated */ }
-
- /**
- * Checks if given array is null or has zero elements.
- */
- public static boolean isEmpty(@Nullable int[] array) {
- return array == null || array.length == 0;
- }
-
- /**
- * True if the byte array is null or has length 0.
- */
- public static boolean isEmpty(@Nullable byte[] array) {
- return array == null || array.length == 0;
- }
-}
diff --git a/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java
deleted file mode 100644
index d60a9b9847ca..000000000000
--- a/packages/CrashRecovery/services/module/java/com/android/util/FileUtils.java
+++ /dev/null
@@ -1,117 +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 android.util;
-
-import android.annotation.Nullable;
-
-import java.io.BufferedInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * Bits and pieces copied from hidden API of android.os.FileUtils.
- *
- * @hide
- */
-public class FileUtils {
- /**
- * Read a text file into a String, optionally limiting the length.
- *
- * @param file to read (will not seek, so things like /proc files are OK)
- * @param max length (positive for head, negative of tail, 0 for no limit)
- * @param ellipsis to add of the file was truncated (can be null)
- * @return the contents of the file, possibly truncated
- * @throws IOException if something goes wrong reading the file
- * @hide
- */
- public static @Nullable String readTextFile(@Nullable File file, @Nullable int max,
- @Nullable String ellipsis) throws IOException {
- InputStream input = new FileInputStream(file);
- // wrapping a BufferedInputStream around it because when reading /proc with unbuffered
- // input stream, bytes read not equal to buffer size is not necessarily the correct
- // indication for EOF; but it is true for BufferedInputStream due to its implementation.
- BufferedInputStream bis = new BufferedInputStream(input);
- try {
- long size = file.length();
- if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes
- if (size > 0 && (max == 0 || size < max)) max = (int) size;
- byte[] data = new byte[max + 1];
- int length = bis.read(data);
- if (length <= 0) return "";
- if (length <= max) return new String(data, 0, length);
- if (ellipsis == null) return new String(data, 0, max);
- return new String(data, 0, max) + ellipsis;
- } else if (max < 0) { // "tail" mode: keep the last N
- int len;
- boolean rolled = false;
- byte[] last = null;
- byte[] data = null;
- do {
- if (last != null) rolled = true;
- byte[] tmp = last;
- last = data;
- data = tmp;
- if (data == null) data = new byte[-max];
- len = bis.read(data);
- } while (len == data.length);
-
- if (last == null && len <= 0) return "";
- if (last == null) return new String(data, 0, len);
- if (len > 0) {
- rolled = true;
- System.arraycopy(last, len, last, 0, last.length - len);
- System.arraycopy(data, 0, last, last.length - len, len);
- }
- if (ellipsis == null || !rolled) return new String(last);
- return ellipsis + new String(last);
- } else { // "cat" mode: size unknown, read it all in streaming fashion
- ByteArrayOutputStream contents = new ByteArrayOutputStream();
- int len;
- byte[] data = new byte[1024];
- do {
- len = bis.read(data);
- if (len > 0) contents.write(data, 0, len);
- } while (len == data.length);
- return contents.toString();
- }
- } finally {
- bis.close();
- input.close();
- }
- }
-
- /**
- * Perform an fsync on the given FileOutputStream. The stream at this
- * point must be flushed but not yet closed.
- *
- * @hide
- */
- public static boolean sync(FileOutputStream stream) {
- try {
- if (stream != null) {
- stream.getFD().sync();
- }
- return true;
- } catch (IOException e) {
- }
- return false;
- }
-}
diff --git a/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java b/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java
deleted file mode 100644
index 9a24ada8b69a..000000000000
--- a/packages/CrashRecovery/services/module/java/com/android/util/LongArrayQueue.java
+++ /dev/null
@@ -1,188 +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 android.util;
-
-import libcore.util.EmptyArray;
-
-import java.util.NoSuchElementException;
-
-/**
- * Copied from frameworks/base/core/java/android/util/LongArrayQueue.java
- *
- * @hide
- */
-public class LongArrayQueue {
-
- private long[] mValues;
- private int mSize;
- private int mHead;
- private int mTail;
-
- private long[] newUnpaddedLongArray(int num) {
- return new long[num];
- }
- /**
- * Initializes a queue with the given starting capacity.
- *
- * @param initialCapacity the capacity.
- */
- public LongArrayQueue(int initialCapacity) {
- if (initialCapacity == 0) {
- mValues = EmptyArray.LONG;
- } else {
- mValues = newUnpaddedLongArray(initialCapacity);
- }
- mSize = 0;
- mHead = mTail = 0;
- }
-
- /**
- * Initializes a queue with default starting capacity.
- */
- public LongArrayQueue() {
- this(16);
- }
-
- /** @hide */
- public static int growSize(int currentSize) {
- return currentSize <= 4 ? 8 : currentSize * 2;
- }
-
- private void grow() {
- if (mSize < mValues.length) {
- throw new IllegalStateException("Queue not full yet!");
- }
- final int newSize = growSize(mSize);
- final long[] newArray = newUnpaddedLongArray(newSize);
- final int r = mValues.length - mHead; // Number of elements on and to the right of head.
- System.arraycopy(mValues, mHead, newArray, 0, r);
- System.arraycopy(mValues, 0, newArray, r, mHead);
- mValues = newArray;
- mHead = 0;
- mTail = mSize;
- }
-
- /**
- * Returns the number of elements in the queue.
- */
- public int size() {
- return mSize;
- }
-
- /**
- * Removes all elements from this queue.
- */
- public void clear() {
- mSize = 0;
- mHead = mTail = 0;
- }
-
- /**
- * Adds a value to the tail of the queue.
- *
- * @param value the value to be added.
- */
- public void addLast(long value) {
- if (mSize == mValues.length) {
- grow();
- }
- mValues[mTail] = value;
- mTail = (mTail + 1) % mValues.length;
- mSize++;
- }
-
- /**
- * Removes an element from the head of the queue.
- *
- * @return the element at the head of the queue.
- * @throws NoSuchElementException if the queue is empty.
- */
- public long removeFirst() {
- if (mSize == 0) {
- throw new NoSuchElementException("Queue is empty!");
- }
- final long ret = mValues[mHead];
- mHead = (mHead + 1) % mValues.length;
- mSize--;
- return ret;
- }
-
- /**
- * Returns the element at the given position from the head of the queue, where 0 represents the
- * head of the queue.
- *
- * @param position the position from the head of the queue.
- * @return the element found at the given position.
- * @throws IndexOutOfBoundsException if {@code position} < {@code 0} or
- * {@code position} >= {@link #size()}
- */
- public long get(int position) {
- if (position < 0 || position >= mSize) {
- throw new IndexOutOfBoundsException("Index " + position
- + " not valid for a queue of size " + mSize);
- }
- final int index = (mHead + position) % mValues.length;
- return mValues[index];
- }
-
- /**
- * Returns the element at the head of the queue, without removing it.
- *
- * @return the element at the head of the queue.
- * @throws NoSuchElementException if the queue is empty
- */
- public long peekFirst() {
- if (mSize == 0) {
- throw new NoSuchElementException("Queue is empty!");
- }
- return mValues[mHead];
- }
-
- /**
- * Returns the element at the tail of the queue.
- *
- * @return the element at the tail of the queue.
- * @throws NoSuchElementException if the queue is empty.
- */
- public long peekLast() {
- if (mSize == 0) {
- throw new NoSuchElementException("Queue is empty!");
- }
- final int index = (mTail == 0) ? mValues.length - 1 : mTail - 1;
- return mValues[index];
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public String toString() {
- if (mSize <= 0) {
- return "{}";
- }
-
- final StringBuilder buffer = new StringBuilder(mSize * 64);
- buffer.append('{');
- buffer.append(get(0));
- for (int i = 1; i < mSize; i++) {
- buffer.append(", ");
- buffer.append(get(i));
- }
- buffer.append('}');
- return buffer.toString();
- }
-}
diff --git a/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java b/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java
deleted file mode 100644
index 488b531c2b8a..000000000000
--- a/packages/CrashRecovery/services/module/java/com/android/util/XmlUtils.java
+++ /dev/null
@@ -1,66 +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 android.util;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import java.io.IOException;
-
-/**
- * Bits and pieces copied from hidden API of
- * frameworks/base/core/java/com/android/internal/util/XmlUtils.java
- *
- * @hide
- */
-public class XmlUtils {
-
- /** @hide */
- public static final void beginDocument(XmlPullParser parser, String firstElementName)
- throws XmlPullParserException, IOException {
- int type;
- while ((type = parser.next()) != parser.START_TAG
- && type != parser.END_DOCUMENT) {
- // Do nothing
- }
-
- if (type != parser.START_TAG) {
- throw new XmlPullParserException("No start tag found");
- }
-
- if (!parser.getName().equals(firstElementName)) {
- throw new XmlPullParserException("Unexpected start tag: found " + parser.getName()
- + ", expected " + firstElementName);
- }
- }
-
- /** @hide */
- public static boolean nextElementWithin(XmlPullParser parser, int outerDepth)
- throws IOException, XmlPullParserException {
- for (;;) {
- int type = parser.next();
- if (type == XmlPullParser.END_DOCUMENT
- || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) {
- return false;
- }
- if (type == XmlPullParser.START_TAG
- && parser.getDepth() == outerDepth + 1) {
- return true;
- }
- }
- }
-}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt
index a140eb8424a8..a8483308556d 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/enterprise/RestrictedMode.kt
@@ -35,6 +35,7 @@ interface BlockedByAdmin : RestrictedMode {
interface BlockedByEcm : RestrictedMode {
fun showRestrictedSettingsDetails()
+ fun isBlockedByPhoneCall() = false
}
internal data class BlockedByAdminImpl(
@@ -72,8 +73,13 @@ internal data class BlockedByEcmImpl(
private val context: Context,
private val intent: Intent,
) : BlockedByEcm {
+ private val reasonPhoneState = "phone_state"
override fun showRestrictedSettingsDetails() {
context.startActivity(intent)
}
+
+ override fun isBlockedByPhoneCall(): Boolean {
+ return intent.getStringExtra(Intent.EXTRA_REASON) == reasonPhoneState
+ }
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
index 0bb92ce72595..fb4880f10d3e 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
@@ -163,9 +163,11 @@ internal class RestrictedSwitchPreferenceModel(
is BlockedByAdmin ->
restrictedMode.getSummary(checkedIfBlockedByAdmin ?: checkedIfNoRestricted())
- is BlockedByEcm ->
+ is BlockedByEcm -> if (restrictedMode.isBlockedByPhoneCall()) {
+ context.getString(com.android.settingslib.R.string.disabled_in_phone_call_text)
+ } else {
context.getString(com.android.settingslib.R.string.disabled)
-
+ }
null -> context.getPlaceholder()
}
}
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 3da2271431f8..a3e42f1d1e51 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1228,6 +1228,8 @@
<!-- Summary for settings preference disabled by app ops [CHAR LIMIT=50] -->
<string name="disabled_by_app_ops_text">Controlled by Restricted Setting</string>
+ <!-- Summary for settings preference disabled while the device is in a phone call [CHAR LIMIT=50] -->
+ <string name="disabled_in_phone_call_text">Unavailable during calls</string>
<!-- [CHAR LIMIT=25] Manage applications, text telling using an application is disabled. -->
<string name="disabled">Disabled</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java
index 212e43aa4044..1044750bae25 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java
@@ -44,6 +44,8 @@ import androidx.preference.PreferenceViewHolder;
public class RestrictedPreferenceHelper {
private static final String TAG = "RestrictedPreferenceHelper";
+ private static final String REASON_PHONE_STATE = "phone_state";
+
private final Context mContext;
private final Preference mPreference;
String packageName;
@@ -121,7 +123,7 @@ public class RestrictedPreferenceHelper {
if (mDisabledByAdmin) {
summaryView.setText(disabledText);
} else if (mDisabledByEcm) {
- summaryView.setText(R.string.disabled_by_app_ops_text);
+ summaryView.setText(getEcmTextResId());
} else if (TextUtils.equals(disabledText, summaryView.getText())) {
// It's previously set to disabled text, clear it.
summaryView.setText(null);
@@ -323,7 +325,16 @@ public class RestrictedPreferenceHelper {
}
if (!isEnabled && mDisabledByEcm) {
- mPreference.setSummary(R.string.disabled_by_app_ops_text);
+ mPreference.setSummary(getEcmTextResId());
+ }
+ }
+
+ private int getEcmTextResId() {
+ if (mDisabledByEcmIntent != null && REASON_PHONE_STATE.equals(
+ mDisabledByEcmIntent.getStringExtra(Intent.EXTRA_REASON))) {
+ return R.string.disabled_in_phone_call_text;
+ } else {
+ return R.string.disabled_by_app_ops_text;
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 4ee9ff059502..ceb6f7b080df 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -16,8 +16,6 @@
package com.android.settingslib.media;
import static android.media.MediaRoute2Info.TYPE_AUX_LINE;
-import static android.media.MediaRoute2Info.TYPE_LINE_ANALOG;
-import static android.media.MediaRoute2Info.TYPE_LINE_DIGITAL;
import static android.media.MediaRoute2Info.TYPE_BLE_HEADSET;
import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
@@ -27,6 +25,8 @@ import static android.media.MediaRoute2Info.TYPE_HDMI;
import static android.media.MediaRoute2Info.TYPE_HDMI_ARC;
import static android.media.MediaRoute2Info.TYPE_HDMI_EARC;
import static android.media.MediaRoute2Info.TYPE_HEARING_AID;
+import static android.media.MediaRoute2Info.TYPE_LINE_ANALOG;
+import static android.media.MediaRoute2Info.TYPE_LINE_DIGITAL;
import static android.media.MediaRoute2Info.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER;
import static android.media.MediaRoute2Info.TYPE_REMOTE_CAR;
import static android.media.MediaRoute2Info.TYPE_REMOTE_COMPUTER;
@@ -254,6 +254,10 @@ public abstract class InfoMediaManager {
protected abstract List<MediaRoute2Info> getSelectableRoutes(@NonNull RoutingSessionInfo info);
@NonNull
+ protected abstract List<MediaRoute2Info> getTransferableRoutes(
+ @NonNull RoutingSessionInfo info);
+
+ @NonNull
protected abstract List<MediaRoute2Info> getDeselectableRoutes(
@NonNull RoutingSessionInfo info);
@@ -519,6 +523,22 @@ public abstract class InfoMediaManager {
}
/**
+ * Returns the list of {@link MediaDevice media devices} that can be transferred to with the
+ * current {@link RoutingSessionInfo routing session} by the media route provider.
+ */
+ @NonNull
+ List<MediaDevice> getTransferableMediaDevices() {
+ final RoutingSessionInfo info = getActiveRoutingSession();
+
+ final List<MediaDevice> deviceList = new ArrayList<>();
+ for (MediaRoute2Info route : getTransferableRoutes(info)) {
+ deviceList.add(
+ new InfoMediaDevice(mContext, route, mPreferenceItemMap.get(route.getId())));
+ }
+ return deviceList;
+ }
+
+ /**
* Returns the list of {@link MediaDevice media devices} that can be deselected from the current
* {@link RoutingSessionInfo routing session}.
*/
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index fe6659d1dc4f..76f366d3d1b6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -352,6 +352,17 @@ public class LocalMediaManager implements BluetoothCallback {
}
/**
+ * Gets the MediaDevice list that can be transferred to with the current media session by the
+ * media route provider.
+ *
+ * @return list of MediaDevice
+ */
+ @NonNull
+ public List<MediaDevice> getTransferableMediaDevices() {
+ return mInfoMediaManager.getTransferableMediaDevices();
+ }
+
+ /**
* Get the MediaDevice list that can be removed from current media session.
*
* @return list of MediaDevice
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
index 82b197682459..9e511ffb4e34 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/ManagerInfoMediaManager.java
@@ -117,6 +117,12 @@ public class ManagerInfoMediaManager extends InfoMediaManager {
@Override
@NonNull
+ protected List<MediaRoute2Info> getTransferableRoutes(@NonNull RoutingSessionInfo info) {
+ return mRouterManager.getTransferableRoutes(info);
+ }
+
+ @Override
+ @NonNull
protected List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo info) {
return mRouterManager.getDeselectableRoutes(info);
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index b01b7c9048ba..d018d1404623 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -15,6 +15,7 @@
*/
package com.android.settingslib.media;
+import static android.media.MediaRoute2Info.TYPE_AUX_LINE;
import static android.media.MediaRoute2Info.TYPE_BLE_HEADSET;
import static android.media.MediaRoute2Info.TYPE_BLUETOOTH_A2DP;
import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER;
@@ -24,6 +25,8 @@ import static android.media.MediaRoute2Info.TYPE_HDMI;
import static android.media.MediaRoute2Info.TYPE_HDMI_ARC;
import static android.media.MediaRoute2Info.TYPE_HDMI_EARC;
import static android.media.MediaRoute2Info.TYPE_HEARING_AID;
+import static android.media.MediaRoute2Info.TYPE_LINE_ANALOG;
+import static android.media.MediaRoute2Info.TYPE_LINE_DIGITAL;
import static android.media.MediaRoute2Info.TYPE_REMOTE_AUDIO_VIDEO_RECEIVER;
import static android.media.MediaRoute2Info.TYPE_REMOTE_SPEAKER;
import static android.media.MediaRoute2Info.TYPE_REMOTE_TV;
@@ -33,9 +36,6 @@ import static android.media.MediaRoute2Info.TYPE_USB_DEVICE;
import static android.media.MediaRoute2Info.TYPE_USB_HEADSET;
import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES;
import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
-import static android.media.MediaRoute2Info.TYPE_LINE_DIGITAL;
-import static android.media.MediaRoute2Info.TYPE_LINE_ANALOG;
-import static android.media.MediaRoute2Info.TYPE_AUX_LINE;
import static android.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION;
import static android.media.RouteListingPreference.Item.FLAG_ONGOING_SESSION_MANAGED;
import static android.media.RouteListingPreference.Item.FLAG_SUGGESTED;
@@ -244,6 +244,11 @@ public abstract class MediaDevice implements Comparable<MediaDevice> {
*/
public abstract String getId();
+ /** Returns {@code true} if the device has a non-null {@link RouteListingPreference.Item}. */
+ public boolean hasRouteListingPreferenceItem() {
+ return mItem != null;
+ }
+
/**
* Get selection behavior of device
*
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
index 2c7ec9302117..9fe5b1d58752 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/NoOpInfoMediaManager.java
@@ -114,6 +114,12 @@ import java.util.List;
@NonNull
@Override
+ protected List<MediaRoute2Info> getTransferableRoutes(@NonNull RoutingSessionInfo info) {
+ return Collections.emptyList();
+ }
+
+ @NonNull
+ @Override
protected List<MediaRoute2Info> getDeselectableRoutes(@NonNull RoutingSessionInfo info) {
return Collections.emptyList();
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
index eced7b3a116b..6a2da182dbb1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
@@ -203,6 +203,13 @@ public final class RouterInfoMediaManager extends InfoMediaManager {
@NonNull
@Override
+ protected List<MediaRoute2Info> getTransferableRoutes(@NonNull RoutingSessionInfo info) {
+ RoutingController controller = getControllerForSession(info);
+ return getTransferableRoutes(controller);
+ }
+
+ @NonNull
+ @Override
protected List<MediaRoute2Info> getSelectedRoutes(@NonNull RoutingSessionInfo info) {
RoutingController controller = getControllerForSession(info);
if (controller == null) {
@@ -272,22 +279,27 @@ public final class RouterInfoMediaManager extends InfoMediaManager {
protected List<MediaRoute2Info> getTransferableRoutes(@NonNull String packageName) {
List<RoutingController> controllers = mRouter.getControllers();
RoutingController activeController = controllers.get(controllers.size() - 1);
- HashMap<String, MediaRoute2Info> transferableRoutes = new HashMap<>();
-
- activeController
- .getTransferableRoutes()
- .forEach(route -> transferableRoutes.put(route.getId(), route));
+ return getTransferableRoutes(activeController);
+ }
- if (activeController.getRoutingSessionInfo().isSystemSession()) {
- mRouter.getRoutes().stream()
- .filter(route -> !route.isSystemRoute())
- .forEach(route -> transferableRoutes.put(route.getId(), route));
- } else {
- mRouter.getRoutes().stream()
- .filter(route -> route.isSystemRoute())
+ @NonNull
+ private List<MediaRoute2Info> getTransferableRoutes(@Nullable RoutingController controller) {
+ HashMap<String, MediaRoute2Info> transferableRoutes = new HashMap<>();
+ if (controller != null) {
+ controller
+ .getTransferableRoutes()
.forEach(route -> transferableRoutes.put(route.getId(), route));
- }
+ if (controller.getRoutingSessionInfo().isSystemSession()) {
+ mRouter.getRoutes().stream()
+ .filter(route -> !route.isSystemRoute())
+ .forEach(route -> transferableRoutes.put(route.getId(), route));
+ } else {
+ mRouter.getRoutes().stream()
+ .filter(route -> route.isSystemRoute())
+ .forEach(route -> transferableRoutes.put(route.getId(), route));
+ }
+ }
return new ArrayList<>(transferableRoutes.values());
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
index 64a2de5025de..ecea5fd35150 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
@@ -33,10 +33,12 @@ import android.service.notification.ZenPolicy;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
-import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
public class TestModeBuilder {
+ private static final AtomicInteger sNextId = new AtomicInteger(0);
+
private String mId;
private AutomaticZenRule mRule;
private ZenModeConfig.ZenRule mConfigZenRule;
@@ -47,7 +49,7 @@ public class TestModeBuilder {
public TestModeBuilder() {
// Reasonable defaults
- int id = new Random().nextInt(1000);
+ int id = sNextId.incrementAndGet();
mId = "rule_" + id;
mRule = new AutomaticZenRule.Builder("Test Rule #" + id, Uri.parse("rule://" + id))
.setPackage("some_package")
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 1a83f0a2e775..219ad6ca3f1a 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
@@ -65,7 +65,8 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
@@ -78,6 +79,7 @@ import java.util.Set;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowRouter2Manager.class})
public class InfoMediaManagerTest {
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
private static final String TEST_PACKAGE_NAME = "com.test.packagename";
private static final String TEST_PACKAGE_NAME_2 = "com.test.packagename2";
@@ -146,7 +148,6 @@ public class InfoMediaManagerTest {
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
mContext = spy(RuntimeEnvironment.application);
doReturn(mMediaSessionManager).when(mContext).getSystemService(
@@ -663,6 +664,26 @@ public class InfoMediaManagerTest {
}
@Test
+ public void getTransferableMediaDevice_checkList() {
+ final List<MediaRoute2Info> mediaRoute2Infos = new ArrayList<>();
+ final MediaRoute2Info mediaRoute2Info = mock(MediaRoute2Info.class);
+ mediaRoute2Infos.add(mediaRoute2Info);
+ mShadowRouter2Manager.setTransferableRoutes(mediaRoute2Infos);
+ when(mediaRoute2Info.getName()).thenReturn(TEST_NAME);
+ when(mediaRoute2Info.getId()).thenReturn(TEST_ID);
+ mInfoMediaManager.mRouterManager = mRouterManager;
+ when(mRouterManager.getRoutingSessions(TEST_PACKAGE_NAME))
+ .thenReturn(List.of(TEST_REMOTE_ROUTING_SESSION));
+ when(mRouterManager.getTransferableRoutes(any(RoutingSessionInfo.class)))
+ .thenReturn(mediaRoute2Infos);
+
+ final List<MediaDevice> mediaDevices = mInfoMediaManager.getTransferableMediaDevices();
+
+ assertThat(mediaDevices.size()).isEqualTo(1);
+ assertThat(mediaDevices.get(0).getName()).isEqualTo(TEST_NAME);
+ }
+
+ @Test
public void getDeselectableMediaDevice_checkList() {
final List<RoutingSessionInfo> routingSessionInfos = new ArrayList<>();
final RoutingSessionInfo info = mock(RoutingSessionInfo.class);
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 8fe3a0c4b4ae..55f7317f25e4 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -1015,6 +1015,9 @@
<uses-permission android:name="android.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE" />
<uses-permission android:name="android.permission.READ_COLOR_ZONES" />
+ <!-- Permission required for trade-in mode testing -->
+ <uses-permission android:name="android.permission.ENTER_TRADE_IN_MODE" />
+
<application
android:label="@string/app_label"
android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 0a7d880677d8..744388f47d0e 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -314,6 +314,7 @@ filegroup {
"tests/src/**/systemui/statusbar/policy/WalletControllerImplTest.kt",
"tests/src/**/keyguard/ClockEventControllerTest.kt",
"tests/src/**/systemui/bluetooth/qsdialog/BluetoothStateInteractorTest.kt",
+ "tests/src/**/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt",
"tests/src/**/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt",
"tests/src/**/systemui/bluetooth/qsdialog/BluetoothTileDialogRepositoryTest.kt",
"tests/src/**/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt",
@@ -424,7 +425,6 @@ android_library {
manifest: "AndroidManifest-res.xml",
flags_packages: [
"android.app.flags-aconfig",
- "com_android_systemui_flags",
],
}
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index fb21be4c3bd1..3cb30258fcb1 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -120,6 +120,16 @@ flag {
}
flag {
+ name: "update_window_magnifier_bottom_boundary"
+ namespace: "accessibility"
+ description: "Update the window magnifier boundary at the bottom to the top of the system gesture inset."
+ bug: "380320995"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "hearing_devices_dialog_related_tools"
namespace: "accessibility"
description: "Shows the related tools for hearing devices dialog."
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 6c96279711d0..ac53dcb1982a 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -2013,3 +2013,10 @@ flag {
description: "Show a Locked by your watch indicator on the keyguard when the device is locked by the watch."
bug: "387322459"
}
+
+flag {
+ name: "decouple_view_controller_in_animlib"
+ namespace: "systemui"
+ description: "Decouple view and controller in AnimLib."
+ bug: "393241010"
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index c8d3430bf54b..f03bd3d9a2a7 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -73,6 +73,9 @@ import com.android.wm.shell.shared.ShellTransitions
import com.android.wm.shell.shared.TransitionUtil
import java.util.concurrent.Executor
import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeoutOrNull
private const val TAG = "ActivityTransitionAnimator"
@@ -241,7 +244,7 @@ constructor(
override fun onTransitionAnimationProgress(linearProgress: Float) {
LinkedHashSet(listeners).forEach {
- it.onTransitionAnimationProgress(linearProgress)
+ it.onTransitionAnimationProgress(linearProgress)
}
}
@@ -494,15 +497,19 @@ constructor(
/**
* Create a new animation [Runner] controlled by the [Controller] that [controllerFactory] can
- * create based on [forLaunch].
+ * create based on [forLaunch] and within the given [scope].
*
* This method must only be used for long-lived registrations. Otherwise, use
* [createEphemeralRunner].
*/
@VisibleForTesting
- fun createLongLivedRunner(controllerFactory: ControllerFactory, forLaunch: Boolean): Runner {
+ fun createLongLivedRunner(
+ controllerFactory: ControllerFactory,
+ scope: CoroutineScope,
+ forLaunch: Boolean,
+ ): Runner {
assertLongLivedReturnAnimations()
- return Runner(callback!!, transitionAnimator, lifecycleListener) {
+ return Runner(scope, callback!!, transitionAnimator, lifecycleListener) {
controllerFactory.createController(forLaunch)
}
}
@@ -564,7 +571,7 @@ constructor(
* Creates a [Controller] for launching or returning from the activity linked to [cookie]
* and [component].
*/
- abstract fun createController(forLaunch: Boolean): Controller
+ abstract suspend fun createController(forLaunch: Boolean): Controller
}
/**
@@ -691,9 +698,14 @@ constructor(
* animations.
*
* The [Controller]s created by [controllerFactory] will only be used for transitions matching
- * the [cookie], or the [ComponentName] defined within it if the cookie matching fails.
+ * the [cookie], or the [ComponentName] defined within it if the cookie matching fails. These
+ * [Controller]s can only be created within [scope].
*/
- fun register(cookie: TransitionCookie, controllerFactory: ControllerFactory) {
+ fun register(
+ cookie: TransitionCookie,
+ controllerFactory: ControllerFactory,
+ scope: CoroutineScope,
+ ) {
assertLongLivedReturnAnimations()
if (transitionRegister == null) {
@@ -725,7 +737,7 @@ constructor(
}
val launchRemoteTransition =
RemoteTransition(
- OriginTransition(createLongLivedRunner(controllerFactory, forLaunch = true)),
+ OriginTransition(createLongLivedRunner(controllerFactory, scope, forLaunch = true)),
"${cookie}_launchTransition",
)
transitionRegister.register(launchFilter, launchRemoteTransition, includeTakeover = true)
@@ -749,7 +761,9 @@ constructor(
}
val returnRemoteTransition =
RemoteTransition(
- OriginTransition(createLongLivedRunner(controllerFactory, forLaunch = false)),
+ OriginTransition(
+ createLongLivedRunner(controllerFactory, scope, forLaunch = false)
+ ),
"${cookie}_returnTransition",
)
transitionRegister.register(returnFilter, returnRemoteTransition, includeTakeover = true)
@@ -952,7 +966,9 @@ constructor(
* Reusable factory to generate single-use controllers. In case of an ephemeral [Runner],
* this must be null and [controller] must be defined instead.
*/
- private val controllerFactory: (() -> Controller)?,
+ private val controllerFactory: (suspend () -> Controller)?,
+ /** The scope to use when this runner is based on [controllerFactory]. */
+ private val scope: CoroutineScope? = null,
private val callback: Callback,
/** The animator to use to animate the window transition. */
private val transitionAnimator: TransitionAnimator,
@@ -973,13 +989,15 @@ constructor(
)
constructor(
+ scope: CoroutineScope,
callback: Callback,
transitionAnimator: TransitionAnimator,
listener: Listener? = null,
- controllerFactory: () -> Controller,
+ controllerFactory: suspend () -> Controller,
) : this(
controller = null,
controllerFactory = controllerFactory,
+ scope = scope,
callback = callback,
transitionAnimator = transitionAnimator,
listener = listener,
@@ -994,12 +1012,12 @@ constructor(
assert((controller != null).xor(controllerFactory != null))
delegate = null
- if (controller != null) {
+ controller?.let {
// Ephemeral launches bundle the runner with the launch request (instead of being
// registered ahead of time for later use). This means that there could be a timeout
// between creation and invocation, so the delegate needs to exist from the
// beginning in order to handle such timeout.
- createDelegate()
+ createDelegate(it)
}
}
@@ -1040,49 +1058,79 @@ constructor(
finishedCallback: IRemoteAnimationFinishedCallback?,
performAnimation: (AnimationDelegate) -> Unit,
) {
- maybeSetUp()
- val delegate = delegate
- mainExecutor.execute {
- if (delegate == null) {
- Log.i(TAG, "onAnimationStart called after completion")
- // Animation started too late and timed out already. We need to still
- // signal back that we're done with it.
- finishedCallback?.onAnimationFinished()
- } else {
- performAnimation(delegate)
+ val controller = controller
+ val controllerFactory = controllerFactory
+
+ if (controller != null) {
+ maybeSetUp(controller)
+ val success = startAnimation(performAnimation)
+ if (!success) finishedCallback?.onAnimationFinished()
+ } else if (controllerFactory != null) {
+ scope?.launch {
+ val success =
+ withTimeoutOrNull(TRANSITION_TIMEOUT) {
+ setUp(controllerFactory)
+ startAnimation(performAnimation)
+ } ?: false
+ if (!success) finishedCallback?.onAnimationFinished()
}
+ } else {
+ // This should never happen, as either the controller or factory should always be
+ // defined. This final call is for safety in case something goes wrong.
+ Log.wtf(TAG, "initAndRun with neither a controller nor factory")
+ finishedCallback?.onAnimationFinished()
+ }
+ }
+
+ /** Tries to start the animation on the main thread and returns whether it succeeded. */
+ @BinderThread
+ private fun startAnimation(performAnimation: (AnimationDelegate) -> Unit): Boolean {
+ val delegate = delegate
+ return if (delegate != null) {
+ mainExecutor.execute { performAnimation(delegate) }
+ true
+ } else {
+ // Animation started too late and timed out already.
+ Log.i(TAG, "startAnimation called after completion")
+ false
}
}
@BinderThread
override fun onAnimationCancelled() {
val delegate = delegate
- mainExecutor.execute {
- delegate ?: Log.wtf(TAG, "onAnimationCancelled called after completion")
- delegate?.onAnimationCancelled()
+ if (delegate != null) {
+ mainExecutor.execute { delegate.onAnimationCancelled() }
+ } else {
+ Log.wtf(TAG, "onAnimationCancelled called after completion")
}
}
+ /**
+ * Posts the default animation timeouts. Since this only applies to ephemeral launches, this
+ * method is a no-op if [controller] is not defined.
+ */
@VisibleForTesting
@UiThread
fun postTimeouts() {
- maybeSetUp()
+ controller?.let { maybeSetUp(it) }
delegate?.postTimeouts()
}
@AnyThread
- private fun maybeSetUp() {
- if (controllerFactory == null || delegate != null) return
- createDelegate()
+ private fun maybeSetUp(controller: Controller) {
+ if (delegate != null) return
+ createDelegate(controller)
}
@AnyThread
- private fun createDelegate() {
- var controller = controller
- val factory = controllerFactory
- if (controller == null && factory == null) return
+ private suspend fun setUp(createController: suspend () -> Controller) {
+ val controller = createController()
+ createDelegate(controller)
+ }
- controller = controller ?: factory!!.invoke()
+ @AnyThread
+ private fun createDelegate(controller: Controller) {
delegate =
AnimationDelegate(
mainExecutor,
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
index 103a9b5cf5f4..c5d2802c8941 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/ExpandableController.kt
@@ -319,7 +319,7 @@ internal class ExpandableControllerImpl(
override fun onTransitionAnimationStart(isExpandingFullyAbove: Boolean) {
delegate.onTransitionAnimationStart(isExpandingFullyAbove)
- overlay.value = composeViewRoot.rootView.overlay as ViewGroupOverlay
+ overlay.value = transitionContainer.overlay as ViewGroupOverlay
cujType?.let { InteractionJankMonitor.getInstance().begin(composeViewRoot, it) }
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
index 195b060932eb..20f1cdf4242c 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/gesture/NestedDraggable.kt
@@ -296,13 +296,6 @@ private class NestedDraggableNode(
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
- check(down.position == lastFirstDown) {
- "Position from detectDrags() is not the same as position in trackDownPosition()"
- }
- check(pointersDown.size == 1 && pointersDown.keys.first() == down.id) {
- "pointersDown should only contain $down but it contains $pointersDown"
- }
-
var overSlop = 0f
val onTouchSlopReached = { change: PointerInputChange, over: Float ->
if (draggable.shouldStartDrag(change)) {
@@ -342,7 +335,12 @@ private class NestedDraggableNode(
check(pointersDown.size > 0) { "pointersDown is empty" }
val controller =
- draggable.onDragStarted(down.position, sign, pointersDown.size, drag.type)
+ draggable.onDragStarted(
+ down.position,
+ sign,
+ pointersDown.size.coerceAtLeast(1),
+ drag.type,
+ )
if (overSlop != 0f) {
onDrag(controller, drag, overSlop, velocityTracker)
}
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 3ffbabb09710..4a4607b6e8fc 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
@@ -159,8 +159,7 @@ fun CommunalContainer(
content: CommunalContent,
) {
val coroutineScope = rememberCoroutineScope()
- val currentSceneKey: SceneKey by
- viewModel.currentScene.collectAsStateWithLifecycle(CommunalScenes.Blank)
+ val currentSceneKey: SceneKey by viewModel.currentScene.collectAsStateWithLifecycle()
val touchesAllowed by viewModel.touchesAllowed.collectAsStateWithLifecycle()
val backgroundType by
viewModel.communalBackground.collectAsStateWithLifecycle(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index d3417022565b..fa5e8aceef1d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -109,15 +109,22 @@ constructor(
}
if (isShadeLayoutWide && !isBypassEnabled) {
with(notificationSection) {
- Notifications(
- areNotificationsVisible = areNotificationsVisible,
- isShadeLayoutWide = true,
- burnInParams = null,
- modifier =
- Modifier.fillMaxWidth(0.5f)
- .fillMaxHeight()
- .align(alignment = Alignment.TopEnd),
- )
+ Box(modifier = Modifier.fillMaxHeight()) {
+ AodPromotedNotificationArea(
+ modifier =
+ Modifier.fillMaxWidth(0.5f)
+ .align(alignment = Alignment.TopStart)
+ )
+ Notifications(
+ areNotificationsVisible = areNotificationsVisible,
+ isShadeLayoutWide = true,
+ burnInParams = null,
+ modifier =
+ Modifier.fillMaxWidth(0.5f)
+ .fillMaxHeight()
+ .align(alignment = Alignment.TopEnd),
+ )
+ }
}
}
}
@@ -142,9 +149,18 @@ constructor(
}
} else {
Column {
- AodPromotedNotificationArea()
+ if (!isShadeLayoutWide) {
+ AodPromotedNotificationArea()
+ }
AodNotificationIcons(
- modifier = Modifier.padding(start = aodIconPadding)
+ modifier =
+ Modifier.padding(
+ top =
+ dimensionResource(
+ R.dimen.keyguard_status_view_bottom_margin
+ ),
+ start = aodIconPadding,
+ )
)
}
}
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 931134795a53..26e7524f4fa8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt
@@ -29,7 +29,6 @@ import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
@@ -44,10 +43,7 @@ import com.android.systemui.scene.ui.composable.Overlay
import com.android.systemui.shade.ui.composable.OverlayShade
import com.android.systemui.shade.ui.composable.OverlayShadeHeader
import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
-import com.android.systemui.statusbar.phone.ui.StatusBarIconController
-import com.android.systemui.statusbar.phone.ui.TintedIconManager
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -58,11 +54,6 @@ class NotificationsShadeOverlay
constructor(
private val actionsViewModelFactory: NotificationsShadeOverlayActionsViewModel.Factory,
private val contentViewModelFactory: NotificationsShadeOverlayContentViewModel.Factory,
- private val tintedIconManagerFactory: TintedIconManager.Factory,
- private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
- private val statusBarIconController: StatusBarIconController,
- private val notificationIconContainerStatusBarViewBinder:
- NotificationIconContainerStatusBarViewBinder,
private val shadeSession: SaveableSession,
private val stackScrollView: Lazy<NotificationScrollView>,
private val clockSection: DefaultClockSection,
@@ -94,18 +85,16 @@ constructor(
}
OverlayShade(
- isShadeLayoutWide = viewModel.isShadeLayoutWide,
panelAlignment = Alignment.TopStart,
modifier = modifier,
onScrimClicked = viewModel::onScrimClicked,
header = {
+ val headerViewModel =
+ rememberViewModel("NotificationsShadeOverlayHeader") {
+ viewModel.shadeHeaderViewModelFactory.create()
+ }
OverlayShadeHeader(
- viewModelFactory = viewModel.shadeHeaderViewModelFactory,
- createTintedIconManager = tintedIconManagerFactory::create,
- createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
- statusBarIconController = statusBarIconController,
- notificationIconContainerStatusBarViewBinder =
- notificationIconContainerStatusBarViewBinder,
+ viewModel = headerViewModel,
modifier =
Modifier.element(NotificationsShade.Elements.StatusBar)
.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
@@ -114,7 +103,7 @@ constructor(
) {
Box {
Column {
- if (viewModel.showHeader) {
+ if (viewModel.showClock) {
val burnIn = rememberBurnIn(clockInteractor)
with(clockSection) {
@@ -140,8 +129,7 @@ constructor(
modifier = Modifier.fillMaxWidth(),
)
}
- // Communicates the bottom position of the drawable area within the shade to
- // NSSL.
+ // Communicates the bottom position of the drawable area within the shade to NSSL.
NotificationStackCutoffGuideline(
stackScrollView = stackScrollView.get(),
viewModel = placeholderViewModel,
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 4bfbb3a908fa..62a8cc5a7fe3 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
@@ -16,7 +16,6 @@
package com.android.systemui.qs.ui.composable
-import android.view.ViewGroup
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
@@ -76,7 +75,6 @@ import com.android.compose.animation.scene.animateSceneFloatAsState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.thenIf
import com.android.compose.windowsizeclass.LocalWindowSizeClass
-import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
import com.android.systemui.compose.modifiers.sysuiResTag
@@ -100,15 +98,14 @@ import com.android.systemui.res.R
import com.android.systemui.scene.session.ui.composable.SaveableSession
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.Scene
+import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel
import com.android.systemui.shade.ui.composable.CollapsedShadeHeader
import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
import com.android.systemui.shade.ui.composable.Shade
import com.android.systemui.shade.ui.composable.ShadeHeader
+import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
-import com.android.systemui.statusbar.phone.StatusBarLocation
-import com.android.systemui.statusbar.phone.ui.StatusBarIconController
-import com.android.systemui.statusbar.phone.ui.TintedIconManager
import dagger.Lazy
import javax.inject.Inject
import javax.inject.Named
@@ -125,9 +122,6 @@ constructor(
private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
private val actionsViewModelFactory: QuickSettingsUserActionsViewModel.Factory,
private val contentViewModelFactory: QuickSettingsSceneContentViewModel.Factory,
- private val tintedIconManagerFactory: TintedIconManager.Factory,
- private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
- private val statusBarIconController: StatusBarIconController,
private val mediaCarouselController: MediaCarouselController,
@Named(MediaModule.QS_PANEL) private val mediaHost: MediaHost,
) : ExclusiveActivatable(), Scene {
@@ -145,16 +139,26 @@ constructor(
@Composable
override fun ContentScope.Content(modifier: Modifier) {
+ val viewModel =
+ rememberViewModel("QuickSettingsScene-viewModel") { contentViewModelFactory.create() }
+ val headerViewModel =
+ rememberViewModel("QuickSettingsScene-headerViewModel") {
+ viewModel.shadeHeaderViewModelFactory.create()
+ }
+ val brightnessMirrorViewModel =
+ rememberViewModel("QuickSettingsScene-brightnessMirrorViewModel") {
+ viewModel.brightnessMirrorViewModelFactory.create()
+ }
+ val notificationsPlaceholderViewModel =
+ rememberViewModel("QuickSettingsScene-notifPlaceholderViewModel") {
+ notificationsPlaceholderViewModelFactory.create()
+ }
QuickSettingsScene(
notificationStackScrollView = notificationStackScrollView.get(),
- viewModelFactory = contentViewModelFactory,
- notificationsPlaceholderViewModel =
- rememberViewModel("QuickSettingsScene-notifPlaceholderViewModel") {
- notificationsPlaceholderViewModelFactory.create()
- },
- createTintedIconManager = tintedIconManagerFactory::create,
- createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
- statusBarIconController = statusBarIconController,
+ viewModel = viewModel,
+ headerViewModel = headerViewModel,
+ brightnessMirrorViewModel = brightnessMirrorViewModel,
+ notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
mediaCarouselController = mediaCarouselController,
mediaHost = mediaHost,
modifier = modifier,
@@ -166,23 +170,16 @@ constructor(
@Composable
private fun ContentScope.QuickSettingsScene(
notificationStackScrollView: NotificationScrollView,
- viewModelFactory: QuickSettingsSceneContentViewModel.Factory,
+ viewModel: QuickSettingsSceneContentViewModel,
+ headerViewModel: ShadeHeaderViewModel,
+ brightnessMirrorViewModel: BrightnessMirrorViewModel,
notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
- createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
- createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
- statusBarIconController: StatusBarIconController,
mediaCarouselController: MediaCarouselController,
mediaHost: MediaHost,
modifier: Modifier = Modifier,
shadeSession: SaveableSession,
) {
val cutoutLocation = LocalDisplayCutout.current.location
-
- val viewModel = rememberViewModel("QuickSettingsScene-viewModel") { viewModelFactory.create() }
- val brightnessMirrorViewModel =
- rememberViewModel("QuickSettingsScene-brightnessMirrorViewModel") {
- viewModel.brightnessMirrorViewModelFactory.create()
- }
val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle()
val contentAlpha by
animateFloatAsState(
@@ -222,8 +219,7 @@ private fun ContentScope.QuickSettingsScene(
.graphicsLayer { alpha = contentAlpha }
.thenIf(shouldPunchHoleBehindScrim) {
// Render the scene to an offscreen buffer so that BlendMode.DstOut only clears
- // this
- // scene (and not the one under it) during a scene transition.
+ // this scene (and not the one under it) during a scene transition.
Modifier.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
}
.thenIf(cutoutLocation != CutoutLocation.CENTER) { Modifier.displayCutoutPadding() }
@@ -348,21 +344,11 @@ private fun ContentScope.QuickSettingsScene(
fadeOut(tween(customizingAnimationDuration)),
) {
ExpandedShadeHeader(
- viewModelFactory = viewModel.shadeHeaderViewModelFactory,
- createTintedIconManager = createTintedIconManager,
- createBatteryMeterViewController =
- createBatteryMeterViewController,
- statusBarIconController = statusBarIconController,
+ viewModel = headerViewModel,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
- else ->
- CollapsedShadeHeader(
- viewModelFactory = viewModel.shadeHeaderViewModelFactory,
- createTintedIconManager = createTintedIconManager,
- createBatteryMeterViewController = createBatteryMeterViewController,
- statusBarIconController = statusBarIconController,
- )
+ else -> CollapsedShadeHeader(viewModel = headerViewModel)
}
Spacer(modifier = Modifier.height(16.dp))
// This view has its own horizontal padding
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
index 3ec14a23421c..2fa370458ab0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt
@@ -45,7 +45,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
@@ -67,13 +66,10 @@ import com.android.systemui.shade.ui.composable.OverlayShade
import com.android.systemui.shade.ui.composable.OverlayShadeHeader
import com.android.systemui.shade.ui.composable.QuickSettingsOverlayHeader
import com.android.systemui.shade.ui.composable.SingleShadeMeasurePolicy
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
-import com.android.systemui.statusbar.phone.ui.StatusBarIconController
-import com.android.systemui.statusbar.phone.ui.TintedIconManager
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -84,11 +80,7 @@ class QuickSettingsShadeOverlay
constructor(
private val actionsViewModelFactory: QuickSettingsShadeOverlayActionsViewModel.Factory,
private val contentViewModelFactory: QuickSettingsShadeOverlayContentViewModel.Factory,
- private val tintedIconManagerFactory: TintedIconManager.Factory,
- private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
- private val statusBarIconController: StatusBarIconController,
- private val notificationIconContainerStatusBarViewBinder:
- NotificationIconContainerStatusBarViewBinder,
+ private val quickSettingsContainerViewModelFactory: QuickSettingsContainerViewModel.Factory,
private val notificationStackScrollView: Lazy<NotificationScrollView>,
private val notificationsPlaceholderViewModelFactory: NotificationsPlaceholderViewModel.Factory,
) : Overlay {
@@ -107,35 +99,35 @@ constructor(
@Composable
override fun ContentScope.Content(modifier: Modifier) {
- val viewModel =
- rememberViewModel("QuickSettingsShadeOverlay") { contentViewModelFactory.create() }
+ val contentViewModel =
+ rememberViewModel("QuickSettingsShadeOverlayContent") {
+ contentViewModelFactory.create()
+ }
+ val quickSettingsContainerViewModel =
+ rememberViewModel("QuickSettingsShadeOverlayContainer") {
+ // TODO(b/393054014): Add support for brightness mirroring.
+ quickSettingsContainerViewModelFactory.create(supportsBrightnessMirroring = false)
+ }
val panelCornerRadius =
with(LocalDensity.current) { OverlayShade.Dimensions.PanelCornerRadius.toPx().toInt() }
- // set the bounds to null when the QuickSettings overlay disappears
- DisposableEffect(Unit) { onDispose { viewModel.onPanelShapeChanged(null) } }
+ // Set the bounds to null when the QuickSettings overlay disappears.
+ DisposableEffect(Unit) { onDispose { contentViewModel.onPanelShapeChanged(null) } }
Box(modifier = modifier) {
SnoozeableHeadsUpNotificationSpace(
stackScrollView = notificationStackScrollView.get(),
viewModel =
- rememberViewModel("QuickSettingsShadeOverlay") {
+ rememberViewModel("QuickSettingsShadeOverlayPlaceholder") {
notificationsPlaceholderViewModelFactory.create()
},
)
OverlayShade(
- isShadeLayoutWide = viewModel.isShadeLayoutWide,
panelAlignment = Alignment.TopEnd,
- onScrimClicked = viewModel::onScrimClicked,
+ onScrimClicked = contentViewModel::onScrimClicked,
header = {
OverlayShadeHeader(
- viewModelFactory = viewModel.shadeHeaderViewModelFactory,
- createTintedIconManager = tintedIconManagerFactory::create,
- createBatteryMeterViewController =
- batteryMeterViewControllerFactory::create,
- statusBarIconController = statusBarIconController,
- notificationIconContainerStatusBarViewBinder =
- notificationIconContainerStatusBarViewBinder,
+ viewModel = quickSettingsContainerViewModel.shadeHeaderViewModel,
modifier =
Modifier.element(NotificationsShade.Elements.StatusBar)
.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
@@ -143,7 +135,7 @@ constructor(
},
) {
ShadeBody(
- viewModel = viewModel.quickSettingsContainerViewModel,
+ viewModel = quickSettingsContainerViewModel,
modifier =
Modifier.onPlaced { coordinates ->
val boundsInWindow = coordinates.boundsInWindow()
@@ -160,14 +152,12 @@ constructor(
topRadius = 0,
bottomRadius = panelCornerRadius,
)
- viewModel.onPanelShapeChanged(shape)
+ contentViewModel.onPanelShapeChanged(shape)
},
header = {
- if (viewModel.isShadeLayoutWide) {
+ if (quickSettingsContainerViewModel.showHeader) {
QuickSettingsOverlayHeader(
- viewModelFactory = viewModel.shadeHeaderViewModelFactory,
- createBatteryMeterViewController =
- batteryMeterViewControllerFactory::create,
+ viewModel = quickSettingsContainerViewModel.shadeHeaderViewModel,
modifier =
Modifier.padding(top = QuickSettingsShade.Dimensions.Padding),
)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/ribbon/ui/composable/Ribbon.kt b/packages/SystemUI/compose/features/src/com/android/systemui/ribbon/ui/composable/Ribbon.kt
index daa15929b9ce..377bebc404e7 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/ribbon/ui/composable/Ribbon.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/ribbon/ui/composable/Ribbon.kt
@@ -20,8 +20,12 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.layout.layout
import com.android.compose.modifiers.thenIf
import kotlin.math.PI
@@ -39,6 +43,9 @@ import kotlin.math.tan
* The background color of the strip can be modified by passing a value to the [backgroundColor] or
* `null` to remove the strip background.
*
+ * The [colorSaturation] is a function that returns that amount of color saturation to apply to the
+ * entire ribbon. If it's `1`, the full color will be used, if it's `0` it will be greyscale.
+ *
* Note: this function assumes that it's been placed at the bottom right of its parent by its
* caller. It's the caller's responsibility to meet that assumption by actually placing this
* composable element at the bottom right.
@@ -49,6 +56,7 @@ fun BottomRightCornerRibbon(
modifier: Modifier = Modifier,
degrees: Int = 45,
alpha: Float = 0.6f,
+ colorSaturation: () -> Float = { 1f },
backgroundColor: Color? = Color.Red,
) {
check(degrees in 1..89)
@@ -73,6 +81,22 @@ fun BottomRightCornerRibbon(
translationY = (h - w * sine + h * cosine) / 2f
rotationZ = 360f - degrees
}
+ .drawWithCache {
+ val layer =
+ obtainGraphicsLayer().apply {
+ record {
+ colorFilter =
+ ColorFilter.colorMatrix(
+ colorMatrix =
+ ColorMatrix().apply {
+ setToSaturation(colorSaturation())
+ }
+ )
+ drawContent()
+ }
+ }
+ onDrawWithContent { drawLayer(layer) }
+ }
.thenIf(backgroundColor != null) { Modifier.background(backgroundColor!!) }
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
@@ -87,6 +111,6 @@ fun BottomRightCornerRibbon(
) {
placeable.place(leftPadding, 0)
}
- }
+ },
)
}
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 6c0c5c7e49b9..40e3000ee8a7 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
@@ -232,6 +232,7 @@ fun SceneContainer(
BottomRightCornerRibbon(
content = { Text(text = "flexi\uD83E\uDD43", color = Color.White) },
+ colorSaturation = { viewModel.ribbonColorSaturation },
modifier = Modifier.align(Alignment.BottomEnd),
)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
index fc59d40ec443..3d2d7c37ce48 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
@@ -58,29 +58,31 @@ import com.android.systemui.res.R
/** Renders a lightweight shade UI container, as an overlay. */
@Composable
fun ContentScope.OverlayShade(
- isShadeLayoutWide: Boolean,
panelAlignment: Alignment,
onScrimClicked: () -> Unit,
modifier: Modifier = Modifier,
header: @Composable () -> Unit,
content: @Composable () -> Unit,
) {
+ val isFullWidth = isFullWidthShade()
Box(modifier) {
Scrim(onClicked = onScrimClicked)
- Box(modifier = Modifier.fillMaxSize().panelPadding(), contentAlignment = panelAlignment) {
+ Box(
+ modifier = Modifier.fillMaxSize().panelContainerPadding(isFullWidth),
+ contentAlignment = panelAlignment,
+ ) {
Panel(
- isShadeLayoutWide = isShadeLayoutWide,
modifier =
Modifier.overscroll(verticalOverscrollEffect)
.element(OverlayShade.Elements.Panel)
- .panelSize(),
- header = header,
+ .panelWidth(isFullWidth),
+ header = header.takeIf { isFullWidth },
content = content,
)
}
- if (isShadeLayoutWide) {
+ if (!isFullWidth) {
header()
}
}
@@ -100,9 +102,8 @@ private fun ContentScope.Scrim(onClicked: () -> Unit, modifier: Modifier = Modif
@Composable
private fun ContentScope.Panel(
- isShadeLayoutWide: Boolean,
modifier: Modifier = Modifier,
- header: @Composable () -> Unit,
+ header: (@Composable () -> Unit)?,
content: @Composable () -> Unit,
) {
Box(modifier = modifier.clip(OverlayShade.Shapes.RoundedCornerPanel)) {
@@ -117,9 +118,7 @@ private fun ContentScope.Panel(
)
Column {
- if (!isShadeLayoutWide) {
- header()
- }
+ header?.invoke()
// This content is intentionally rendered as a separate element from the background in
// order to allow for more flexibility when defining transitions.
@@ -129,14 +128,12 @@ private fun ContentScope.Panel(
}
@Composable
-private fun Modifier.panelSize(): Modifier {
- return this.then(
- if (isFullWidthShade()) {
- Modifier.fillMaxWidth()
- } else {
- Modifier.width(dimensionResource(id = R.dimen.shade_panel_width))
- }
- )
+private fun Modifier.panelWidth(isFullWidthPanel: Boolean): Modifier {
+ return if (isFullWidthPanel) {
+ fillMaxWidth()
+ } else {
+ width(dimensionResource(id = R.dimen.shade_panel_width))
+ }
}
@Composable
@@ -146,27 +143,23 @@ internal fun isFullWidthShade(): Boolean {
}
@Composable
-private fun Modifier.panelPadding(): Modifier {
- val widthSizeClass = LocalWindowSizeClass.current.widthSizeClass
+private fun Modifier.panelContainerPadding(isFullWidthPanel: Boolean): Modifier {
+ if (isFullWidthPanel) {
+ return this
+ }
val systemBars = WindowInsets.systemBarsIgnoringVisibility
val displayCutout = WindowInsets.displayCutout
val waterfall = WindowInsets.waterfall
val horizontalPadding =
PaddingValues(horizontal = dimensionResource(id = R.dimen.shade_panel_margin_horizontal))
-
- val combinedPadding =
+ return padding(
combinePaddings(
systemBars.asPaddingValues(),
displayCutout.asPaddingValues(),
waterfall.asPaddingValues(),
horizontalPadding,
)
-
- return if (widthSizeClass == WindowWidthSizeClass.Compact) {
- padding(bottom = combinedPadding.calculateBottomPadding())
- } else {
- padding(combinedPadding)
- }
+ )
}
/** Creates a union of [paddingValues] by using the max padding of each edge. */
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index c5d28adce601..02de78bc84ce 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -74,23 +74,20 @@ import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.compose.modifiers.sysuiResTag
-import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.chipBackground
import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.chipHighlighted
import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.onScrimDim
import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.CollapsedHeight
import com.android.systemui.shade.ui.composable.ShadeHeader.Values.ClockScale
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
+import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight
import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder
import com.android.systemui.statusbar.phone.NotificationIconContainer
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.phone.StatusIconContainer
-import com.android.systemui.statusbar.phone.ui.StatusBarIconController
-import com.android.systemui.statusbar.phone.ui.TintedIconManager
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
import com.android.systemui.statusbar.policy.Clock
@@ -137,14 +134,9 @@ object ShadeHeader {
/** The status bar that appears above the Shade scene on small screens */
@Composable
fun ContentScope.CollapsedShadeHeader(
- viewModelFactory: ShadeHeaderViewModel.Factory,
- createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
- createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
- statusBarIconController: StatusBarIconController,
+ viewModel: ShadeHeaderViewModel,
modifier: Modifier = Modifier,
) {
- val viewModel = rememberViewModel("CollapsedShadeHeader") { viewModelFactory.create() }
-
val cutoutLocation = LocalDisplayCutout.current.location
val horizontalPadding =
max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)
@@ -157,12 +149,14 @@ fun ContentScope.CollapsedShadeHeader(
}
}
+ val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle()
+ val shorterDateText by viewModel.shorterDateText.collectAsStateWithLifecycle()
+
val isShadeLayoutWide = viewModel.isShadeLayoutWide
val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
- // This layout assumes it is globally positioned at (0, 0) and is the
- // same size as the screen.
+ // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen.
CutoutAwareShadeHeader(
modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root),
startContent = {
@@ -171,9 +165,11 @@ fun ContentScope.CollapsedShadeHeader(
horizontalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.padding(horizontal = horizontalPadding),
) {
- Clock(scale = 1f, viewModel = viewModel)
+ Clock(scale = 1f, onClick = viewModel::onClockClicked)
VariableDayDate(
- viewModel = viewModel,
+ longerDateText = longerDateText,
+ shorterDateText = shorterDateText,
+ chipHighlight = viewModel.notificationsChipHighlight,
modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentStart),
)
}
@@ -202,17 +198,17 @@ fun ContentScope.CollapsedShadeHeader(
if (isShadeLayoutWide) {
ShadeCarrierGroup(viewModel = viewModel)
}
- SystemIconChip(viewModel = viewModel, isClickable = isShadeLayoutWide) {
+ SystemIconChip(
+ onClick = viewModel::onSystemIconChipClicked.takeIf { isShadeLayoutWide }
+ ) {
StatusIcons(
viewModel = viewModel,
- createTintedIconManager = createTintedIconManager,
- statusBarIconController = statusBarIconController,
useExpandedFormat = useExpandedTextFormat,
modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false),
)
BatteryIcon(
- viewModel = viewModel,
- createBatteryMeterViewController = createBatteryMeterViewController,
+ createBatteryMeterViewController =
+ viewModel.createBatteryMeterViewController,
useExpandedFormat = useExpandedTextFormat,
modifier = Modifier.padding(vertical = 8.dp),
)
@@ -226,18 +222,15 @@ fun ContentScope.CollapsedShadeHeader(
/** The status bar that appears above the Quick Settings scene on small screens */
@Composable
fun ContentScope.ExpandedShadeHeader(
- viewModelFactory: ShadeHeaderViewModel.Factory,
- createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
- createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
- statusBarIconController: StatusBarIconController,
+ viewModel: ShadeHeaderViewModel,
modifier: Modifier = Modifier,
) {
- val viewModel = rememberViewModel("ExpandedShadeHeader") { viewModelFactory.create() }
-
val useExpandedFormat by remember {
derivedStateOf { shouldUseExpandedFormat(layoutState.transitionState) }
}
+ val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle()
+ val shorterDateText by viewModel.shorterDateText.collectAsStateWithLifecycle()
val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
Box(modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root)) {
@@ -256,7 +249,7 @@ fun ContentScope.ExpandedShadeHeader(
Box {
Clock(
scale = 2.57f,
- viewModel = viewModel,
+ onClick = viewModel::onClockClicked,
modifier = Modifier.align(Alignment.CenterStart),
)
}
@@ -275,20 +268,23 @@ fun ContentScope.ExpandedShadeHeader(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.element(ShadeHeader.Elements.ExpandedContent),
) {
- VariableDayDate(viewModel = viewModel, modifier = Modifier.widthIn(max = 90.dp))
+ VariableDayDate(
+ longerDateText = longerDateText,
+ shorterDateText = shorterDateText,
+ chipHighlight = viewModel.notificationsChipHighlight,
+ modifier = Modifier.widthIn(max = 90.dp),
+ )
Spacer(modifier = Modifier.weight(1f))
- SystemIconChip(viewModel = viewModel) {
+ SystemIconChip {
StatusIcons(
viewModel = viewModel,
- createTintedIconManager = createTintedIconManager,
- statusBarIconController = statusBarIconController,
useExpandedFormat = useExpandedFormat,
modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false),
)
BatteryIcon(
- viewModel = viewModel,
useExpandedFormat = useExpandedFormat,
- createBatteryMeterViewController = createBatteryMeterViewController,
+ createBatteryMeterViewController =
+ viewModel.createBatteryMeterViewController,
)
}
}
@@ -302,15 +298,9 @@ fun ContentScope.ExpandedShadeHeader(
*/
@Composable
fun ContentScope.OverlayShadeHeader(
- viewModelFactory: ShadeHeaderViewModel.Factory,
- createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
- createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
- statusBarIconController: StatusBarIconController,
- notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder,
+ viewModel: ShadeHeaderViewModel,
modifier: Modifier = Modifier,
) {
- val viewModel = rememberViewModel("OverlayShadeHeader") { viewModelFactory.create() }
-
val horizontalPadding =
max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)
@@ -318,8 +308,7 @@ fun ContentScope.OverlayShadeHeader(
val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
- // This layout assumes it is globally positioned at (0, 0) and is the
- // same size as the screen.
+ // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen.
CutoutAwareShadeHeader(
modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root),
startContent = {
@@ -330,21 +319,32 @@ fun ContentScope.OverlayShadeHeader(
if (isShadeLayoutWide) {
Clock(
scale = 1f,
- viewModel = viewModel,
+ onClick = viewModel::onClockClicked,
modifier = Modifier.padding(horizontal = 4.dp),
)
Spacer(modifier = Modifier.width(5.dp))
}
- NotificationIconChip(viewModel = viewModel) {
+ val chipHighlight = viewModel.notificationsChipHighlight
+ NotificationIconChip(
+ chipHighlight = chipHighlight,
+ onClick = viewModel::onNotificationIconChipClicked,
+ ) {
if (isShadeLayoutWide) {
NotificationIcons(
- viewModel = viewModel,
+ chipHighlight = chipHighlight,
notificationIconContainerStatusBarViewBinder =
- notificationIconContainerStatusBarViewBinder,
+ viewModel.notificationIconContainerStatusBarViewBinder,
modifier = Modifier.width(IntrinsicSize.Min).height(20.dp),
)
} else {
- VariableDayDate(viewModel = viewModel)
+ val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle()
+ val shorterDateText by
+ viewModel.shorterDateText.collectAsStateWithLifecycle()
+ VariableDayDate(
+ longerDateText = longerDateText,
+ shorterDateText = shorterDateText,
+ chipHighlight = viewModel.notificationsChipHighlight,
+ )
}
}
}
@@ -355,20 +355,22 @@ fun ContentScope.OverlayShadeHeader(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = horizontalPadding),
) {
- SystemIconChip(viewModel = viewModel, isClickable = true, showBackground = true) {
+ val chipHighlight = viewModel.quickSettingsChipHighlight
+ SystemIconChip(
+ chipHighlight = chipHighlight,
+ onClick = viewModel::onSystemIconChipClicked,
+ ) {
StatusIcons(
viewModel = viewModel,
- createTintedIconManager = createTintedIconManager,
- statusBarIconController = statusBarIconController,
useExpandedFormat = false,
- highlightable = true,
modifier = Modifier.padding(end = 6.dp).weight(1f, fill = false),
+ chipHighlight = chipHighlight,
)
BatteryIcon(
- viewModel = viewModel,
- createBatteryMeterViewController = createBatteryMeterViewController,
+ createBatteryMeterViewController =
+ viewModel.createBatteryMeterViewController,
useExpandedFormat = false,
- highlightable = true,
+ chipHighlight = chipHighlight,
)
}
if (isPrivacyChipVisible) {
@@ -391,13 +393,7 @@ fun ContentScope.OverlayShadeHeader(
/** The header that appears at the top of the Quick Settings shade overlay. */
@Composable
-fun ContentScope.QuickSettingsOverlayHeader(
- viewModelFactory: ShadeHeaderViewModel.Factory,
- createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
- modifier: Modifier = Modifier,
-) {
- val viewModel = rememberViewModel("QuickSettingsOverlayHeader") { viewModelFactory.create() }
-
+fun QuickSettingsOverlayHeader(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
@@ -405,8 +401,7 @@ fun ContentScope.QuickSettingsOverlayHeader(
) {
ShadeCarrierGroup(viewModel = viewModel)
BatteryIcon(
- viewModel = viewModel,
- createBatteryMeterViewController = createBatteryMeterViewController,
+ createBatteryMeterViewController = viewModel.createBatteryMeterViewController,
useExpandedFormat = true,
)
}
@@ -468,11 +463,7 @@ private fun CutoutAwareShadeHeader(
}
@Composable
-private fun ContentScope.Clock(
- scale: Float,
- viewModel: ShadeHeaderViewModel,
- modifier: Modifier = Modifier,
-) {
+private fun ContentScope.Clock(scale: Float, onClick: () -> Unit, modifier: Modifier = Modifier) {
val layoutDirection = LocalLayoutDirection.current
Element(key = ShadeHeader.Elements.Clock, modifier = modifier) {
@@ -500,18 +491,17 @@ private fun ContentScope.Clock(
0.5f,
)
}
- .clickable { viewModel.onClockClicked() },
+ .clickable { onClick() },
)
}
}
@Composable
private fun BatteryIcon(
- viewModel: ShadeHeaderViewModel,
createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
useExpandedFormat: Boolean,
- highlightable: Boolean = false,
modifier: Modifier = Modifier,
+ chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None,
) {
val localContext = LocalContext.current
val themedContext =
@@ -521,8 +511,6 @@ private fun BatteryIcon(
val inverseColor =
Utils.getColorAttrDefaultColor(themedContext, android.R.attr.textColorPrimaryInverse)
- val isHighlighted = viewModel.highlightQuickSettingsIcons
-
AndroidView(
factory = { context ->
val batteryIcon = BatteryMeterView(context, null)
@@ -544,18 +532,12 @@ private fun BatteryIcon(
// TODO(b/298525212): use MODE_ESTIMATE in collapsed view when the screen
// has no center cutout. See [QsBatteryModeController.getBatteryMode]
batteryIcon.setPercentShowMode(
- if (useExpandedFormat) {
- BatteryMeterView.MODE_ESTIMATE
- } else {
- BatteryMeterView.MODE_ON
- }
+ if (useExpandedFormat) BatteryMeterView.MODE_ESTIMATE else BatteryMeterView.MODE_ON
)
- if (highlightable) {
- if (isHighlighted) {
- batteryIcon.updateColors(primaryColor, inverseColor, inverseColor)
- } else {
- batteryIcon.updateColors(primaryColor, inverseColor, primaryColor)
- }
+ if (chipHighlight is HeaderChipHighlight.Strong) {
+ batteryIcon.updateColors(primaryColor, inverseColor, inverseColor)
+ } else if (chipHighlight is HeaderChipHighlight.Weak) {
+ batteryIcon.updateColors(primaryColor, inverseColor, primaryColor)
}
},
modifier = modifier,
@@ -590,14 +572,12 @@ private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifie
@Composable
private fun NotificationIcons(
- viewModel: ShadeHeaderViewModel,
+ chipHighlight: HeaderChipHighlight,
notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder,
modifier: Modifier = Modifier,
) {
val scope = rememberCoroutineScope()
- val isHighlighted = viewModel.highlightNotificationIcons
-
AndroidView(
factory = { context ->
NotificationIconContainer(context, null).also { view ->
@@ -610,7 +590,7 @@ private fun NotificationIcons(
}
}
},
- update = { it.setUseInverseOverrideIconColor(isHighlighted) },
+ update = { it.setUseInverseOverrideIconColor(chipHighlight is HeaderChipHighlight.Strong) },
modifier = modifier,
)
}
@@ -618,11 +598,9 @@ private fun NotificationIcons(
@Composable
private fun ContentScope.StatusIcons(
viewModel: ShadeHeaderViewModel,
- createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
- statusBarIconController: StatusBarIconController,
useExpandedFormat: Boolean,
- highlightable: Boolean = false,
modifier: Modifier = Modifier,
+ chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None,
) {
val localContext = LocalContext.current
val themedContext =
@@ -632,8 +610,6 @@ private fun ContentScope.StatusIcons(
val inverseColor =
Utils.getColorAttrDefaultColor(themedContext, android.R.attr.textColorPrimaryInverse)
- val isHighlighted = viewModel.highlightQuickSettingsIcons
-
val carrierIconSlots =
listOf(stringResource(id = com.android.internal.R.string.status_bar_mobile))
val cameraSlot = stringResource(id = com.android.internal.R.string.status_bar_camera)
@@ -648,12 +624,14 @@ private fun ContentScope.StatusIcons(
viewModel.isLocationIndicationEnabled.collectAsStateWithLifecycle()
val iconContainer = remember { StatusIconContainer(themedContext, null) }
- val iconManager = remember { createTintedIconManager(iconContainer, StatusBarLocation.QS) }
+ val iconManager = remember {
+ viewModel.createTintedIconManager(iconContainer, StatusBarLocation.QS)
+ }
AndroidView(
factory = { context ->
iconManager.setTint(primaryColor, inverseColor)
- statusBarIconController.addIconGroup(iconManager)
+ viewModel.statusBarIconController.addIconGroup(iconManager)
iconContainer
},
@@ -686,12 +664,10 @@ private fun ContentScope.StatusIcons(
iconContainer.removeIgnoredSlot(locationSlot)
}
- if (highlightable) {
- if (isHighlighted) {
- iconManager.setTint(inverseColor, primaryColor)
- } else {
- iconManager.setTint(primaryColor, inverseColor)
- }
+ if (chipHighlight is HeaderChipHighlight.Strong) {
+ iconManager.setTint(inverseColor, primaryColor)
+ } else if (chipHighlight is HeaderChipHighlight.Weak) {
+ iconManager.setTint(primaryColor, inverseColor)
}
},
modifier = modifier,
@@ -700,15 +676,12 @@ private fun ContentScope.StatusIcons(
@Composable
private fun NotificationIconChip(
- viewModel: ShadeHeaderViewModel,
+ chipHighlight: HeaderChipHighlight,
+ onClick: () -> Unit,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
- val backgroundColor =
- if (viewModel.highlightNotificationIcons) MaterialTheme.colorScheme.chipHighlighted
- else MaterialTheme.colorScheme.chipBackground
-
Box(modifier = modifier) {
Row(
modifier =
@@ -716,16 +689,15 @@ private fun NotificationIconChip(
.clickable(
interactionSource = interactionSource,
indication = null,
- onClick = { viewModel.onNotificationIconChipClicked() },
+ onClick = { onClick() },
)
- .thenIf(DualShade.isEnabled) {
- Modifier.graphicsLayer {
- shape = RoundedCornerShape(25.dp)
- clip = true
- }
- .background(backgroundColor)
- .padding(horizontal = 8.dp, vertical = 4.dp)
- }
+ .clip(RoundedCornerShape(25.dp))
+ .background(
+ if (chipHighlight is HeaderChipHighlight.Strong)
+ MaterialTheme.colorScheme.chipHighlighted
+ else MaterialTheme.colorScheme.chipBackground
+ )
+ .padding(horizontal = 8.dp, vertical = 4.dp)
) {
content()
}
@@ -734,10 +706,9 @@ private fun NotificationIconChip(
@Composable
private fun SystemIconChip(
- viewModel: ShadeHeaderViewModel,
- isClickable: Boolean = false,
- showBackground: Boolean = false,
modifier: Modifier = Modifier,
+ chipHighlight: HeaderChipHighlight = HeaderChipHighlight.None,
+ onClick: (() -> Unit)? = null,
content: @Composable RowScope.() -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
@@ -746,14 +717,14 @@ private fun SystemIconChip(
Modifier.clip(RoundedCornerShape(CollapsedHeight / 4))
.background(MaterialTheme.colorScheme.onScrimDim)
val backgroundColor =
- if (viewModel.highlightQuickSettingsIcons) MaterialTheme.colorScheme.chipHighlighted
+ if (chipHighlight is HeaderChipHighlight.Strong) MaterialTheme.colorScheme.chipHighlighted
else MaterialTheme.colorScheme.chipBackground
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
modifier
- .thenIf(showBackground) {
+ .thenIf(chipHighlight !is HeaderChipHighlight.None) {
Modifier.graphicsLayer {
shape = RoundedCornerShape(25.dp)
clip = true
@@ -761,11 +732,11 @@ private fun SystemIconChip(
.background(backgroundColor)
.padding(horizontal = 8.dp, vertical = 4.dp)
}
- .thenIf(isClickable) {
+ .thenIf(onClick != null) {
Modifier.clickable(
interactionSource = interactionSource,
indication = null,
- onClick = { viewModel.onSystemIconChipClicked() },
+ onClick = { onClick?.invoke() },
)
}
.thenIf(isHovered) { hoverModifier },
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index f829a0d6facf..5040490da8f6 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -101,6 +101,7 @@ import com.android.systemui.scene.session.ui.composable.SaveableSession
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.Scene
import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.shade.ui.viewmodel.ShadeSceneContentViewModel
import com.android.systemui.shade.ui.viewmodel.ShadeUserActionsViewModel
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
@@ -124,8 +125,6 @@ object Shade {
object Dimensions {
val HorizontalPadding = 16.dp
- val ScrimOverscrollLimit = 32.dp
- const val ScrimVisibilityThreshold = 5f
}
}
@@ -160,15 +159,22 @@ constructor(
override val userActions: Flow<Map<UserAction, UserActionResult>> = actionsViewModel.actions
@Composable
- override fun ContentScope.Content(modifier: Modifier) =
+ override fun ContentScope.Content(modifier: Modifier) {
+ val viewModel =
+ rememberViewModel("ShadeScene-viewModel") { contentViewModelFactory.create() }
+ val headerViewModel =
+ rememberViewModel("ShadeScene-headerViewModel") {
+ viewModel.shadeHeaderViewModelFactory.create()
+ }
+ val notificationsPlaceholderViewModel =
+ rememberViewModel("ShadeScene-notifPlaceholderViewModel") {
+ notificationsPlaceholderViewModelFactory.create()
+ }
ShadeScene(
notificationStackScrollView.get(),
- viewModel =
- rememberViewModel("ShadeScene-viewModel") { contentViewModelFactory.create() },
- notificationsPlaceholderViewModel =
- rememberViewModel("ShadeScene-notifPlaceholderViewModel") {
- notificationsPlaceholderViewModelFactory.create()
- },
+ viewModel = viewModel,
+ headerViewModel = headerViewModel,
+ notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
createTintedIconManager = tintedIconManagerFactory::create,
createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
statusBarIconController = statusBarIconController,
@@ -180,6 +186,7 @@ constructor(
usingCollapsedLandscapeMedia =
Utils.useCollapsedMediaInLandscape(LocalContext.current.resources),
)
+ }
init {
qqsMediaHost.expansion = EXPANDED
@@ -196,6 +203,7 @@ constructor(
private fun ContentScope.ShadeScene(
notificationStackScrollView: NotificationScrollView,
viewModel: ShadeSceneContentViewModel,
+ headerViewModel: ShadeHeaderViewModel,
notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
@@ -207,13 +215,13 @@ private fun ContentScope.ShadeScene(
shadeSession: SaveableSession,
usingCollapsedLandscapeMedia: Boolean,
) {
-
val shadeMode by viewModel.shadeMode.collectAsStateWithLifecycle()
when (shadeMode) {
is ShadeMode.Single ->
SingleShade(
notificationStackScrollView = notificationStackScrollView,
viewModel = viewModel,
+ headerViewModel = headerViewModel,
notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
createTintedIconManager = createTintedIconManager,
createBatteryMeterViewController = createBatteryMeterViewController,
@@ -228,10 +236,8 @@ private fun ContentScope.ShadeScene(
SplitShade(
notificationStackScrollView = notificationStackScrollView,
viewModel = viewModel,
+ headerViewModel = headerViewModel,
notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
- createTintedIconManager = createTintedIconManager,
- createBatteryMeterViewController = createBatteryMeterViewController,
- statusBarIconController = statusBarIconController,
mediaCarouselController = mediaCarouselController,
mediaHost = qsMediaHost,
modifier = modifier,
@@ -245,6 +251,7 @@ private fun ContentScope.ShadeScene(
private fun ContentScope.SingleShade(
notificationStackScrollView: NotificationScrollView,
viewModel: ShadeSceneContentViewModel,
+ headerViewModel: ShadeHeaderViewModel,
notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
@@ -332,10 +339,7 @@ private fun ContentScope.SingleShade(
},
content = {
CollapsedShadeHeader(
- viewModelFactory = viewModel.shadeHeaderViewModelFactory,
- createTintedIconManager = createTintedIconManager,
- createBatteryMeterViewController = createBatteryMeterViewController,
- statusBarIconController = statusBarIconController,
+ viewModel = headerViewModel,
modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
)
@@ -413,10 +417,8 @@ private fun ContentScope.SingleShade(
private fun ContentScope.SplitShade(
notificationStackScrollView: NotificationScrollView,
viewModel: ShadeSceneContentViewModel,
+ headerViewModel: ShadeHeaderViewModel,
notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
- createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
- createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
- statusBarIconController: StatusBarIconController,
mediaCarouselController: MediaCarouselController,
mediaHost: MediaHost,
modifier: Modifier = Modifier,
@@ -509,10 +511,7 @@ private fun ContentScope.SplitShade(
Column(modifier = Modifier.fillMaxSize()) {
CollapsedShadeHeader(
- viewModelFactory = viewModel.shadeHeaderViewModelFactory,
- createTintedIconManager = createTintedIconManager,
- createBatteryMeterViewController = createBatteryMeterViewController,
- statusBarIconController = statusBarIconController,
+ viewModel = headerViewModel,
modifier =
Modifier.then(brightnessMirrorShowingModifier)
.padding(horizontal = { unfoldTranslationXForStartSide.roundToInt() }),
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt
index 93eca86e15cf..64aada52626b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/VariableDayDate.kt
@@ -5,17 +5,19 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.theme.colorAttr
-import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
+import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight
@Composable
-fun VariableDayDate(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) {
- val longerText = viewModel.longerDateText.collectAsStateWithLifecycle()
- val shorterText = viewModel.shorterDateText.collectAsStateWithLifecycle()
-
+fun VariableDayDate(
+ longerDateText: String,
+ shorterDateText: String,
+ chipHighlight: HeaderChipHighlight,
+ modifier: Modifier = Modifier,
+) {
val textColor =
- if (viewModel.highlightNotificationIcons) colorAttr(android.R.attr.textColorPrimaryInverse)
+ if (chipHighlight is HeaderChipHighlight.Strong)
+ colorAttr(android.R.attr.textColorPrimaryInverse)
else colorAttr(android.R.attr.textColorPrimary)
Layout(
@@ -23,7 +25,7 @@ fun VariableDayDate(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifi
listOf(
{
Text(
- text = longerText.value,
+ text = longerDateText,
style = MaterialTheme.typography.bodyMedium,
color = textColor,
maxLines = 1,
@@ -31,7 +33,7 @@ fun VariableDayDate(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifi
},
{
Text(
- text = shorterText.value,
+ text = shorterDateText,
style = MaterialTheme.typography.bodyMedium,
color = textColor,
maxLines = 1,
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
index e2bbe0fef3c0..d7d8d28a71e0 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt
@@ -37,6 +37,7 @@ import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import com.android.systemui.shared.clocks.view.FlexClockView
import com.android.systemui.shared.clocks.view.HorizontalAlignment
+import com.android.systemui.shared.clocks.view.VerticalAlignment
import java.util.Locale
import java.util.TimeZone
import kotlin.math.max
@@ -255,7 +256,7 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock:
timespec = DigitalTimespec.TIME_FULL_FORMAT,
style = FontTextStyle(fontSizeScale = 0.98f),
aodStyle = FontTextStyle(),
- alignment = DigitalAlignment(HorizontalAlignment.LEFT, null),
+ alignment = DigitalAlignment(HorizontalAlignment.LEFT, VerticalAlignment.CENTER),
dateTimeFormat = "h:mm",
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index b8d4bb4b8e77..50762edc1179 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -16,7 +16,6 @@
package com.android.systemui.biometrics
import android.app.ActivityTaskManager
-import android.app.admin.DevicePolicyManager
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.hardware.biometrics.BiometricAuthenticator
@@ -43,6 +42,8 @@ import androidx.test.filters.SmallTest
import com.android.app.viewcapture.ViewCapture
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN
+import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN
import com.android.launcher3.icons.IconProvider
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository
@@ -432,8 +433,7 @@ open class AuthContainerViewTest : SysuiTestCase() {
.setMoreOptionsButtonListener(fakeExecutor) { _, _ -> isButtonClicked = true }
.build()
- val container =
- initializeFingerprintContainer(contentViewWithMoreOptionsButton = contentView)
+ val container = initializeFingerprintContainer()
waitForIdleSync()
@@ -488,8 +488,7 @@ open class AuthContainerViewTest : SysuiTestCase() {
.build()
val container =
initializeFingerprintContainer(
- authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL,
- contentViewWithMoreOptionsButton = contentView,
+ authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
waitForIdleSync()
@@ -500,8 +499,8 @@ open class AuthContainerViewTest : SysuiTestCase() {
@Test
fun testCredentialViewUsesEffectiveUserId() {
whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(200)
- whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(200)))
- .thenReturn(DevicePolicyManager.PASSWORD_QUALITY_SOMETHING)
+ whenever(lockPatternUtils.getCredentialTypeForUser(eq(200)))
+ .thenReturn(CREDENTIAL_TYPE_PATTERN)
val container =
initializeFingerprintContainer(
@@ -578,8 +577,7 @@ open class AuthContainerViewTest : SysuiTestCase() {
addToView: Boolean = true
): TestAuthContainerView {
whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(20)
- whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(20)))
- .thenReturn(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC)
+ whenever(lockPatternUtils.getCredentialTypeForUser(eq(20))).thenReturn(CREDENTIAL_TYPE_PIN)
// In the credential view, clicking on the background (to cancel authentication) is not
// valid. Thus, the listener should be null, and it should not be in the accessibility
@@ -599,7 +597,6 @@ open class AuthContainerViewTest : SysuiTestCase() {
authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK,
addToView: Boolean = true,
verticalListContentView: PromptVerticalListContentView? = null,
- contentViewWithMoreOptionsButton: PromptContentViewWithMoreOptionsButton? = null,
) =
initializeContainer(
TestAuthContainerView(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
index 58fe2c9cbe57..fde847897f72 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt
@@ -135,9 +135,9 @@ class CredentialInteractorImplTest : SysuiTestCase() {
private fun pinCredential(result: VerifyCredentialResponse, credentialOwner: Int = USER_ID) =
runTest {
val usedAttempts = 1
- whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID)))
+ whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(credentialOwner)))
.thenReturn(usedAttempts)
- whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt()))
+ whenever(lockPatternUtils.verifyCredential(any(), eq(credentialOwner), anyInt()))
.thenReturn(result)
whenever(lockPatternUtils.verifyTiedProfileChallenge(any(), eq(USER_ID), anyInt()))
.thenReturn(result)
@@ -170,7 +170,7 @@ class CredentialInteractorImplTest : SysuiTestCase() {
assertThat(successfulResult).isNotNull()
assertThat(successfulResult!!.hat).isEqualTo(result.gatekeeperHAT)
- verify(lockPatternUtils).userPresent(eq(USER_ID))
+ verify(lockPatternUtils).userPresent(eq(credentialOwner))
verify(lockPatternUtils)
.removeGatekeeperPasswordHandle(eq(result.gatekeeperPasswordHandle))
} else {
@@ -190,13 +190,13 @@ class CredentialInteractorImplTest : SysuiTestCase() {
.hasSize(statusList.size)
verify(lockPatternUtils)
- .setLockoutAttemptDeadline(eq(USER_ID), eq(result.timeout))
+ .setLockoutAttemptDeadline(eq(credentialOwner), eq(result.timeout))
} else { // failed
assertThat(failedResult.error)
.matches(Regex("(.*)try again(.*)", RegexOption.IGNORE_CASE).toPattern())
assertThat(statusList).isEmpty()
- verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID))
+ verify(lockPatternUtils).reportFailedPasswordAttempt(eq(credentialOwner))
}
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt
index 2d093bf1630b..f9e88341316e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelTest.kt
@@ -26,10 +26,8 @@ import com.android.systemui.brightness.domain.interactor.screenBrightnessInterac
import com.android.systemui.brightness.shared.model.GammaBrightness
import com.android.systemui.brightness.shared.model.LinearBrightness
import com.android.systemui.classifier.domain.interactor.falsingInteractor
-import com.android.systemui.common.shared.model.ContentDescription
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.common.shared.model.Text
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.graphics.imageLoader
import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
@@ -65,6 +63,7 @@ class BrightnessSliderViewModelTest : SysuiTestCase() {
falsingInteractor,
supportsMirroring = true,
brightnessWarningToast,
+ imageLoader,
)
}
}
@@ -162,20 +161,21 @@ class BrightnessSliderViewModelTest : SysuiTestCase() {
}
@Test
- fun label() {
- assertThat(underTest.label)
- .isEqualTo(Text.Resource(R.string.quick_settings_brightness_dialog_title))
- }
-
- @Test
fun icon() {
- assertThat(underTest.icon)
- .isEqualTo(
- Icon.Resource(
- R.drawable.ic_brightness_full,
- ContentDescription.Resource(underTest.label.res),
- )
- )
+ assertThat(BrightnessSliderViewModel.getIconForPercentage(0f))
+ .isEqualTo(R.drawable.ic_brightness_low)
+ assertThat(BrightnessSliderViewModel.getIconForPercentage(20f))
+ .isEqualTo(R.drawable.ic_brightness_low)
+ assertThat(BrightnessSliderViewModel.getIconForPercentage(20.1f))
+ .isEqualTo(R.drawable.ic_brightness_medium)
+ assertThat(BrightnessSliderViewModel.getIconForPercentage(50f))
+ .isEqualTo(R.drawable.ic_brightness_medium)
+ assertThat(BrightnessSliderViewModel.getIconForPercentage(79.9f))
+ .isEqualTo(R.drawable.ic_brightness_medium)
+ assertThat(BrightnessSliderViewModel.getIconForPercentage(80f))
+ .isEqualTo(R.drawable.ic_brightness_full)
+ assertThat(BrightnessSliderViewModel.getIconForPercentage(100f))
+ .isEqualTo(R.drawable.ic_brightness_full)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt
index fd0bf4dae198..293d32471713 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,34 +21,44 @@ import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.scene.shared.model.sceneDataSource
+import com.android.systemui.kosmos.backgroundScope
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.scene.shared.model.SceneDataSource
+import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
-class CommunalRepositoryImplTest : SysuiTestCase() {
+class CommunalSceneRepositoryImplTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
- private val underTest by lazy {
- CommunalSceneRepositoryImpl(
- kosmos.applicationCoroutineScope,
- kosmos.applicationCoroutineScope,
- kosmos.sceneDataSource,
- )
- }
+ private val delegator = mock<SceneDataSourceDelegator> {}
+
+ private val Kosmos.underTest by
+ Kosmos.Fixture {
+ CommunalSceneRepositoryImpl(
+ applicationScope = applicationCoroutineScope,
+ backgroundScope = backgroundScope,
+ sceneDataSource = delegator,
+ delegator = delegator,
+ )
+ }
@Test
fun transitionState_idleByDefault() =
- testScope.runTest {
+ kosmos.runTest {
val transitionState by collectLastValue(underTest.transitionState)
assertThat(transitionState)
.isEqualTo(ObservableTransitionState.Idle(CommunalScenes.Default))
@@ -56,7 +66,7 @@ class CommunalRepositoryImplTest : SysuiTestCase() {
@Test
fun transitionState_setTransitionState_returnsNewValue() =
- testScope.runTest {
+ kosmos.runTest {
val expectedSceneKey = CommunalScenes.Communal
underTest.setTransitionState(flowOf(ObservableTransitionState.Idle(expectedSceneKey)))
@@ -66,7 +76,7 @@ class CommunalRepositoryImplTest : SysuiTestCase() {
@Test
fun transitionState_setNullTransitionState_returnsDefaultValue() =
- testScope.runTest {
+ kosmos.runTest {
// Set a value for the transition state flow.
underTest.setTransitionState(
flowOf(ObservableTransitionState.Idle(CommunalScenes.Communal))
@@ -80,4 +90,18 @@ class CommunalRepositoryImplTest : SysuiTestCase() {
assertThat(transitionState)
.isEqualTo(ObservableTransitionState.Idle(CommunalScenes.Default))
}
+
+ @Test
+ fun showHubFromPowerButton() =
+ kosmos.runTest {
+ fakeKeyguardRepository.setKeyguardShowing(false)
+
+ underTest.showHubFromPowerButton()
+
+ argumentCaptor<SceneDataSource>().apply {
+ verify(delegator).setDelegate(capture())
+
+ assertThat(firstValue.currentScene.value).isEqualTo(CommunalScenes.Communal)
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
index eb1f1d9c52f4..5c983656225e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt
@@ -51,6 +51,7 @@ import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -78,6 +79,32 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT
setKeyguardFeaturesDisabled(PRIMARY_USER, KEYGUARD_DISABLE_FEATURES_NONE)
setKeyguardFeaturesDisabled(SECONDARY_USER, KEYGUARD_DISABLE_FEATURES_NONE)
setKeyguardFeaturesDisabled(WORK_PROFILE, KEYGUARD_DISABLE_FEATURES_NONE)
+
+ mContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault,
+ false,
+ )
+ mContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault,
+ false,
+ )
+ mContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault,
+ false,
+ )
+ }
+
+ @After
+ fun tearDown() {
+ mContext.orCreateTestableResources.removeOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault
+ )
+ mContext.orCreateTestableResources.removeOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault
+ )
+ mContext.orCreateTestableResources.removeOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault
+ )
}
@EnableFlags(FLAG_COMMUNAL_HUB)
@@ -334,6 +361,18 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT
}
@Test
+ fun whenToDream_charging_defaultValue() =
+ kosmos.runTest {
+ mContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault,
+ true,
+ )
+
+ val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER))
+ assertThat(whenToDreamState).isEqualTo(WhenToDream.WHILE_CHARGING)
+ }
+
+ @Test
fun whenToDream_docked() =
kosmos.runTest {
val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER))
@@ -348,6 +387,18 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT
}
@Test
+ fun whenToDream_docked_defaultValue() =
+ kosmos.runTest {
+ mContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault,
+ true,
+ )
+
+ val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER))
+ assertThat(whenToDreamState).isEqualTo(WhenToDream.WHILE_DOCKED)
+ }
+
+ @Test
fun whenToDream_postured() =
kosmos.runTest {
val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER))
@@ -362,6 +413,18 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT
}
@Test
+ fun whenToDream_postured_defaultValue() =
+ kosmos.runTest {
+ mContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault,
+ true,
+ )
+
+ val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER))
+ assertThat(whenToDreamState).isEqualTo(WhenToDream.WHILE_POSTURED)
+ }
+
+ @Test
fun whenToDream_default() =
kosmos.runTest {
val whenToDreamState by collectLastValue(underTest.getWhenToDreamState(PRIMARY_USER))
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 c9e7a5d7df05..c3cc3e66f81f 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
@@ -21,6 +21,7 @@ import android.app.admin.DevicePolicyManager
import android.app.admin.devicePolicyManager
import android.content.Intent
import android.content.pm.UserInfo
+import android.content.res.mainResources
import android.os.UserHandle
import android.os.UserManager
import android.os.userManager
@@ -85,6 +86,7 @@ import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.advanceTimeBy
+import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -106,7 +108,10 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN)
private val secondaryUser = UserInfo(/* id= */ 1, /* name= */ "secondary user", /* flags= */ 0)
- private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val kosmos =
+ testKosmos()
+ .apply { mainResources = mContext.orCreateTestableResources.resources }
+ .useUnconfinedTestDispatcher()
private val Kosmos.underTest by Kosmos.Fixture { communalInteractor }
@@ -122,6 +127,32 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
+
+ mContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault,
+ false,
+ )
+ mContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault,
+ false,
+ )
+ mContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault,
+ false,
+ )
+ }
+
+ @After
+ fun tearDown() {
+ mContext.orCreateTestableResources.removeOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault
+ )
+ mContext.orCreateTestableResources.removeOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault
+ )
+ mContext.orCreateTestableResources.removeOverride(
+ com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault
+ )
}
@Test
@@ -398,6 +429,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
fun ctaTile_showsByDefault() =
kosmos.runTest {
fakeCommunalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
@@ -412,6 +444,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
fun ctaTile_afterDismiss_doesNotShow() =
kosmos.runTest {
// Set to main user, so we can dismiss the tile for the main user.
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 b70f46c4b01c..7866a7f01658 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
@@ -29,6 +29,7 @@ import com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.Flags.FLAG_COMMUNAL_RESPONSIVE_GRID
import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_DIRECT_EDIT_MODE
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
@@ -219,6 +220,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
fun ordering_smartspaceBeforeUmoBeforeWidgetsBeforeCtaTile() =
testScope.runTest {
tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
@@ -258,7 +260,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
/** TODO(b/378171351): Handle ongoing content in responsive grid. */
@Test
- @DisableFlags(FLAG_COMMUNAL_RESPONSIVE_GRID)
+ @DisableFlags(FLAG_COMMUNAL_RESPONSIVE_GRID, FLAG_GLANCEABLE_HUB_V2)
fun ongoingContent_umoAndOneTimer_sizedAppropriately() =
testScope.runTest {
// Widgets available.
@@ -296,7 +298,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
/** TODO(b/378171351): Handle ongoing content in responsive grid. */
@Test
- @DisableFlags(FLAG_COMMUNAL_RESPONSIVE_GRID)
+ @DisableFlags(FLAG_COMMUNAL_RESPONSIVE_GRID, FLAG_GLANCEABLE_HUB_V2)
fun ongoingContent_umoAndTwoTimers_sizedAppropriately() =
testScope.runTest {
// Widgets available.
@@ -342,6 +344,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
fun communalContent_mediaHostVisible_umoIncluded() =
testScope.runTest {
// Media playing.
@@ -353,6 +356,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
fun communalContent_mediaHostVisible_umoExcluded() =
testScope.runTest {
whenever(mediaHost.visible).thenReturn(false)
@@ -408,6 +412,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
fun dismissCta_hidesCtaTileAndShowsPopup_thenHidesPopupAfterTimeout() =
testScope.runTest {
setIsMainUser(true)
@@ -734,6 +739,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
fun communalContent_emitsFrozenContent_whenFrozen() =
testScope.runTest {
val communalContent by collectLastValue(underTest.communalContent)
@@ -790,6 +796,7 @@ class CommunalViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
fun communalContent_emitsLatestContent_whenNotFrozen() =
testScope.runTest {
val communalContent by collectLastValue(underTest.communalContent)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamClockTimeComplicationTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamClockTimeComplicationTest.java
index 22ab4994f026..92ccf1294554 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamClockTimeComplicationTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/complication/DreamClockTimeComplicationTest.java
@@ -23,7 +23,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
-import android.view.View;
+import android.widget.TextClock;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -41,6 +41,8 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.Locale;
+
@SmallTest
@RunWith(AndroidJUnit4.class)
public class DreamClockTimeComplicationTest extends SysuiTestCase {
@@ -68,7 +70,7 @@ public class DreamClockTimeComplicationTest extends SysuiTestCase {
private ComplicationViewModel mComplicationViewModel;
@Mock
- private View mView;
+ private TextClock mView;
@Mock
private ComplicationLayoutParams mLayoutParams;
@@ -86,6 +88,7 @@ public class DreamClockTimeComplicationTest extends SysuiTestCase {
MockitoAnnotations.initMocks(this);
when(mComponentFactory.create()).thenReturn(mComponent);
when(mComponent.getViewHolder()).thenReturn(mDreamClockTimeViewHolder);
+ when(mView.getTextLocale()).thenReturn(Locale.US);
mMonitor = SelfExecutingMonitor.createInstance();
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
index 030233625027..39baa01e07d6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt
@@ -570,7 +570,7 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() {
unlockDevice()
assertThat(isUnlocked).isTrue()
- underTest.lockNow()
+ underTest.lockNow("test")
runCurrent()
assertThat(isUnlocked).isFalse()
@@ -597,6 +597,62 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() {
assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
}
+ @Test
+ fun deviceUnlockStatus_staysUnlocked_whenDeviceGoesToSleep_whileIsTrusted() =
+ testScope.runTest {
+ setLockAfterScreenTimeout(5000)
+ kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = false
+ val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+
+ kosmos.fakeTrustRepository.setCurrentUserTrusted(true)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+
+ kosmos.powerInteractor.setAsleepForTest(
+ sleepReason = PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON
+ )
+ runCurrent()
+
+ assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+ }
+
+ @Test
+ fun deviceUnlockStatus_staysUnlocked_whileIsTrusted() =
+ testScope.runTest {
+ val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+ kosmos.fakeTrustRepository.setCurrentUserTrusted(true)
+ unlockDevice()
+
+ kosmos.powerInteractor.setAsleepForTest(
+ sleepReason = PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON
+ )
+ runCurrent()
+
+ assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+ }
+
+ @Test
+ fun deviceUnlockStatus_becomesLocked_whenNoLongerTrusted_whileAsleep() =
+ testScope.runTest {
+ val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
+ kosmos.fakeTrustRepository.setCurrentUserTrusted(true)
+ unlockDevice()
+ kosmos.powerInteractor.setAsleepForTest(
+ sleepReason = PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON
+ )
+ runCurrent()
+ assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+
+ kosmos.fakeTrustRepository.setCurrentUserTrusted(false)
+ runCurrent()
+
+ assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+ }
+
private fun TestScope.unlockDevice() {
val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dump/DumpHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dump/DumpHandlerTest.kt
index 597629219634..09c4231b7178 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dump/DumpHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dump/DumpHandlerTest.kt
@@ -275,7 +275,9 @@ class DumpHandlerTest : SysuiTestCase() {
@Test
fun testDumpAllProtoDumpables() {
+ @Suppress("DEPRECATION")
dumpManager.registerDumpable("protoDumpable1", protoDumpable1)
+ @Suppress("DEPRECATION")
dumpManager.registerDumpable("protoDumpable2", protoDumpable2)
val args = arrayOf(DumpHandler.PROTO)
@@ -287,7 +289,9 @@ class DumpHandlerTest : SysuiTestCase() {
@Test
fun testDumpSingleProtoDumpable() {
+ @Suppress("DEPRECATION")
dumpManager.registerDumpable("protoDumpable1", protoDumpable1)
+ @Suppress("DEPRECATION")
dumpManager.registerDumpable("protoDumpable2", protoDumpable2)
val args = arrayOf(DumpHandler.PROTO, "protoDumpable1")
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
index 6899d23bcfd7..e126b4d7265c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorParameterizedTest.kt
@@ -57,7 +57,6 @@ import platform.test.runner.parameterized.Parameters
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
-@kotlinx.coroutines.ExperimentalCoroutinesApi
class KeyboardTouchpadEduInteractorParameterizedTest(private val gestureType: GestureType) :
SysuiTestCase() {
private val kosmos = testKosmos()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
index dc393c01dce1..adcd849db49a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt
@@ -50,7 +50,6 @@ import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
-@kotlinx.coroutines.ExperimentalCoroutinesApi
class KeyboardTouchpadEduInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
index 4e8a2a349283..49d324b27bb1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryTest.kt
@@ -79,6 +79,7 @@ import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.stub
@@ -473,6 +474,51 @@ class BiometricSettingsRepositoryTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION)
+ fun registerEnabledOnKeyguardCallback_multipleUsers_shouldSendAllUpdates() =
+ testScope.runTest {
+
+ // Simulate call to register callback when in multiple users setup
+ biometricManager.stub {
+ on { registerEnabledOnKeyguardCallback(any()) } doAnswer
+ { invocation ->
+ val callback =
+ invocation.arguments[0] as IBiometricEnabledOnKeyguardCallback
+ callback.onChanged(true, PRIMARY_USER_ID, TYPE_FACE)
+ callback.onChanged(true, PRIMARY_USER_ID, TYPE_FINGERPRINT)
+ callback.onChanged(true, ANOTHER_USER_ID, TYPE_FACE)
+ callback.onChanged(true, ANOTHER_USER_ID, TYPE_FINGERPRINT)
+ }
+ }
+ authController.stub {
+ on { isFingerprintEnrolled(anyInt()) } doReturn true
+ on { isFaceAuthEnrolled(anyInt()) } doReturn true
+ }
+
+ // Check primary user status
+ createBiometricSettingsRepository()
+ var fingerprintAllowed = collectLastValue(underTest.isFingerprintEnrolledAndEnabled)
+ var faceAllowed = collectLastValue(underTest.isFaceAuthEnrolledAndEnabled)
+ runCurrent()
+
+ enrollmentChange(UNDER_DISPLAY_FINGERPRINT, PRIMARY_USER_ID, true)
+ enrollmentChange(FACE, PRIMARY_USER_ID, true)
+ assertThat(fingerprintAllowed()).isTrue()
+ assertThat(faceAllowed()).isTrue()
+
+ // Check secondary user status
+ userRepository.setSelectedUserInfo(ANOTHER_USER)
+ fingerprintAllowed = collectLastValue(underTest.isFingerprintEnrolledAndEnabled)
+ faceAllowed = collectLastValue(underTest.isFaceAuthEnrolledAndEnabled)
+ runCurrent()
+
+ enrollmentChange(UNDER_DISPLAY_FINGERPRINT, ANOTHER_USER_ID, true)
+ enrollmentChange(FACE, ANOTHER_USER_ID, true)
+ assertThat(fingerprintAllowed()).isTrue()
+ assertThat(faceAllowed()).isTrue()
+ }
+
+ @Test
fun devicePolicyControlsFaceAuthenticationEnabledState() =
testScope.runTest {
faceAuthIsEnrolled()
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 57ac90648f33..3078a943be32 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
@@ -26,6 +26,7 @@ import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECT
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -643,6 +644,132 @@ public class MediaOutputAdapterTest extends SysuiTestCase {
verify(mMediaSwitchingController).addDeviceToPlayMedia(mMediaDevice2);
}
+ @DisableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ public void clickFullItemOfSelectableDevice_flagOff_hasListingPreference_verifyConnectDevice() {
+ List<MediaDevice> mediaDevices = new ArrayList<>();
+ mediaDevices.add(mMediaDevice2);
+ when(mMediaDevice2.hasRouteListingPreferenceItem()).thenReturn(true);
+ when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(mediaDevices);
+ when(mMediaSwitchingController.getTransferableMediaDevices()).thenReturn(List.of());
+ when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
+
+ mViewHolder.mContainerLayout.performClick();
+
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2);
+ }
+
+ @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ public void clickFullItemOfSelectableDevice_flagOn_hasListingPreference_verifyConnectDevice() {
+ List<MediaDevice> mediaDevices = new ArrayList<>();
+ mediaDevices.add(mMediaDevice2);
+ when(mMediaDevice2.hasRouteListingPreferenceItem()).thenReturn(true);
+ when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(mediaDevices);
+ when(mMediaSwitchingController.getTransferableMediaDevices()).thenReturn(List.of());
+ when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
+
+ mViewHolder.mContainerLayout.performClick();
+
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2);
+ }
+
+ @DisableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ public void clickFullItemOfSelectableDevice_flagOff_isTransferable_verifyConnectDevice() {
+ List<MediaDevice> mediaDevices = new ArrayList<>();
+ mediaDevices.add(mMediaDevice2);
+ when(mMediaDevice2.hasRouteListingPreferenceItem()).thenReturn(false);
+ when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(mediaDevices);
+ when(mMediaSwitchingController.getTransferableMediaDevices()).thenReturn(mediaDevices);
+ when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
+
+ mViewHolder.mContainerLayout.performClick();
+
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2);
+ }
+
+ @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ public void clickFullItemOfSelectableDevice_flagOn_isTransferable_verifyConnectDevice() {
+ List<MediaDevice> mediaDevices = new ArrayList<>();
+ mediaDevices.add(mMediaDevice2);
+ when(mMediaDevice2.hasRouteListingPreferenceItem()).thenReturn(false);
+ when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(mediaDevices);
+ when(mMediaSwitchingController.getTransferableMediaDevices()).thenReturn(mediaDevices);
+ when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
+
+ mViewHolder.mContainerLayout.performClick();
+
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2);
+ }
+
+ @DisableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ public void clickFullItemOfSelectableDevice_flagOff_notTransferable_verifyConnectDevice() {
+ List<MediaDevice> mediaDevices = new ArrayList<>();
+ mediaDevices.add(mMediaDevice2);
+ when(mMediaDevice2.hasRouteListingPreferenceItem()).thenReturn(false);
+ when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(mediaDevices);
+ when(mMediaSwitchingController.getTransferableMediaDevices()).thenReturn(List.of());
+ when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
+
+ mViewHolder.mContainerLayout.performClick();
+
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2);
+ }
+
+ @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ public void clickFullItemOfSelectableDevice_flagOn_notTransferable_verifyNotConnectDevice() {
+ List<MediaDevice> mediaDevices = new ArrayList<>();
+ mediaDevices.add(mMediaDevice2);
+ when(mMediaDevice2.hasRouteListingPreferenceItem()).thenReturn(false);
+ when(mMediaSwitchingController.getSelectableMediaDevice()).thenReturn(mediaDevices);
+ when(mMediaSwitchingController.getTransferableMediaDevices()).thenReturn(List.of());
+ when(mMediaSwitchingController.isCurrentOutputDeviceHasSessionOngoing()).thenReturn(false);
+ mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+ assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+ assertThat(mViewHolder.mContainerLayout.isFocusable()).isTrue();
+
+ mViewHolder.mContainerLayout.performClick();
+
+ verify(mMediaSwitchingController, never()).connectDevice(any(MediaDevice.class));
+ }
+
@Test
public void onGroupActionTriggered_clickSelectedRemoteDevice_triggerUngrouping() {
when(mMediaSwitchingController.getSelectableMediaDevice())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
index ebc00c3897cb..9283455d3e54 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt
@@ -38,7 +38,7 @@ class FakeNoteTaskBubbleController(
) : NoteTaskBubblesController(unUsed1, unsUsed2) {
override suspend fun areBubblesAvailable() = optionalBubbles.isPresent
- override suspend fun showOrHideAppBubble(
+ override suspend fun showOrHideNoteBubble(
intent: Intent,
userHandle: UserHandle,
icon: Icon,
@@ -49,12 +49,12 @@ class FakeNoteTaskBubbleController(
if (
bubbleExpandBehavior == NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED &&
bubbles.isBubbleExpanded(
- Bubble.getAppBubbleKeyForApp(intent.`package`, userHandle)
+ Bubble.getNoteBubbleKeyForApp(intent.`package`, userHandle)
)
) {
return@ifPresentOrElse
}
- bubbles.showOrHideAppBubble(intent, userHandle, icon)
+ bubbles.showOrHideNoteBubble(intent, userHandle, icon)
},
{ throw IllegalAccessException() },
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
index e55d6ad6c5a0..7b9af90ff228 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt
@@ -64,41 +64,41 @@ internal class NoteTaskBubblesServiceTest : SysuiTestCase() {
}
@Test
- fun showOrHideAppBubble_defaultExpandBehavior_shouldCallBubblesApi() {
+ fun showOrHideNoteBubble_defaultExpandBehavior_shouldCallBubblesApi() {
val intent = Intent()
val user = UserHandle.SYSTEM
val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
val bubbleExpandBehavior = NoteTaskBubbleExpandBehavior.DEFAULT
whenever(bubbles.isBubbleExpanded(any())).thenReturn(false)
- createServiceBinder().showOrHideAppBubble(intent, user, icon, bubbleExpandBehavior)
+ createServiceBinder().showOrHideNoteBubble(intent, user, icon, bubbleExpandBehavior)
- verify(bubbles).showOrHideAppBubble(intent, user, icon)
+ verify(bubbles).showOrHideNoteBubble(intent, user, icon)
}
@Test
- fun showOrHideAppBubble_keepIfExpanded_bubbleShown_shouldNotCallBubblesApi() {
+ fun showOrHideNoteBubble_keepIfExpanded_bubbleShown_shouldNotCallBubblesApi() {
val intent = Intent().apply { setPackage("test") }
val user = UserHandle.SYSTEM
val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
val bubbleExpandBehavior = NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED
whenever(bubbles.isBubbleExpanded(any())).thenReturn(true)
- createServiceBinder().showOrHideAppBubble(intent, user, icon, bubbleExpandBehavior)
+ createServiceBinder().showOrHideNoteBubble(intent, user, icon, bubbleExpandBehavior)
- verify(bubbles, never()).showOrHideAppBubble(intent, user, icon)
+ verify(bubbles, never()).showOrHideNoteBubble(intent, user, icon)
}
@Test
- fun showOrHideAppBubble_keepIfExpanded_bubbleNotShown_shouldCallBubblesApi() {
+ fun showOrHideNoteBubble_keepIfExpanded_bubbleNotShown_shouldCallBubblesApi() {
val intent = Intent().apply { setPackage("test") }
val user = UserHandle.SYSTEM
val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
val bubbleExpandBehavior = NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED
whenever(bubbles.isBubbleExpanded(any())).thenReturn(false)
- createServiceBinder().showOrHideAppBubble(intent, user, icon, bubbleExpandBehavior)
+ createServiceBinder().showOrHideNoteBubble(intent, user, icon, bubbleExpandBehavior)
- verify(bubbles).showOrHideAppBubble(intent, user, icon)
+ verify(bubbles).showOrHideNoteBubble(intent, user, icon)
}
}
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 675960832edc..43db50ad675f 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
@@ -124,35 +124,35 @@ class NotificationsShadeOverlayContentViewModelTest : SysuiTestCase() {
}
@Test
- fun showHeader_showsOnNarrowScreen() =
+ fun showClock_showsOnNarrowScreen() =
testScope.runTest {
kosmos.shadeRepository.setShadeLayoutWide(false)
// Shown when notifications are present.
kosmos.activeNotificationListRepository.setActiveNotifs(1)
runCurrent()
- assertThat(underTest.showHeader).isTrue()
+ assertThat(underTest.showClock).isTrue()
// Hidden when notifications are not present.
kosmos.activeNotificationListRepository.setActiveNotifs(0)
runCurrent()
- assertThat(underTest.showHeader).isFalse()
+ assertThat(underTest.showClock).isFalse()
}
@Test
- fun showHeader_hidesOnWideScreen() =
+ fun showClock_hidesOnWideScreen() =
testScope.runTest {
kosmos.shadeRepository.setShadeLayoutWide(true)
// Hidden when notifications are present.
kosmos.activeNotificationListRepository.setActiveNotifs(1)
runCurrent()
- assertThat(underTest.showHeader).isFalse()
+ assertThat(underTest.showClock).isFalse()
// Hidden when notifications are not present.
kosmos.activeNotificationListRepository.setActiveNotifs(0)
runCurrent()
- assertThat(underTest.showHeader).isFalse()
+ assertThat(underTest.showClock).isFalse()
}
private fun TestScope.lockDevice() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt
index 98770c724126..c5d679f5df05 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt
@@ -62,8 +62,8 @@ class DetailsViewModelTest : SysuiTestCase() {
val tiles = currentTilesInteractor.currentTiles.value
assertThat(currentTilesInteractor.currentTilesSpecs.size).isEqualTo(2)
- assertThat(tiles!![1].spec).isEqualTo(specNoDetails)
- (tiles!![1].tile as FakeQSTile).hasDetailsViewModel = false
+ assertThat(tiles[1].spec).isEqualTo(specNoDetails)
+ (tiles[1].tile as FakeQSTile).hasDetailsViewModel = false
assertThat(underTest.activeTileDetails).isNull()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt
new file mode 100644
index 000000000000..6e26fa119888
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.ui.viewmodel
+
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.qs.composefragment.dagger.usingMediaInComposeFragment
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
+import com.android.systemui.shade.domain.interactor.enableDualShade
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+@EnableSceneContainer
+class QuickSettingsContainerViewModelTest : SysuiTestCase() {
+
+ private val kosmos =
+ testKosmos().apply {
+ usingMediaInComposeFragment = false // This is not for the compose fragment
+ }
+ private val testScope = kosmos.testScope
+
+ private val shadeModeInteractor = kosmos.shadeModeInteractor
+
+ private val underTest by lazy {
+ kosmos.quickSettingsContainerViewModelFactory.create(supportsBrightnessMirroring = false)
+ }
+
+ @Before
+ fun setUp() {
+ kosmos.sceneContainerStartable.start()
+ kosmos.enableDualShade()
+ underTest.activateIn(testScope)
+ }
+
+ @Test
+ fun showHeader_showsOnNarrowScreen() =
+ testScope.runTest {
+ kosmos.enableDualShade(wideLayout = false)
+ val isShadeLayoutWide by collectLastValue(shadeModeInteractor.isShadeLayoutWide)
+ assertThat(isShadeLayoutWide).isFalse()
+
+ assertThat(underTest.showHeader).isTrue()
+ }
+
+ @Test
+ fun showHeader_hidesOnWideScreen() =
+ testScope.runTest {
+ kosmos.enableDualShade(wideLayout = true)
+ val isShadeLayoutWide by collectLastValue(shadeModeInteractor.isShadeLayoutWide)
+ assertThat(isShadeLayoutWide).isTrue()
+
+ assertThat(underTest.showHeader).isFalse()
+ }
+}
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 01714d7a4b87..b532554f5dfd 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
@@ -127,24 +127,6 @@ class QuickSettingsShadeOverlayContentViewModelTest : SysuiTestCase() {
}
@Test
- fun showHeader_showsOnNarrowScreen() =
- testScope.runTest {
- kosmos.enableDualShade(wideLayout = false)
- runCurrent()
-
- assertThat(underTest.showHeader).isTrue()
- }
-
- @Test
- fun showHeader_hidesOnWideScreen() =
- testScope.runTest {
- kosmos.enableDualShade(wideLayout = true)
- runCurrent()
-
- assertThat(underTest.showHeader).isFalse()
- }
-
- @Test
fun onPanelShapeChanged() =
testScope.runTest {
var actual: ShadeScrimShape? = null
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt
index f2e658dc3759..7bcaeabfee69 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/IssueRecordingServiceSessionTest.kt
@@ -38,6 +38,7 @@ import com.android.systemui.util.settings.fakeGlobalSettings
import com.android.traceur.TraceConfig
import com.google.common.truth.Truth
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
@@ -126,6 +127,7 @@ class IssueRecordingServiceSessionTest : SysuiTestCase() {
verify(iActivityManager).requestBugReportWithExtraAttachments(any())
}
+ @Ignore("b/392753499")
@Test
fun sharesTracesDirectly_afterReceivingShareCommand_withTakeBugreportFalse() {
underTest.takeBugReport = false
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 af30e435da73..5648c62fb439 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
@@ -2697,6 +2697,34 @@ class SceneContainerStartableTest : SysuiTestCase() {
assertThat(isVisible).isFalse()
}
+ @Test
+ fun deviceLocks_whenNoLongerTrusted_whileDeviceNotEntered() =
+ testScope.runTest {
+ prepareState(isDeviceUnlocked = true, initialSceneKey = Scenes.Gone)
+ underTest.start()
+
+ val isDeviceEntered by collectLastValue(kosmos.deviceEntryInteractor.isDeviceEntered)
+ val deviceUnlockStatus by
+ collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)
+ val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene)
+ assertThat(isDeviceEntered).isTrue()
+ assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ kosmos.fakeTrustRepository.setCurrentUserTrusted(true)
+ kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+ runCurrent()
+ assertThat(isDeviceEntered).isFalse()
+ assertThat(deviceUnlockStatus?.isUnlocked).isTrue()
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+ kosmos.fakeTrustRepository.setCurrentUserTrusted(false)
+ runCurrent()
+
+ assertThat(isDeviceEntered).isFalse()
+ assertThat(deviceUnlockStatus?.isUnlocked).isFalse()
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ }
+
private fun TestScope.emulateSceneTransition(
transitionStateFlow: MutableStateFlow<ObservableTransitionState>,
toScene: SceneKey,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java
index d8897e9048c3..81301b3886f1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java
@@ -183,14 +183,14 @@ public final class AppClipsServiceTest extends SysuiTestCase {
when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
when(mOptionalBubbles.isEmpty()).thenReturn(false);
when(mOptionalBubbles.get()).thenReturn(mBubbles);
- when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false);
+ when(mBubbles.isNoteBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false);
}
private void mockForScreenshotBlocked() {
when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
when(mOptionalBubbles.isEmpty()).thenReturn(false);
when(mOptionalBubbles.get()).thenReturn(mBubbles);
- when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
+ when(mBubbles.isNoteBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true);
}
@@ -199,7 +199,7 @@ public final class AppClipsServiceTest extends SysuiTestCase {
when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
when(mOptionalBubbles.isEmpty()).thenReturn(false);
when(mOptionalBubbles.get()).thenReturn(mBubbles);
- when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
+ when(mBubbles.isNoteBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
}
@@ -208,7 +208,7 @@ public final class AppClipsServiceTest extends SysuiTestCase {
when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
when(mOptionalBubbles.isEmpty()).thenReturn(false);
when(mOptionalBubbles.get()).thenReturn(mBubbles);
- when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
+ when(mBubbles.isNoteBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
}
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 62c360400582..85e59364d6b6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -375,7 +375,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
SystemClock systemClock = new FakeSystemClock();
mStatusBarStateController = new StatusBarStateControllerImpl(
mUiEventLogger,
- () -> mKosmos.getInteractionJankMonitor(),
mJavaAdapter,
() -> mKeyguardInteractor,
() -> mKeyguardTransitionInteractor,
@@ -456,7 +455,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
mock(HeadsUpManager.class),
new StatusBarStateControllerImpl(
new UiEventLoggerFake(),
- () -> mKosmos.getInteractionJankMonitor(),
mJavaAdapter,
() -> mKeyguardInteractor,
() -> mKeyguardTransitionInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index 8ce20d2a05e9..d08c8a7c5974 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -23,6 +23,9 @@ import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.disableDualShade
import com.android.systemui.shade.domain.interactor.enableDualShade
+import com.android.systemui.shade.domain.interactor.enableSingleShade
+import com.android.systemui.shade.domain.interactor.enableSplitShade
+import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor
import com.android.systemui.testKosmos
@@ -273,6 +276,116 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade)
}
+ @Test
+ fun highlightChips_notifsOpenInSingleShade_bothNone() =
+ testScope.runTest {
+ kosmos.enableSingleShade()
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ setScene(Scenes.Shade)
+ assertThat(currentScene).isEqualTo(Scenes.Shade)
+ assertThat(currentOverlays).isEmpty()
+
+ assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None)
+ assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None)
+ }
+
+ @Test
+ fun highlightChips_notifsOpenInSplitShade_bothNone() =
+ testScope.runTest {
+ kosmos.enableSplitShade()
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ setScene(Scenes.Shade)
+ assertThat(currentScene).isEqualTo(Scenes.Shade)
+ assertThat(currentOverlays).isEmpty()
+
+ assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None)
+ assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None)
+ }
+
+ @Test
+ fun highlightChips_quickSettingsOpenInSingleShade_bothNone() =
+ testScope.runTest {
+ kosmos.enableSingleShade()
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ setScene(Scenes.QuickSettings)
+ assertThat(currentScene).isEqualTo(Scenes.QuickSettings)
+ assertThat(currentOverlays).isEmpty()
+
+ assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None)
+ assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None)
+ }
+
+ @Test
+ fun highlightChips_notifsOpenInDualShade_notifsStrongQuickSettingsWeak() =
+ testScope.runTest {
+ kosmos.enableDualShade()
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+
+ // Test the lockscreen scenario.
+ setScene(Scenes.Lockscreen)
+ setOverlay(Overlays.NotificationsShade)
+ assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong)
+ assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak)
+
+ // Test the unlocked scenario.
+ setDeviceEntered(true)
+ setScene(Scenes.Gone)
+ setOverlay(Overlays.NotificationsShade)
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ assertThat(currentOverlays).isNotEmpty()
+ assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong)
+ assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak)
+ }
+
+ @Test
+ fun highlightChips_quickSettingsOpenInDualShade_notifsWeakQuickSettingsStrong() =
+ testScope.runTest {
+ kosmos.enableDualShade()
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+
+ // Test the lockscreen scenario.
+ setScene(Scenes.Lockscreen)
+ setOverlay(Overlays.QuickSettingsShade)
+ assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak)
+ assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong)
+
+ // Test the unlocked scenario.
+ setDeviceEntered(true)
+ setScene(Scenes.Gone)
+ setOverlay(Overlays.QuickSettingsShade)
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ assertThat(currentOverlays).isNotEmpty()
+ assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak)
+ assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong)
+ }
+
+ @Test
+ fun highlightChips_noOverlaysInDualShade_bothNone() =
+ testScope.runTest {
+ kosmos.enableDualShade()
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+
+ // Test the lockscreen scenario.
+ setScene(Scenes.Lockscreen)
+ assertThat(currentOverlays).isEmpty()
+ assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None)
+ assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None)
+
+ // Test the unlocked scenario.
+ setDeviceEntered(true)
+ setScene(Scenes.Gone)
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ assertThat(currentOverlays).isEmpty()
+ assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None)
+ assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None)
+ }
+
companion object {
private val SUB_1 =
SubscriptionModel(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index a51e0c0add37..a458ab6e713c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -33,7 +33,6 @@ import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteract
import com.android.systemui.flags.DisableSceneContainer
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.parameterizeSceneContainerFlag
-import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
@@ -113,7 +112,6 @@ class StatusBarStateControllerImplTest(flags: FlagsParameterization) : SysuiTest
object :
StatusBarStateControllerImpl(
uiEventLogger,
- { kosmos.interactionJankMonitor },
JavaAdapter(testScope.backgroundScope),
{ kosmos.keyguardInteractor },
{ kosmos.keyguardTransitionInteractor },
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
index b297bed61ecb..fc13cdaae8ce 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
@@ -16,8 +16,6 @@
package com.android.systemui.statusbar.chips.casttootherdevice.ui.view
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.junit.runner.RunWith
import android.content.ComponentName
import android.content.DialogInterface
import android.content.Intent
@@ -25,6 +23,11 @@ import android.content.applicationContext
import android.content.packageManager
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.View
+import android.view.Window
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
@@ -43,10 +46,12 @@ import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
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
@@ -250,6 +255,36 @@ class EndCastScreenToOtherDeviceDialogDelegateTest : SysuiTestCase() {
assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
}
+ @Test
+ @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END)
+ fun accessibilityDataSensitive_flagEnabled_appliesSetting() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
+ val window = mock<Window>()
+ val decorView = mock<View>()
+ whenever(sysuiDialog.window).thenReturn(window)
+ whenever(window.decorView).thenReturn(decorView)
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(decorView).setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES)
+ }
+
+ @Test
+ @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END)
+ fun accessibilityDataSensitive_flagDisabled_doesNotApplySetting() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+
+ val window = mock<Window>()
+ val decorView = mock<View>()
+ whenever(sysuiDialog.window).thenReturn(window)
+ whenever(window.decorView).thenReturn(decorView)
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(decorView, never()).setAccessibilityDataSensitive(any())
+ }
+
private fun createAndSetDelegate(state: MediaProjectionState.Projecting) {
underTest =
EndCastScreenToOtherDeviceDialogDelegate(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
index 9e8f22e331ed..ddac98dde45d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
@@ -18,6 +18,10 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.view
import android.content.DialogInterface
import android.content.applicationContext
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.View
+import android.view.Window
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -37,10 +41,13 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.runner.RunWith
+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)
@@ -132,7 +139,7 @@ class EndGenericCastToOtherDeviceDialogDelegateTest : SysuiTestCase() {
verify(sysuiDialog)
.setPositiveButton(
eq(R.string.cast_to_other_device_stop_dialog_button),
- clickListener.capture()
+ clickListener.capture(),
)
// Verify that clicking the button stops the recording
@@ -144,6 +151,36 @@ class EndGenericCastToOtherDeviceDialogDelegateTest : SysuiTestCase() {
assertThat(kosmos.fakeMediaRouterRepository.lastStoppedDevice).isEqualTo(device)
}
+ @Test
+ @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END)
+ fun accessibilityDataSensitive_flagEnabled_appliesSetting() {
+ createAndSetDelegate()
+
+ val window = mock<Window>()
+ val decorView = mock<View>()
+ whenever(sysuiDialog.window).thenReturn(window)
+ whenever(window.decorView).thenReturn(decorView)
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(decorView).setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES)
+ }
+
+ @Test
+ @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END)
+ fun accessibilityDataSensitive_flagDisabled_doesNotApplySetting() {
+ createAndSetDelegate()
+
+ val window = mock<Window>()
+ val decorView = mock<View>()
+ whenever(sysuiDialog.window).thenReturn(window)
+ whenever(window.decorView).thenReturn(decorView)
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(decorView, never()).setAccessibilityDataSensitive(any())
+ }
+
private fun createAndSetDelegate(deviceName: String? = null) {
underTest =
EndGenericCastToOtherDeviceDialogDelegate(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
index 709e0b57c02a..1f91babbfa47 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
@@ -23,6 +23,10 @@ import android.content.Intent
import android.content.applicationContext
import android.content.packageManager
import android.content.pm.ApplicationInfo
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.View
+import android.view.Window
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -45,6 +49,7 @@ 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
@@ -130,7 +135,7 @@ class EndScreenRecordingDialogDelegateTest : SysuiTestCase() {
verify(sysuiDialog)
.setPositiveButton(
eq(R.string.screenrecord_stop_dialog_button),
- clickListener.capture()
+ clickListener.capture(),
)
// Verify that clicking the button stops the recording
@@ -142,6 +147,36 @@ class EndScreenRecordingDialogDelegateTest : SysuiTestCase() {
assertThat(kosmos.screenRecordRepository.stopRecordingInvoked).isTrue()
}
+ @Test
+ @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END)
+ fun accessibilityDataSensitive_flagEnabled_appliesSetting() {
+ createAndSetDelegate(recordedTask = null)
+
+ val window = mock<Window>()
+ val decorView = mock<View>()
+ whenever(sysuiDialog.window).thenReturn(window)
+ whenever(window.decorView).thenReturn(decorView)
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(decorView).setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES)
+ }
+
+ @Test
+ @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END)
+ fun accessibilityDataSensitive_flagDisabled_doesNotApplySetting() {
+ createAndSetDelegate(recordedTask = null)
+
+ val window = mock<Window>()
+ val decorView = mock<View>()
+ whenever(sysuiDialog.window).thenReturn(window)
+ whenever(window.decorView).thenReturn(decorView)
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(decorView, never()).setAccessibilityDataSensitive(any())
+ }
+
private fun createAndSetDelegate(recordedTask: ActivityManager.RunningTaskInfo?) {
underTest =
EndScreenRecordingDialogDelegate(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt
index 411d306f163c..259d3872cfa2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegateTest.kt
@@ -18,6 +18,11 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.view
import android.content.DialogInterface
import android.content.applicationContext
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.View
+import android.view.Window
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.testScope
@@ -31,26 +36,26 @@ import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
@SmallTest
@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
class EndGenericShareToAppDialogDelegateTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val sysuiDialog = mock<SystemUIDialog>()
- private val underTest =
- EndGenericShareToAppDialogDelegate(
- kosmos.endMediaProjectionDialogHelper,
- kosmos.applicationContext,
- stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
- )
+ private lateinit var underTest: EndGenericShareToAppDialogDelegate
@Test
fun positiveButton_clickStopsRecording() =
kosmos.testScope.runTest {
+ createAndSetDelegate()
underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isFalse()
@@ -62,4 +67,43 @@ class EndGenericShareToAppDialogDelegateTest : SysuiTestCase() {
assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
}
+
+ @Test
+ @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END)
+ fun accessibilityDataSensitive_flagEnabled_appliesSetting() {
+ createAndSetDelegate()
+
+ val window = mock<Window>()
+ val decorView = mock<View>()
+ whenever(sysuiDialog.window).thenReturn(window)
+ whenever(window.decorView).thenReturn(decorView)
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(decorView).setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES)
+ }
+
+ @Test
+ @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END)
+ fun accessibilityDataSensitive_flagDisabled_doesNotApplySetting() {
+ createAndSetDelegate()
+
+ val window = mock<Window>()
+ val decorView = mock<View>()
+ whenever(sysuiDialog.window).thenReturn(window)
+ whenever(window.decorView).thenReturn(decorView)
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(decorView, never()).setAccessibilityDataSensitive(any())
+ }
+
+ private fun createAndSetDelegate() {
+ underTest =
+ EndGenericShareToAppDialogDelegate(
+ kosmos.endMediaProjectionDialogHelper,
+ kosmos.applicationContext,
+ stopAction = kosmos.mediaProjectionChipInteractor::stopProjecting,
+ )
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt
index 6885a6bd7229..0ae0d178185e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegateTest.kt
@@ -23,6 +23,11 @@ import android.content.applicationContext
import android.content.packageManager
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.View
+import android.view.Window
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
@@ -41,15 +46,18 @@ import kotlin.test.Test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.runner.RunWith
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
@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
class EndShareScreenToAppDialogDelegateTest : SysuiTestCase() {
private val kosmos = Kosmos().also { it.testCase = this }
private val sysuiDialog = mock<SystemUIDialog>()
@@ -193,6 +201,40 @@ class EndShareScreenToAppDialogDelegateTest : SysuiTestCase() {
assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
}
+ @Test
+ @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END)
+ fun accessibilityDataSensitive_flagEnabled_appliesSetting() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+ whenever(kosmos.packageManager.getApplicationInfo(eq(HOST_PACKAGE), any<Int>()))
+ .thenThrow(PackageManager.NameNotFoundException())
+
+ val window = mock<Window>()
+ val decorView = mock<View>()
+ whenever(sysuiDialog.window).thenReturn(window)
+ whenever(window.decorView).thenReturn(decorView)
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(decorView).setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES)
+ }
+
+ @Test
+ @DisableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END)
+ fun accessibilityDataSensitive_flagDisabled_doesNotApplySetting() {
+ createAndSetDelegate(ENTIRE_SCREEN)
+ whenever(kosmos.packageManager.getApplicationInfo(eq(HOST_PACKAGE), any<Int>()))
+ .thenThrow(PackageManager.NameNotFoundException())
+
+ val window = mock<Window>()
+ val decorView = mock<View>()
+ whenever(sysuiDialog.window).thenReturn(window)
+ whenever(window.decorView).thenReturn(decorView)
+
+ underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+ verify(decorView, never()).setAccessibilityDataSensitive(any())
+ }
+
private fun createAndSetDelegate(state: MediaProjectionState.Projecting) {
underTest =
EndShareScreenToAppDialogDelegate(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
index 5a66888e4da4..3961e177d1aa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
@@ -297,6 +297,33 @@ class ShareToAppChipViewModelTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(com.android.media.projection.flags.Flags.FLAG_SHOW_STOP_DIALOG_POST_CALL_END)
+ fun stopDialog_flagEnabled_eventEmitted_dialogCannotBeDismissedByTouchOutside() =
+ kosmos.runTest {
+ val latestDialogModel by collectLastValue(underTest.stopDialogToShow)
+
+ mediaProjectionRepo.mediaProjectionState.value =
+ MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+
+ fakeMediaProjectionRepository.emitProjectionStartedDuringCallAndActivePostCallEvent()
+
+ // Verify that the dialog is shown
+ assertThat(latestDialogModel)
+ .isInstanceOf(MediaProjectionStopDialogModel.Shown::class.java)
+
+ val dialogModel = latestDialogModel as MediaProjectionStopDialogModel.Shown
+
+ whenever(dialogModel.dialogDelegate.createDialog()).thenReturn(mockDialog)
+
+ dialogModel.createAndShowDialog()
+
+ verify(mockDialog).show()
+
+ // Verify that setCanceledOnTouchOutside(false) is called
+ verify(mockDialog).setCanceledOnTouchOutside(false)
+ }
+
+ @Test
fun chip_notProjectingState_isHidden() =
testScope.runTest {
val latest by collectLastValue(underTest.chip)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
index 9dfc922eb7d0..339f8fac3820 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
@@ -46,6 +46,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.provider.visualStabilityProvider
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.NotificationTestHelper
import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
@@ -678,25 +679,32 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- @DisableFlags(StatusBarNotifChips.FLAG_NAME)
- fun testIsSticky_promotedAndExpanded_notifChipsFlagOff_true() {
- val notif = Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build()
- notif.flags = FLAG_PROMOTED_ONGOING
- val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, notif)
- val row = testHelper.createRow().apply { setPinnedStatus(PinnedStatus.PinnedBySystem) }
- notifEntry.row = row
-
- underTest.showNotification(notifEntry)
+ @DisableFlags(StatusBarNotifChips.FLAG_NAME, PromotedNotificationUi.FLAG_NAME)
+ fun testIsSticky_promotedAndExpanded_notifChipsFlagOff_promotedUiFlagOff_true() {
+ assertThat(getIsSticky_promotedAndExpanded()).isTrue()
+ }
- val headsUpEntry = underTest.getHeadsUpEntry(notifEntry.key)
- headsUpEntry!!.setExpanded(true)
+ @Test
+ @EnableFlags(StatusBarNotifChips.FLAG_NAME, PromotedNotificationUi.FLAG_NAME)
+ fun testIsSticky_promotedAndExpanded_notifChipsFlagOn_promotedUiFlagOn_false() {
+ assertThat(getIsSticky_promotedAndExpanded()).isFalse()
+ }
- assertThat(underTest.isSticky(notifEntry.key)).isTrue()
+ @Test
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME)
+ @DisableFlags(StatusBarNotifChips.FLAG_NAME)
+ fun testIsSticky_promotedAndExpanded_promotedUiFlagOn_false() {
+ assertThat(getIsSticky_promotedAndExpanded()).isFalse()
}
@Test
@EnableFlags(StatusBarNotifChips.FLAG_NAME)
+ @DisableFlags(PromotedNotificationUi.FLAG_NAME)
fun testIsSticky_promotedAndExpanded_notifChipsFlagOn_false() {
+ assertThat(getIsSticky_promotedAndExpanded()).isFalse()
+ }
+
+ private fun getIsSticky_promotedAndExpanded(): Boolean {
val notif = Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build()
notif.flags = FLAG_PROMOTED_ONGOING
val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, notif)
@@ -708,7 +716,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
val headsUpEntry = underTest.getHeadsUpEntry(notifEntry.key)
headsUpEntry!!.setExpanded(true)
- assertThat(underTest.isSticky(notifEntry.key)).isFalse()
+ return underTest.isSticky(notifEntry.key)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt
index abb1edf8cb27..8054bd113771 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt
@@ -237,9 +237,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
assertThat(content).isNotNull()
assertThat(content?.style).isEqualTo(Style.Progress)
- assertThat(content?.progress).isNotNull()
- assertThat(content?.progress?.progress).isEqualTo(75)
- assertThat(content?.progress?.progressMax).isEqualTo(100)
+ assertThat(content?.newProgress).isNotNull()
+ assertThat(content?.newProgress?.progress).isEqualTo(75)
+ assertThat(content?.newProgress?.progressMax).isEqualTo(100)
}
@Test
@@ -255,6 +255,43 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
assertThat(content?.style).isEqualTo(Style.Ineligible)
}
+ @Test
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
+ fun extractsContent_fromOldProgressDeterminate() {
+ val entry = createEntry {
+ setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ false)
+ }
+
+ val content = extractContent(entry)
+
+ assertThat(content).isNotNull()
+
+ val oldProgress = content?.oldProgress
+ assertThat(oldProgress).isNotNull()
+
+ assertThat(content).isNotNull()
+ assertThat(content?.oldProgress).isNotNull()
+ assertThat(content?.oldProgress?.progress).isEqualTo(TEST_PROGRESS)
+ assertThat(content?.oldProgress?.max).isEqualTo(TEST_PROGRESS_MAX)
+ assertThat(content?.oldProgress?.isIndeterminate).isFalse()
+ }
+
+ @Test
+ @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME)
+ fun extractsContent_fromOldProgressIndeterminate() {
+ val entry = createEntry {
+ setProgress(TEST_PROGRESS_MAX, TEST_PROGRESS, /* indeterminate= */ true)
+ }
+
+ val content = extractContent(entry)
+
+ assertThat(content).isNotNull()
+ assertThat(content?.oldProgress).isNotNull()
+ assertThat(content?.oldProgress?.progress).isEqualTo(TEST_PROGRESS)
+ assertThat(content?.oldProgress?.max).isEqualTo(TEST_PROGRESS_MAX)
+ assertThat(content?.oldProgress?.isIndeterminate).isTrue()
+ }
+
private fun extractContent(entry: NotificationEntry): PromotedNotificationContentModel? {
val recoveredBuilder = Notification.Builder(context, entry.sbn.notification)
return underTest.extractContent(entry, recoveredBuilder)
@@ -277,6 +314,9 @@ class PromotedNotificationContentExtractorImplTest : SysuiTestCase() {
private const val TEST_CONTENT_TEXT = "content text"
private const val TEST_SHORT_CRITICAL_TEXT = "short"
+ private const val TEST_PROGRESS = 50
+ private const val TEST_PROGRESS_MAX = 100
+
private const val TEST_PERSON_NAME = "person name"
private const val TEST_PERSON_KEY = "person key"
private val TEST_PERSON =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 39cff63f363e..4f9beb6ffc73 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -6,6 +6,7 @@ import android.platform.test.flag.junit.FlagsParameterization
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ShadeInterpolation.getContentAlpha
import com.android.systemui.dump.DumpManager
@@ -481,7 +482,11 @@ class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() {
stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
val marginBottom =
- context.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom)
+ context.resources.getDimensionPixelSize(
+ if (Flags.notificationsRedesignFooterView())
+ R.dimen.notification_2025_panel_margin_bottom
+ else R.dimen.notification_panel_margin_bottom
+ )
val fullHeight = ambientState.layoutMaxHeight + marginBottom - ambientState.stackY
val centeredY = ambientState.stackY + fullHeight / 2f - emptyShadeView.height / 2f
assertThat(emptyShadeView.viewState.yTranslation).isEqualTo(centeredY)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 786b3590facb..77f02c050098 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -19,9 +19,12 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
import com.android.systemui.common.shared.model.NotificationContainerBounds
@@ -298,6 +301,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S
}
@Test
+ @DisableFlags(FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW)
fun validateMarginBottom() =
testScope.runTest {
overrideResource(R.dimen.notification_panel_margin_bottom, 50)
@@ -310,6 +314,19 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S
}
@Test
+ @EnableFlags(FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW)
+ fun validateMarginBottom_footerRedesign() =
+ testScope.runTest {
+ overrideResource(R.dimen.notification_2025_panel_margin_bottom, 50)
+
+ val dimens by collectLastValue(underTest.configurationBasedDimensions)
+
+ configurationRepository.onAnyConfigurationChange()
+
+ assertThat(dimens!!.marginBottom).isEqualTo(50)
+ }
+
+ @Test
@DisableSceneContainer
fun validateMarginTopWithLargeScreenHeader_usesHelper() =
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index b0b80a9419e2..52c41a07198d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
@@ -25,11 +25,14 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.testScope
import com.android.systemui.shared.Flags as SharedFlags
import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -48,6 +51,7 @@ class ActivityStarterImplTest : SysuiTestCase() {
@Mock private lateinit var activityStarterInternal: ActivityStarterInternalImpl
@Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
private lateinit var underTest: ActivityStarterImpl
+ private val kosmos = testKosmos()
private val mainExecutor = FakeExecutor(FakeSystemClock())
@Before
@@ -69,12 +73,18 @@ class ActivityStarterImplTest : SysuiTestCase() {
@EnableSceneContainer
@Test
fun registerTransition_forwardsTheRequest() {
- val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
- val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java)
-
- underTest.registerTransition(cookie, controllerFactory)
-
- verify(activityStarterInternal).registerTransition(eq(cookie), eq(controllerFactory))
+ with(kosmos) {
+ testScope.runTest {
+ val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
+ val controllerFactory =
+ mock(ActivityTransitionAnimator.ControllerFactory::class.java)
+
+ underTest.registerTransition(cookie, controllerFactory, testScope)
+
+ verify(activityStarterInternal)
+ .registerTransition(eq(cookie), eq(controllerFactory), eq(testScope))
+ }
+ }
}
@DisableFlags(
@@ -83,12 +93,17 @@ class ActivityStarterImplTest : SysuiTestCase() {
)
@Test
fun registerTransition_doesNotForwardTheRequest_whenFlaggedOff() {
- val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
- val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java)
+ with(kosmos) {
+ testScope.runTest {
+ val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
+ val controllerFactory =
+ mock(ActivityTransitionAnimator.ControllerFactory::class.java)
- underTest.registerTransition(cookie, controllerFactory)
+ underTest.registerTransition(cookie, controllerFactory, testScope)
- verify(activityStarterInternal, never()).registerTransition(any(), any())
+ verify(activityStarterInternal, never()).registerTransition(any(), any(), any())
+ }
+ }
}
@EnableFlags(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
index 5406acf694ff..dfa5c9a26d79 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImplTest.kt
@@ -42,6 +42,7 @@ import com.android.systemui.communal.domain.interactor.CommunalSettingsInteracto
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.keyguard.KeyguardViewMediator
import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.ActivityStarter.OnDismissAction
import com.android.systemui.settings.UserTracker
import com.android.systemui.shade.ShadeController
@@ -58,12 +59,14 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
+import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
@@ -109,6 +112,7 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() {
@Mock private lateinit var communalSceneInteractor: CommunalSceneInteractor
@Mock private lateinit var communalSettingsInteractor: CommunalSettingsInteractor
private lateinit var underTest: LegacyActivityStarterInternalImpl
+ private val kosmos = testKosmos()
private val mainExecutor = FakeExecutor(FakeSystemClock())
private val shadeAnimationInteractor =
ShadeAnimationInteractorLegacyImpl(ShadeAnimationRepository(), FakeShadeRepository())
@@ -157,13 +161,18 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() {
)
@Test
fun registerTransition_registers() {
- val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
- val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java)
- `when`(controllerFactory.cookie).thenReturn(cookie)
+ with(kosmos) {
+ testScope.runTest {
+ val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
+ val controllerFactory =
+ mock(ActivityTransitionAnimator.ControllerFactory::class.java)
+ `when`(controllerFactory.cookie).thenReturn(cookie)
- underTest.registerTransition(cookie, controllerFactory)
+ underTest.registerTransition(cookie, controllerFactory, testScope)
- verify(activityTransitionAnimator).register(eq(cookie), any())
+ verify(activityTransitionAnimator).register(eq(cookie), any(), eq(testScope))
+ }
+ }
}
@DisableFlags(
@@ -172,14 +181,19 @@ class LegacyActivityStarterInternalImplTest : SysuiTestCase() {
)
@Test
fun registerTransition_throws_whenFlagsAreDisabled() {
- val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
- val controllerFactory = mock(ActivityTransitionAnimator.ControllerFactory::class.java)
+ with(kosmos) {
+ testScope.runTest {
+ val cookie = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
+ val controllerFactory =
+ mock(ActivityTransitionAnimator.ControllerFactory::class.java)
- assertThrows(IllegalStateException::class.java) {
- underTest.registerTransition(cookie, controllerFactory)
- }
+ assertThrows(IllegalStateException::class.java) {
+ underTest.registerTransition(cookie, controllerFactory, testScope)
+ }
- verify(activityTransitionAnimator, never()).register(any(), any())
+ verify(activityTransitionAnimator, never()).register(any(), any(), any())
+ }
+ }
}
@EnableFlags(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java
index 75c174229564..ab6fbc3f47a2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -30,7 +30,7 @@ import com.android.wm.shell.bubbles.BubbleData;
import com.android.wm.shell.bubbles.BubbleDataRepository;
import com.android.wm.shell.bubbles.BubbleLogger;
import com.android.wm.shell.bubbles.BubblePositioner;
-import com.android.wm.shell.bubbles.properties.BubbleProperties;
+import com.android.wm.shell.bubbles.ResizabilityChecker;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
@@ -82,14 +82,14 @@ public class TestableBubbleController extends BubbleController {
Transitions transitions,
SyncTransactionQueue syncQueue,
IWindowManager wmService,
- BubbleProperties bubbleProperties) {
+ ResizabilityChecker resizabilityChecker) {
super(context, shellInit, shellCommandHandler, shellController, data, Runnable::run,
floatingContentCoordinator, dataRepository, statusBarService, windowManager,
displayInsetsController, displayImeController, userManager, launcherApps,
bubbleLogger, taskStackListener, shellTaskOrganizer, positioner, displayController,
oneHandedOptional, dragAndDropController, shellMainExecutor, shellMainHandler,
new SyncExecutor(), taskViewRepository, taskViewTransitions, transitions,
- syncQueue, wmService, bubbleProperties);
+ syncQueue, wmService, resizabilityChecker);
setInflateSynchronously(true);
onInit();
}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
index ca98cbf20c3a..18891dba4b0d 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ActivityStarter.java
@@ -25,6 +25,8 @@ import android.view.View;
import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.plugins.annotations.ProvidesInterface;
+import kotlinx.coroutines.CoroutineScope;
+
/**
* An interface to start activities. This is used as a callback from the views to
* {@link PhoneStatusBar} to allow custom handling for starting the activity, i.e. dismissing the
@@ -37,11 +39,12 @@ public interface ActivityStarter {
/**
* Registers the given {@link ActivityTransitionAnimator.ControllerFactory} for launching and
* closing transitions matching the {@link ActivityTransitionAnimator.TransitionCookie} and the
- * {@link ComponentName} that it contains.
+ * {@link ComponentName} that it contains, within the given {@link CoroutineScope}.
*/
void registerTransition(
ActivityTransitionAnimator.TransitionCookie cookie,
- ActivityTransitionAnimator.ControllerFactory controllerFactory);
+ ActivityTransitionAnimator.ControllerFactory controllerFactory,
+ CoroutineScope scope);
/**
* Unregisters the {@link ActivityTransitionAnimator.ControllerFactory} previously registered
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt
index eab7d7913129..be0362fd7481 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt
@@ -16,17 +16,10 @@
package com.android.systemui.plugins.qs
-import androidx.compose.runtime.Composable
-
/**
* The base view model class for rendering the Tile's TileDetailsView.
*/
abstract class TileDetailsViewModel {
-
- // The view content of this tile details view.
- @Composable
- abstract fun GetContentView()
-
// The callback when the settings button is clicked. Currently this is the same as the on tile
// long press callback
abstract fun clickOnSettingsButton()
diff --git a/packages/SystemUI/pods/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/pods/com/android/systemui/util/settings/UserSettingsProxy.kt
index 1a5517059ca4..68ad11e3ec01 100644
--- a/packages/SystemUI/pods/com/android/systemui/util/settings/UserSettingsProxy.kt
+++ b/packages/SystemUI/pods/com/android/systemui/util/settings/UserSettingsProxy.kt
@@ -157,7 +157,11 @@ interface UserSettingsProxy : SettingsProxy {
userHandle: Int,
) =
settingsScope.launch("registerContentObserverForUserAsync-A") {
- registerContentObserverForUserSync(getUriFor(name), settingsObserver, userHandle)
+ try {
+ registerContentObserverForUserSync(getUriFor(name), settingsObserver, userHandle)
+ } catch (e: SecurityException) {
+ throw SecurityException("registerContentObserverForUserAsync-A, name: $name", e)
+ }
}
/** Convenience wrapper around [ContentResolver.registerContentObserver] */
@@ -198,7 +202,11 @@ interface UserSettingsProxy : SettingsProxy {
userHandle: Int,
) =
settingsScope.launch("registerContentObserverForUserAsync-B") {
- registerContentObserverForUserSync(uri, settingsObserver, userHandle)
+ try {
+ registerContentObserverForUserSync(uri, settingsObserver, userHandle)
+ } catch (e: SecurityException) {
+ throw SecurityException("registerContentObserverForUserAsync-B, uri: $uri", e)
+ }
}
/**
@@ -215,7 +223,11 @@ interface UserSettingsProxy : SettingsProxy {
@WorkerThread registered: Runnable,
) =
settingsScope.launch("registerContentObserverForUserAsync-C") {
- registerContentObserverForUserSync(uri, settingsObserver, userHandle)
+ try {
+ registerContentObserverForUserSync(uri, settingsObserver, userHandle)
+ } catch (e: SecurityException) {
+ throw SecurityException("registerContentObserverForUserAsync-C, uri: $uri", e)
+ }
registered.run()
}
@@ -274,12 +286,16 @@ interface UserSettingsProxy : SettingsProxy {
userHandle: Int,
) {
settingsScope.launch("registerContentObserverForUserAsync-D") {
- registerContentObserverForUserSync(
- getUriFor(name),
- notifyForDescendants,
- settingsObserver,
- userHandle,
- )
+ try {
+ registerContentObserverForUserSync(
+ getUriFor(name),
+ notifyForDescendants,
+ settingsObserver,
+ userHandle,
+ )
+ } catch (e: SecurityException) {
+ throw SecurityException("registerContentObserverForUserAsync-D, name: $name", e)
+ }
}
}
@@ -338,12 +354,16 @@ interface UserSettingsProxy : SettingsProxy {
userHandle: Int,
) =
settingsScope.launch("registerContentObserverForUserAsync-E") {
- registerContentObserverForUserSync(
- uri,
- notifyForDescendants,
- settingsObserver,
- userHandle,
- )
+ try {
+ registerContentObserverForUserSync(
+ uri,
+ notifyForDescendants,
+ settingsObserver,
+ userHandle,
+ )
+ } catch (e: SecurityException) {
+ throw SecurityException("registerContentObserverForUserAsync-E, uri: $uri", e)
+ }
}
/**
diff --git a/packages/SystemUI/res/drawable/notif_footer_btn_settings.xml b/packages/SystemUI/res/drawable/notif_footer_btn_settings.xml
index 800060db7757..6533460a9c99 100644
--- a/packages/SystemUI/res/drawable/notif_footer_btn_settings.xml
+++ b/packages/SystemUI/res/drawable/notif_footer_btn_settings.xml
@@ -6,5 +6,5 @@
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
- android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z"/>
+ android:pathData="M480,864q-30,0 -51,-21t-21,-51h144q0,30 -21,51t-51,21ZM192,744v-72h48v-240q0,-88 55.5,-155T439,196q-5,17 -7,34t-2,34q0,59 23.5,109.5T517,461q40,37 92,56t111,14v141h48v72L192,744ZM664,456 L652,400q-14,-5 -26.5,-11.5T602,372l-55,17 -32,-55 41,-40q-3,-14 -3,-29t3,-29l-41,-39 32,-56 54,16q11,-11 24,-18t27,-11l13,-56h64l13,56q14,5 27.5,11.5T793,157l54,-15 32,55 -40,38q3,14 2.5,29.5T838,294l41,39 -32,55 -55,-16q-11,10 -23.5,16.5T742,400l-14,56h-64ZM697,336q30,0 51,-21t21,-51q0,-30 -21,-51t-51,-21q-30,0 -51,21t-21,51q0,30 21,51t51,21Z"/>
</vector>
diff --git a/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml b/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml
index 65cf81ea416b..5954de4012c8 100644
--- a/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml
+++ b/packages/SystemUI/res/layout/notification_stack_scroll_layout.xml
@@ -16,6 +16,7 @@
-->
<!-- This XML is served to be overridden by other OEMs/device types. -->
+<!-- Note: The margins may be overridden in code, see NotificationStackScrollLayout#getBottomMargin -->
<com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml
index ab0f788dbb13..b4383156dc71 100644
--- a/packages/SystemUI/res/values-sw600dp/config.xml
+++ b/packages/SystemUI/res/values-sw600dp/config.xml
@@ -19,7 +19,7 @@
<!-- These resources are around just to allow their values to be customized
for different hardware and product builds. -->
-<resources xmlns:android="http://schemas.android.com/apk/res/android">
+<resources>
<!-- The maximum number of rows in the QuickSettings -->
<integer name="quick_settings_max_rows">4</integer>
@@ -51,9 +51,7 @@
ignored. -->
<string-array name="config_keyguardQuickAffordanceDefaults" translatable="false">
<item>bottom_start:home</item>
- <!-- TODO(b/384119565): revisit decision on defaults -->
- <item android:featureFlag="!com.android.systemui.glanceable_hub_v2_resources">bottom_end:create_note</item>
- <item android:featureFlag="com.android.systemui.glanceable_hub_v2_resources">bottom_end:glanceable_hub</item>
+ <item>bottom_end:create_note</item>
</string-array>
<!-- Whether volume panel should use the large screen layout or not -->
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 42d66e23feb9..a96ebe7b4fd6 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -574,6 +574,8 @@
<dimen name="notification_panel_margin_bottom">32dp</dimen>
+ <dimen name="notification_2025_panel_margin_bottom">64dp</dimen>
+
<!-- The bottom padding of the panel that holds the list of notifications. -->
<dimen name="notification_panel_padding_bottom">0dp</dimen>
@@ -1781,6 +1783,7 @@
<dimen name="wallet_button_vertical_padding">8dp</dimen>
<!-- Ongoing activity chip -->
+ <dimen name="ongoing_activity_chip_min_text_width">12dp</dimen>
<dimen name="ongoing_activity_chip_max_text_width">74dp</dimen>
<dimen name="ongoing_activity_chip_margin_start">5dp</dimen>
<!-- The activity chip side padding, used with the default phone icon. -->
diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt
index 870e6e6bf803..5b99a3f16fc2 100644
--- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt
+++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt
@@ -17,13 +17,6 @@ package com.android.systemui.biometrics
import android.Manifest
import android.app.ActivityTaskManager
-import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
-import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
-import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX
-import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED
-import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
-import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX
-import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Bitmap
@@ -44,6 +37,9 @@ import android.view.WindowMetrics
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD
+import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN
+import com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PIN
import com.android.systemui.biometrics.shared.model.PromptKind
object Utils {
@@ -80,7 +76,7 @@ object Utils {
view.notifySubtreeAccessibilityStateChanged(
view,
view,
- AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE
+ AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE,
)
}
@@ -94,14 +90,10 @@ object Utils {
@JvmStatic
fun getCredentialType(utils: LockPatternUtils, userId: Int): PromptKind =
- when (utils.getKeyguardStoredPasswordQuality(userId)) {
- PASSWORD_QUALITY_SOMETHING -> PromptKind.Pattern
- PASSWORD_QUALITY_NUMERIC,
- PASSWORD_QUALITY_NUMERIC_COMPLEX -> PromptKind.Pin
- PASSWORD_QUALITY_ALPHABETIC,
- PASSWORD_QUALITY_ALPHANUMERIC,
- PASSWORD_QUALITY_COMPLEX,
- PASSWORD_QUALITY_MANAGED -> PromptKind.Password
+ when (utils.getCredentialTypeForUser(userId)) {
+ CREDENTIAL_TYPE_PATTERN -> PromptKind.Pattern
+ CREDENTIAL_TYPE_PIN -> PromptKind.Pin
+ CREDENTIAL_TYPE_PASSWORD -> PromptKind.Password
else -> PromptKind.Password
}
@@ -112,7 +104,7 @@ object Utils {
@JvmStatic
fun <T : SensorPropertiesInternal> findFirstSensorProperties(
properties: List<T>?,
- sensorIds: IntArray
+ sensorIds: IntArray,
): T? = properties?.firstOrNull { sensorIds.contains(it.sensorId) }
@JvmStatic
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index c266a5b47cff..0b66a0ffb711 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -296,7 +296,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
private final Provider<JavaAdapter> mJavaAdapter;
private final Provider<SceneInteractor> mSceneInteractor;
private final Provider<AlternateBouncerInteractor> mAlternateBouncerInteractor;
- private final CommunalSceneInteractor mCommunalSceneInteractor;
+ private final Provider<CommunalSceneInteractor> mCommunalSceneInteractor;
private final AuthController mAuthController;
private final UiEventLogger mUiEventLogger;
private final Set<String> mAllowFingerprintOnOccludingActivitiesFromPackage;
@@ -2210,7 +2210,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
Provider<AlternateBouncerInteractor> alternateBouncerInteractor,
Provider<JavaAdapter> javaAdapter,
Provider<SceneInteractor> sceneInteractor,
- CommunalSceneInteractor communalSceneInteractor) {
+ Provider<CommunalSceneInteractor> communalSceneInteractor) {
mContext = context;
mSubscriptionManager = subscriptionManager;
mUserTracker = userTracker;
@@ -2543,7 +2543,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab
if (glanceableHubV2()) {
mJavaAdapter.get().alwaysCollectFlow(
- mCommunalSceneInteractor.isCommunalVisible(),
+ mCommunalSceneInteractor.get().isCommunalVisible(),
this::onCommunalShowingChanged
);
}
diff --git a/packages/SystemUI/src/com/android/systemui/BootCompleteCacheImpl.kt b/packages/SystemUI/src/com/android/systemui/BootCompleteCacheImpl.kt
index 3a9a238d90a6..cadef52ce4cc 100644
--- a/packages/SystemUI/src/com/android/systemui/BootCompleteCacheImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/BootCompleteCacheImpl.kt
@@ -41,7 +41,7 @@ class BootCompleteCacheImpl @Inject constructor(dumpManager: DumpManager) :
}
init {
- dumpManager.registerDumpable(TAG, this)
+ dumpManager.registerNormalDumpable(TAG, this)
}
@GuardedBy("listeners")
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java
index 615363da073a..db2ca1dbff02 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationAnimationController.java
@@ -203,8 +203,8 @@ class WindowMagnificationAnimationController implements ValueAnimator.AnimatorUp
return;
}
final float currentScale = mController.getScale();
- final float currentCenterX = mController.getCenterX();
- final float currentCenterY = mController.getCenterY();
+ final float currentCenterX = mController.getMagnificationFrameCenterX();
+ final float currentCenterY = mController.getMagnificationFrameCenterY();
if (mState == STATE_DISABLED) {
// We don't need to offset the center during the animation.
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
index 1587ab16fc38..a67ec65cceda 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java
@@ -497,6 +497,9 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
if (configDiff == 0) {
return;
}
+ if (Flags.updateWindowMagnifierBottomBoundary()) {
+ updateSystemGestureInsetsTop();
+ }
if ((configDiff & ActivityInfo.CONFIG_ORIENTATION) != 0) {
onRotate();
}
@@ -542,8 +545,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
}
mWindowBounds.set(currentWindowBounds);
final Size windowFrameSize = restoreMagnificationWindowFrameIndexAndSizeIfPossible();
- final float newCenterX = (getCenterX()) * mWindowBounds.width() / oldWindowBounds.width();
- final float newCenterY = (getCenterY()) * mWindowBounds.height() / oldWindowBounds.height();
+ final float newCenterX =
+ (getMagnificationFrameCenterX()) * mWindowBounds.width() / oldWindowBounds.width();
+ final float newCenterY =
+ (getMagnificationFrameCenterY()) * mWindowBounds.height()
+ / oldWindowBounds.height();
setMagnificationFrame(windowFrameSize.getWidth(), windowFrameSize.getHeight(),
(int) newCenterX, (int) newCenterY);
@@ -672,8 +678,12 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
}
private void onWindowInsetChanged() {
- if (updateSystemGestureInsetsTop()) {
- updateSystemUIStateIfNeeded();
+ if (Flags.updateWindowMagnifierBottomBoundary()) {
+ updateSystemGestureInsetsTop();
+ } else {
+ if (updateSystemGestureInsetsTop()) {
+ updateSystemUIStateIfNeeded();
+ }
}
}
@@ -939,7 +949,9 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
final int x = MathUtils.clamp(mMagnificationFrame.left - mMirrorSurfaceMargin, minX, maxX);
final int minY = -mOuterBorderSize;
- final int maxY = mWindowBounds.bottom - height + mOuterBorderSize;
+ final int maxY = Flags.updateWindowMagnifierBottomBoundary()
+ ? mSystemGestureTop - height + mOuterBorderSize
+ : mWindowBounds.bottom - height + mOuterBorderSize;
final int y = MathUtils.clamp(mMagnificationFrame.top - mMirrorSurfaceMargin, minY, maxY);
if (computeWindowSize) {
@@ -1098,6 +1110,10 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
}
private void updateSysUIState(boolean force) {
+ if (Flags.updateWindowMagnifierBottomBoundary()) {
+ return;
+ }
+
final boolean overlap = isActivated() && mSystemGestureTop > 0
&& mMirrorViewBounds.bottom > mSystemGestureTop;
if (force || overlap != mOverlapWithGestureInsets) {
@@ -1313,7 +1329,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
*
* @return the X coordinate. {@link Float#NaN} if the window is invisible.
*/
- float getCenterX() {
+ float getMagnificationFrameCenterX() {
return isActivated() ? mMagnificationFrame.exactCenterX() : Float.NaN;
}
@@ -1322,10 +1338,30 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold
*
* @return the Y coordinate. {@link Float#NaN} if the window is invisible.
*/
- float getCenterY() {
+ float getMagnificationFrameCenterY() {
return isActivated() ? mMagnificationFrame.exactCenterY() : Float.NaN;
}
+ /**
+ * Returns the screen-relative X coordinate of the center of the magnifier window.
+ * This could be different from the position of the magnification frame since the magnification
+ * frame could overlap with the bottom inset, but the magnifier window would not.
+ * @return the Y coordinate. {@link Float#NaN} if the window is invisible.
+ */
+ float getMagnifierWindowX() {
+ return isActivated() ? (float) mMirrorViewBounds.left : Float.NaN;
+ }
+
+ /**
+ * Returns the screen-relative Y coordinate of the center of the magnifier window.
+ * This could be different from the position of the magnification frame since the magnification
+ * frame could overlap with the bottom inset, but the magnifier window would not.
+ * @return the Y coordinate. {@link Float#NaN} if the window is invisible.
+ */
+ float getMagnifierWindowY() {
+ return isActivated() ? (float) mMirrorViewBounds.top : Float.NaN;
+ }
+
@VisibleForTesting
boolean isDiagonalScrollingEnabled() {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
index 21569c13fab4..64a46c0d7f00 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt
@@ -131,7 +131,7 @@ abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>(
override fun onViewAttached() {
dialogManager.registerListener(dialogListener)
- dumpManager.registerDumpable(dumpTag, this)
+ dumpManager.registerNormalDumpable(dumpTag, this)
udfpsOverlayInteractor.setHandleTouches(shouldHandle = !shouldPauseAuth())
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
index 08b3e99fadd0..84d3d7f81c25 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt
@@ -72,10 +72,9 @@ constructor(
// Request LockSettingsService to return the Gatekeeper Password in the
// VerifyCredentialResponse so that we can request a Gatekeeper HAT with the
// Gatekeeper Password and operationId.
- var effectiveUserId = request.userInfo.deviceCredentialOwnerId
+ val effectiveUserId = request.userInfo.deviceCredentialOwnerId
val response =
if (Flags.privateSpaceBp() && effectiveUserId != request.userInfo.userId) {
- effectiveUserId = request.userInfo.userId
lockPatternUtils.verifyTiedProfileChallenge(
credential,
request.userInfo.userId,
@@ -101,7 +100,7 @@ constructor(
lockPatternUtils.verifyGatekeeperPasswordHandle(
pwHandle,
request.operationInfo.gatekeeperChallenge,
- effectiveUserId,
+ request.userInfo.userId,
)
val hat = gkResponse.gatekeeperHAT
lockPatternUtils.removeGatekeeperPasswordHandle(pwHandle)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
index 008fb26424e2..a164ff483e47 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
@@ -126,7 +126,12 @@ constructor(
is PromptKind.Biometric ->
BiometricPromptRequest.Biometric(
info = promptInfo,
- userInfo = BiometricUserInfo(userId = userId),
+ userInfo =
+ BiometricUserInfo(
+ userId = userId,
+ deviceCredentialOwnerId =
+ credentialInteractor.getCredentialOwnerOrSelfId(userId),
+ ),
operationInfo = BiometricOperationInfo(gatekeeperChallenge = challenge),
modalities = kind.activeModalities,
opPackageName = opPackageName,
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 e7a68ac2cfb9..8d5ea3c21e29 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
@@ -12,6 +12,7 @@ import android.window.OnBackInvokedDispatcher
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.biometrics.ui.CredentialPasswordView
import com.android.systemui.biometrics.ui.CredentialView
import com.android.systemui.biometrics.ui.IPinPad
@@ -21,7 +22,6 @@ import com.android.systemui.res.R
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.firstOrNull
-import com.android.app.tracing.coroutines.launchTraced as launch
/** Sub-binder for the [CredentialPasswordView]. */
object CredentialPasswordViewBinder {
@@ -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.userIdForPasswordEntry))
+ passwordField.setTextOperationUser(UserHandle.of(header.user.deviceCredentialOwnerId))
viewModel.inputFlags.firstOrNull()?.let { flags -> passwordField.inputType = flags }
if (requestFocusForInput) {
passwordField.requestFocus()
@@ -65,7 +65,7 @@ object CredentialPasswordViewBinder {
if (attestation != null) {
imeManager.hideSoftInputFromWindow(
view.windowToken,
- 0 // flag
+ 0, // flag
)
host.onCredentialMatched(attestation)
} else {
@@ -79,7 +79,7 @@ object CredentialPasswordViewBinder {
launch {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
- onBackInvokedCallback
+ onBackInvokedCallback,
)
awaitCancellation()
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
index 0303048436c9..94fca218c74f 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
@@ -53,7 +53,7 @@ constructor(
private val deviceItemActionInteractorImpl: DeviceItemActionInteractorImpl,
) : DeviceItemActionInteractor {
- override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
+ override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog?) {
withContext(backgroundDispatcher) {
if (!audioSharingInteractor.audioSharingAvailable()) {
return@withContext deviceItemActionInteractorImpl.onClick(deviceItem, dialog)
@@ -70,10 +70,18 @@ constructor(
DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
if (audioSharingInteractor.qsDialogImprovementAvailable()) {
withContext(mainDispatcher) {
- delegateFactory
- .create(deviceItem.cachedBluetoothDevice)
- .createDialog()
- .let { dialogTransitionAnimator.showFromDialog(it, dialog) }
+ val audioSharingDialog =
+ delegateFactory
+ .create(deviceItem.cachedBluetoothDevice)
+ .createDialog()
+
+ if (dialog != null) {
+ audioSharingDialog.let {
+ dialogTransitionAnimator.showFromDialog(it, dialog)
+ }
+ } else {
+ audioSharingDialog.show()
+ }
}
} else {
launchSettings(deviceItem.cachedBluetoothDevice.device, dialog)
@@ -141,7 +149,7 @@ constructor(
)
}
- private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) {
+ private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog?) {
val intent =
Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply {
putExtra(
@@ -155,7 +163,8 @@ constructor(
activityStarter.postStartActivityDismissingKeyguard(
intent,
0,
- dialogTransitionAnimator.createActivityTransitionController(dialog),
+ if (dialog == null) null
+ else dialogTransitionAnimator.createActivityTransitionController(dialog),
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContent.kt
new file mode 100644
index 000000000000..8bc929985052
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContent.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.bluetooth.qsdialog
+
+import android.view.LayoutInflater
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.systemui.res.R
+
+@Composable
+fun BluetoothDetailsContent() {
+ AndroidView(
+ modifier = Modifier.fillMaxSize(),
+ factory = { context ->
+ // Inflate with the existing dialog xml layout
+ LayoutInflater.from(context).inflate(R.layout.bluetooth_tile_dialog, null)
+ // TODO: b/378513956 - Implement the bluetooth details view
+ },
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt
new file mode 100644
index 000000000000..0be28f3c5a97
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt
@@ -0,0 +1,442 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.AccessibilityDelegate
+import android.view.View.GONE
+import android.view.View.INVISIBLE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.view.accessibility.AccessibilityNodeInfo
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.ProgressBar
+import android.widget.Switch
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.recyclerview.widget.AsyncListDiffer
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.internal.R as InternalR
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.android.systemui.util.time.SystemClock
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.withContext
+
+data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, val target: Target) {
+ enum class Target {
+ ENTIRE_ROW,
+ ACTION_ICON,
+ }
+}
+
+/** View content manager for showing active, connected and saved bluetooth devices. */
+class BluetoothDetailsContentManager
+@AssistedInject
+internal constructor(
+ @Assisted private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties,
+ @Assisted private val cachedContentHeight: Int,
+ @Assisted private val bluetoothTileDialogCallback: BluetoothTileDialogCallback,
+ @Assisted private val isInDialog: Boolean,
+ @Assisted private val doneButtonCallback: () -> Unit,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ private val systemClock: SystemClock,
+ private val uiEventLogger: UiEventLogger,
+ private val logger: BluetoothTileDialogLogger,
+) {
+
+ private val mutableBluetoothStateToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null)
+ internal val bluetoothStateToggle
+ get() = mutableBluetoothStateToggle.asStateFlow()
+
+ private val mutableBluetoothAutoOnToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null)
+ internal val bluetoothAutoOnToggle
+ get() = mutableBluetoothAutoOnToggle.asStateFlow()
+
+ private val mutableDeviceItemClick: MutableStateFlow<DeviceItemClick?> = MutableStateFlow(null)
+ internal val deviceItemClick
+ get() = mutableDeviceItemClick.asStateFlow()
+
+ private val mutableContentHeight: MutableStateFlow<Int?> = MutableStateFlow(null)
+ internal val contentHeight
+ get() = mutableContentHeight.asStateFlow()
+
+ private val deviceItemAdapter: Adapter = Adapter()
+
+ private var lastUiUpdateMs: Long = -1
+
+ private var lastItemRow: Int = -1
+
+ // UI Components
+ private lateinit var contentView: View
+ private lateinit var doneButton: Button
+ private lateinit var bluetoothToggle: Switch
+ private lateinit var subtitleTextView: TextView
+ private lateinit var seeAllButton: View
+ private lateinit var pairNewDeviceButton: View
+ private lateinit var deviceListView: RecyclerView
+ private lateinit var autoOnToggle: Switch
+ private lateinit var autoOnToggleLayout: View
+ private lateinit var autoOnToggleInfoTextView: TextView
+ private lateinit var audioSharingButton: Button
+ private lateinit var progressBarAnimation: ProgressBar
+ private lateinit var progressBarBackground: View
+ private lateinit var scrollViewContent: View
+
+ @AssistedFactory
+ internal interface Factory {
+ fun create(
+ initialUiProperties: BluetoothTileDialogViewModel.UiProperties,
+ cachedContentHeight: Int,
+ dialogCallback: BluetoothTileDialogCallback,
+ isInDialog: Boolean,
+ doneButtonCallback: () -> Unit,
+ ): BluetoothDetailsContentManager
+ }
+
+ fun bind(contentView: View) {
+ this.contentView = contentView
+
+ doneButton = contentView.requireViewById(R.id.done_button)
+ bluetoothToggle = contentView.requireViewById(R.id.bluetooth_toggle)
+ subtitleTextView = contentView.requireViewById(R.id.bluetooth_tile_dialog_subtitle)
+ seeAllButton = contentView.requireViewById(R.id.see_all_button)
+ pairNewDeviceButton = contentView.requireViewById(R.id.pair_new_device_button)
+ deviceListView = contentView.requireViewById(R.id.device_list)
+ autoOnToggle = contentView.requireViewById(R.id.bluetooth_auto_on_toggle)
+ autoOnToggleLayout = contentView.requireViewById(R.id.bluetooth_auto_on_toggle_layout)
+ autoOnToggleInfoTextView =
+ contentView.requireViewById(R.id.bluetooth_auto_on_toggle_info_text)
+ audioSharingButton = contentView.requireViewById(R.id.audio_sharing_button)
+ progressBarAnimation =
+ contentView.requireViewById(R.id.bluetooth_tile_dialog_progress_animation)
+ progressBarBackground =
+ contentView.requireViewById(R.id.bluetooth_tile_dialog_progress_background)
+ scrollViewContent = contentView.requireViewById(R.id.scroll_view)
+
+ setupToggle()
+ setupRecyclerView()
+ setupDoneButton()
+
+ subtitleTextView.text = contentView.context.getString(initialUiProperties.subTitleResId)
+ seeAllButton.setOnClickListener { bluetoothTileDialogCallback.onSeeAllClicked(it) }
+ pairNewDeviceButton.setOnClickListener {
+ bluetoothTileDialogCallback.onPairNewDeviceClicked(it)
+ }
+ audioSharingButton.apply {
+ setOnClickListener { bluetoothTileDialogCallback.onAudioSharingButtonClicked(it) }
+ accessibilityDelegate =
+ object : AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfo,
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ info.addAction(
+ AccessibilityAction(
+ AccessibilityAction.ACTION_CLICK.id,
+ contentView.context.getString(
+ R.string
+ .quick_settings_bluetooth_audio_sharing_button_accessibility
+ ),
+ )
+ )
+ }
+ }
+ }
+ scrollViewContent.apply {
+ minimumHeight =
+ resources.getDimensionPixelSize(initialUiProperties.scrollViewMinHeightResId)
+ layoutParams.height = maxOf(cachedContentHeight, minimumHeight)
+ }
+ }
+
+ fun start() {
+ lastUiUpdateMs = systemClock.elapsedRealtime()
+ }
+
+ fun releaseView() {
+ mutableContentHeight.value = scrollViewContent.measuredHeight
+ }
+
+ internal suspend fun animateProgressBar(animate: Boolean) {
+ withContext(mainDispatcher) {
+ if (animate) {
+ showProgressBar()
+ } else {
+ delay(PROGRESS_BAR_ANIMATION_DURATION_MS)
+ hideProgressBar()
+ }
+ }
+ }
+
+ internal suspend fun onDeviceItemUpdated(
+ deviceItem: List<DeviceItem>,
+ showSeeAll: Boolean,
+ showPairNewDevice: Boolean,
+ ) {
+ withContext(mainDispatcher) {
+ val start = systemClock.elapsedRealtime()
+ val itemRow = deviceItem.size + showSeeAll.toInt() + showPairNewDevice.toInt()
+ // If not the first load, add a slight delay for smoother dialog height change
+ if (itemRow != lastItemRow && lastItemRow != -1) {
+ delay(MIN_HEIGHT_CHANGE_INTERVAL_MS - (start - lastUiUpdateMs))
+ }
+ if (isActive) {
+ deviceItemAdapter.refreshDeviceItemList(deviceItem) {
+ seeAllButton.visibility = if (showSeeAll) VISIBLE else GONE
+ pairNewDeviceButton.visibility = if (showPairNewDevice) VISIBLE else GONE
+ // Update the height after data is updated
+ scrollViewContent.layoutParams.height = WRAP_CONTENT
+ lastUiUpdateMs = systemClock.elapsedRealtime()
+ lastItemRow = itemRow
+ logger.logDeviceUiUpdate(lastUiUpdateMs - start)
+ }
+ }
+ }
+ }
+
+ internal fun onBluetoothStateUpdated(
+ isEnabled: Boolean,
+ uiProperties: BluetoothTileDialogViewModel.UiProperties,
+ ) {
+ bluetoothToggle.apply {
+ isChecked = isEnabled
+ setEnabled(true)
+ alpha = ENABLED_ALPHA
+ }
+ subtitleTextView.text = contentView.context.getString(uiProperties.subTitleResId)
+ autoOnToggleLayout.visibility = uiProperties.autoOnToggleVisibility
+ }
+
+ internal fun onBluetoothAutoOnUpdated(isEnabled: Boolean, @StringRes infoResId: Int) {
+ autoOnToggle.isChecked = isEnabled
+ autoOnToggleInfoTextView.text = contentView.context.getString(infoResId)
+ }
+
+ internal fun onAudioSharingButtonUpdated(visibility: Int, label: String?, isActive: Boolean) {
+ audioSharingButton.apply {
+ this.visibility = visibility
+ label?.let { text = it }
+ this.isActivated = isActive
+ }
+ }
+
+ private fun setupToggle() {
+ bluetoothToggle.setOnCheckedChangeListener { view, isChecked ->
+ mutableBluetoothStateToggle.value = isChecked
+ view.apply {
+ isEnabled = false
+ alpha = DISABLED_ALPHA
+ }
+ logger.logBluetoothState(BluetoothStateStage.USER_TOGGLED, isChecked.toString())
+ uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED)
+ }
+
+ autoOnToggleLayout.visibility = initialUiProperties.autoOnToggleVisibility
+ autoOnToggle.setOnCheckedChangeListener { _, isChecked ->
+ mutableBluetoothAutoOnToggle.value = isChecked
+ uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUTO_ON_TOGGLE_CLICKED)
+ }
+ }
+
+ private fun setupDoneButton() {
+ if (isInDialog) {
+ doneButton.setOnClickListener { doneButtonCallback() }
+ } else {
+ doneButton.visibility = GONE
+ }
+ }
+
+ private fun setupRecyclerView() {
+ deviceListView.apply {
+ layoutManager = LinearLayoutManager(contentView.context)
+ adapter = deviceItemAdapter
+ }
+ }
+
+ private fun showProgressBar() {
+ if (progressBarAnimation.visibility != VISIBLE) {
+ progressBarAnimation.visibility = VISIBLE
+ progressBarBackground.visibility = INVISIBLE
+ }
+ }
+
+ private fun hideProgressBar() {
+ if (progressBarAnimation.visibility != INVISIBLE) {
+ progressBarAnimation.visibility = INVISIBLE
+ progressBarBackground.visibility = VISIBLE
+ }
+ }
+
+ internal inner class Adapter : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
+
+ private val diffUtilCallback =
+ object : DiffUtil.ItemCallback<DeviceItem>() {
+ override fun areItemsTheSame(
+ deviceItem1: DeviceItem,
+ deviceItem2: DeviceItem,
+ ): Boolean {
+ return deviceItem1.cachedBluetoothDevice == deviceItem2.cachedBluetoothDevice
+ }
+
+ override fun areContentsTheSame(
+ deviceItem1: DeviceItem,
+ deviceItem2: DeviceItem,
+ ): Boolean {
+ return deviceItem1.type == deviceItem2.type &&
+ deviceItem1.cachedBluetoothDevice == deviceItem2.cachedBluetoothDevice &&
+ deviceItem1.deviceName == deviceItem2.deviceName &&
+ deviceItem1.connectionSummary == deviceItem2.connectionSummary &&
+ // Ignored the icon drawable
+ deviceItem1.iconWithDescription?.second ==
+ deviceItem2.iconWithDescription?.second &&
+ deviceItem1.background == deviceItem2.background &&
+ deviceItem1.isEnabled == deviceItem2.isEnabled &&
+ deviceItem1.actionAccessibilityLabel == deviceItem2.actionAccessibilityLabel
+ }
+ }
+
+ private val asyncListDiffer = AsyncListDiffer(this, diffUtilCallback)
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceItemViewHolder {
+ val view =
+ LayoutInflater.from(parent.context)
+ .inflate(R.layout.bluetooth_device_item, parent, false)
+ return DeviceItemViewHolder(view)
+ }
+
+ override fun getItemCount() = asyncListDiffer.currentList.size
+
+ override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) {
+ val item = getItem(position)
+ holder.bind(item)
+ }
+
+ internal fun getItem(position: Int) = asyncListDiffer.currentList[position]
+
+ internal fun refreshDeviceItemList(updated: List<DeviceItem>, callback: () -> Unit) {
+ asyncListDiffer.submitList(updated, callback)
+ }
+
+ internal inner class DeviceItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ private val container = view.requireViewById<View>(R.id.bluetooth_device_row)
+ private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name)
+ private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary)
+ private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon)
+ private val actionIcon = view.requireViewById<ImageView>(R.id.gear_icon_image)
+ private val actionIconView = view.requireViewById<View>(R.id.gear_icon)
+ private val divider = view.requireViewById<View>(R.id.divider)
+
+ internal fun bind(item: DeviceItem) {
+ container.apply {
+ isEnabled = item.isEnabled
+ background = item.background?.let { context.getDrawable(it) }
+ setOnClickListener {
+ mutableDeviceItemClick.value =
+ DeviceItemClick(item, it, DeviceItemClick.Target.ENTIRE_ROW)
+ uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED)
+ }
+
+ // updating icon colors
+ val tintColor =
+ context.getColor(
+ if (item.isActive) InternalR.color.materialColorOnPrimaryContainer
+ else InternalR.color.materialColorOnSurface
+ )
+
+ // update icons
+ iconView.apply {
+ item.iconWithDescription?.let {
+ setImageDrawable(it.first)
+ contentDescription = it.second
+ }
+ }
+
+ actionIcon.setImageResource(item.actionIconRes)
+ actionIcon.drawable?.setTint(tintColor)
+
+ divider.setBackgroundColor(tintColor)
+
+ // update text styles
+ nameView.setTextAppearance(
+ if (item.isActive) R.style.TextAppearance_BluetoothTileDialog_Active
+ else R.style.TextAppearance_BluetoothTileDialog
+ )
+ summaryView.setTextAppearance(
+ if (item.isActive) R.style.TextAppearance_BluetoothTileDialog_Active
+ else R.style.TextAppearance_BluetoothTileDialog
+ )
+
+ accessibilityDelegate =
+ object : AccessibilityDelegate() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfo,
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ info.addAction(
+ AccessibilityAction(
+ AccessibilityAction.ACTION_CLICK.id,
+ item.actionAccessibilityLabel,
+ )
+ )
+ }
+ }
+ }
+ nameView.text = item.deviceName
+ summaryView.text = item.connectionSummary
+
+ actionIconView.setOnClickListener {
+ mutableDeviceItemClick.value =
+ DeviceItemClick(item, it, DeviceItemClick.Target.ACTION_ICON)
+ }
+ }
+ }
+ }
+
+ internal companion object {
+ const val MIN_HEIGHT_CHANGE_INTERVAL_MS = 800L
+ const val ACTION_BLUETOOTH_DEVICE_DETAILS =
+ "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"
+ const val ACTION_PREVIOUSLY_CONNECTED_DEVICE =
+ "com.android.settings.PREVIOUSLY_CONNECTED_DEVICE"
+ const val ACTION_PAIR_NEW_DEVICE = "android.settings.BLUETOOTH_PAIRING_SETTINGS"
+ const val ACTION_AUDIO_SHARING = "com.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS"
+ const val DISABLED_ALPHA = 0.3f
+ const val ENABLED_ALPHA = 1f
+ const val PROGRESS_BAR_ANIMATION_DURATION_MS = 1500L
+
+ private fun Boolean.toInt(): Int {
+ return if (this) 1 else 0
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
index 9dd3b6de423e..ac4d82a95834 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
@@ -16,30 +16,11 @@
package com.android.systemui.bluetooth.qsdialog
-import android.view.LayoutInflater
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.viewinterop.AndroidView
import com.android.systemui.plugins.qs.TileDetailsViewModel
-import com.android.systemui.res.R
class BluetoothDetailsViewModel(onLongClick: () -> Unit) : TileDetailsViewModel() {
private val _onLongClick = onLongClick
- @Composable
- override fun GetContentView() {
- AndroidView(
- modifier = Modifier.fillMaxWidth().fillMaxHeight(),
- factory = { context ->
- // Inflate with the existing dialog xml layout
- LayoutInflater.from(context).inflate(R.layout.bluetooth_tile_dialog, null)
- // TODO: b/378513956 - Implement the bluetooth details view
- },
- )
- }
-
override fun clickOnSettingsButton() {
_onLongClick()
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
index 56caddfbd637..3e61c45c7f25 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
@@ -18,50 +18,14 @@ package com.android.systemui.bluetooth.qsdialog
import android.os.Bundle
import android.view.LayoutInflater
-import android.view.View
-import android.view.View.AccessibilityDelegate
-import android.view.View.GONE
-import android.view.View.INVISIBLE
-import android.view.View.VISIBLE
-import android.view.ViewGroup
-import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
-import android.view.accessibility.AccessibilityNodeInfo
-import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
-import android.widget.Button
-import android.widget.ImageView
-import android.widget.ProgressBar
-import android.widget.Switch
-import android.widget.TextView
-import androidx.annotation.StringRes
-import androidx.recyclerview.widget.AsyncListDiffer
-import androidx.recyclerview.widget.DiffUtil
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import com.android.internal.R as InternalR
import com.android.internal.logging.UiEventLogger
-import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.flags.QsDetailedView
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor
import com.android.systemui.statusbar.phone.SystemUIDialog
-import com.android.systemui.util.time.SystemClock
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.withContext
-
-data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, val target: Target) {
- enum class Target {
- ENTIRE_ROW,
- ACTION_ICON,
- }
-}
/** Dialog for showing active, connected and saved bluetooth devices. */
class BluetoothTileDialogDelegate
@@ -71,37 +35,13 @@ internal constructor(
@Assisted private val cachedContentHeight: Int,
@Assisted private val bluetoothTileDialogCallback: BluetoothTileDialogCallback,
@Assisted private val dismissListener: Runnable,
- @Main private val mainDispatcher: CoroutineDispatcher,
- private val systemClock: SystemClock,
private val uiEventLogger: UiEventLogger,
- private val logger: BluetoothTileDialogLogger,
private val systemuiDialogFactory: SystemUIDialog.Factory,
private val shadeDialogContextInteractor: ShadeDialogContextInteractor,
+ private val bluetoothDetailsContentManagerFactory: BluetoothDetailsContentManager.Factory,
) : SystemUIDialog.Delegate {
- private val mutableBluetoothStateToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null)
- internal val bluetoothStateToggle
- get() = mutableBluetoothStateToggle.asStateFlow()
-
- private val mutableBluetoothAutoOnToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null)
- internal val bluetoothAutoOnToggle
- get() = mutableBluetoothAutoOnToggle.asStateFlow()
-
- private val mutableDeviceItemClick: MutableSharedFlow<DeviceItemClick> =
- MutableSharedFlow(extraBufferCapacity = 1)
- internal val deviceItemClick
- get() = mutableDeviceItemClick.asSharedFlow()
-
- private val mutableContentHeight: MutableSharedFlow<Int> =
- MutableSharedFlow(extraBufferCapacity = 1)
- internal val contentHeight
- get() = mutableContentHeight.asSharedFlow()
-
- private val deviceItemAdapter: Adapter = Adapter()
-
- private var lastUiUpdateMs: Long = -1
-
- private var lastItemRow: Int = -1
+ lateinit var contentManager: BluetoothDetailsContentManager
@AssistedFactory
internal interface Factory {
@@ -114,6 +54,9 @@ internal constructor(
}
override fun createDialog(): SystemUIDialog {
+ // If `QsDetailedView` is enabled, it should show the details view.
+ QsDetailedView.assertInLegacyMode()
+
return systemuiDialogFactory.create(this, shadeDialogContextInteractor.context)
}
@@ -127,362 +70,24 @@ internal constructor(
dialog.setContentView(this)
}
- setupToggle(dialog)
- setupRecyclerView(dialog)
-
- getSubtitleTextView(dialog).text = context.getString(initialUiProperties.subTitleResId)
- dialog.requireViewById<View>(R.id.done_button).setOnClickListener { dialog.dismiss() }
- getSeeAllButton(dialog).setOnClickListener {
- bluetoothTileDialogCallback.onSeeAllClicked(it)
- }
- getPairNewDeviceButton(dialog).setOnClickListener {
- bluetoothTileDialogCallback.onPairNewDeviceClicked(it)
- }
- getAudioSharingButtonView(dialog).apply {
- setOnClickListener { bluetoothTileDialogCallback.onAudioSharingButtonClicked(it) }
- accessibilityDelegate =
- object : AccessibilityDelegate() {
- override fun onInitializeAccessibilityNodeInfo(
- host: View,
- info: AccessibilityNodeInfo,
- ) {
- super.onInitializeAccessibilityNodeInfo(host, info)
- info.addAction(
- AccessibilityAction(
- AccessibilityAction.ACTION_CLICK.id,
- context.getString(
- R.string
- .quick_settings_bluetooth_audio_sharing_button_accessibility
- ),
- )
- )
- }
- }
- }
- getScrollViewContent(dialog).apply {
- minimumHeight =
- resources.getDimensionPixelSize(initialUiProperties.scrollViewMinHeightResId)
- layoutParams.height = maxOf(cachedContentHeight, minimumHeight)
- }
+ contentManager =
+ bluetoothDetailsContentManagerFactory.create(
+ initialUiProperties,
+ cachedContentHeight,
+ bluetoothTileDialogCallback,
+ /* isInDialog= */ true,
+ /* doneButtonCallback= */ fun() {
+ dialog.dismiss()
+ },
+ )
+ contentManager.bind(dialog.requireViewById(R.id.root))
}
override fun onStart(dialog: SystemUIDialog) {
- lastUiUpdateMs = systemClock.elapsedRealtime()
+ contentManager.start()
}
override fun onStop(dialog: SystemUIDialog) {
- mutableContentHeight.tryEmit(getScrollViewContent(dialog).measuredHeight)
- }
-
- internal suspend fun animateProgressBar(dialog: SystemUIDialog, animate: Boolean) {
- withContext(mainDispatcher) {
- if (animate) {
- showProgressBar(dialog)
- } else {
- delay(PROGRESS_BAR_ANIMATION_DURATION_MS)
- hideProgressBar(dialog)
- }
- }
- }
-
- internal suspend fun onDeviceItemUpdated(
- dialog: SystemUIDialog,
- deviceItem: List<DeviceItem>,
- showSeeAll: Boolean,
- showPairNewDevice: Boolean,
- ) {
- withContext(mainDispatcher) {
- val start = systemClock.elapsedRealtime()
- val itemRow = deviceItem.size + showSeeAll.toInt() + showPairNewDevice.toInt()
- // If not the first load, add a slight delay for smoother dialog height change
- if (itemRow != lastItemRow && lastItemRow != -1) {
- delay(MIN_HEIGHT_CHANGE_INTERVAL_MS - (start - lastUiUpdateMs))
- }
- if (isActive) {
- deviceItemAdapter.refreshDeviceItemList(deviceItem) {
- getSeeAllButton(dialog).visibility = if (showSeeAll) VISIBLE else GONE
- getPairNewDeviceButton(dialog).visibility =
- if (showPairNewDevice) VISIBLE else GONE
- // Update the height after data is updated
- getScrollViewContent(dialog).layoutParams.height = WRAP_CONTENT
- lastUiUpdateMs = systemClock.elapsedRealtime()
- lastItemRow = itemRow
- logger.logDeviceUiUpdate(lastUiUpdateMs - start)
- }
- }
- }
- }
-
- internal fun onBluetoothStateUpdated(
- dialog: SystemUIDialog,
- isEnabled: Boolean,
- uiProperties: BluetoothTileDialogViewModel.UiProperties,
- ) {
- getToggleView(dialog).apply {
- isChecked = isEnabled
- setEnabled(true)
- alpha = ENABLED_ALPHA
- }
- getSubtitleTextView(dialog).text = dialog.context.getString(uiProperties.subTitleResId)
- getAutoOnToggleView(dialog).visibility = uiProperties.autoOnToggleVisibility
- }
-
- internal fun onBluetoothAutoOnUpdated(
- dialog: SystemUIDialog,
- isEnabled: Boolean,
- @StringRes infoResId: Int,
- ) {
- getAutoOnToggle(dialog).isChecked = isEnabled
- getAutoOnToggleInfoTextView(dialog).text = dialog.context.getString(infoResId)
- }
-
- internal fun onAudioSharingButtonUpdated(
- dialog: SystemUIDialog,
- visibility: Int,
- label: String?,
- isActive: Boolean,
- ) {
- getAudioSharingButtonView(dialog).apply {
- this.visibility = visibility
- label?.let { text = it }
- this.isActivated = isActive
- }
- }
-
- private fun setupToggle(dialog: SystemUIDialog) {
- val toggleView = getToggleView(dialog)
- toggleView.setOnCheckedChangeListener { view, isChecked ->
- mutableBluetoothStateToggle.value = isChecked
- view.apply {
- isEnabled = false
- alpha = DISABLED_ALPHA
- }
- logger.logBluetoothState(BluetoothStateStage.USER_TOGGLED, isChecked.toString())
- uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED)
- }
-
- getAutoOnToggleView(dialog).visibility = initialUiProperties.autoOnToggleVisibility
- getAutoOnToggle(dialog).setOnCheckedChangeListener { _, isChecked ->
- mutableBluetoothAutoOnToggle.value = isChecked
- uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUTO_ON_TOGGLE_CLICKED)
- }
- }
-
- private fun getToggleView(dialog: SystemUIDialog): Switch {
- return dialog.requireViewById(R.id.bluetooth_toggle)
- }
-
- private fun getSubtitleTextView(dialog: SystemUIDialog): TextView {
- return dialog.requireViewById(R.id.bluetooth_tile_dialog_subtitle)
- }
-
- private fun getSeeAllButton(dialog: SystemUIDialog): View {
- return dialog.requireViewById(R.id.see_all_button)
- }
-
- private fun getPairNewDeviceButton(dialog: SystemUIDialog): View {
- return dialog.requireViewById(R.id.pair_new_device_button)
- }
-
- private fun getDeviceListView(dialog: SystemUIDialog): RecyclerView {
- return dialog.requireViewById(R.id.device_list)
- }
-
- private fun getAutoOnToggle(dialog: SystemUIDialog): Switch {
- return dialog.requireViewById(R.id.bluetooth_auto_on_toggle)
- }
-
- private fun getAudioSharingButtonView(dialog: SystemUIDialog): Button {
- return dialog.requireViewById(R.id.audio_sharing_button)
- }
-
- private fun getAutoOnToggleView(dialog: SystemUIDialog): View {
- return dialog.requireViewById(R.id.bluetooth_auto_on_toggle_layout)
- }
-
- private fun getAutoOnToggleInfoTextView(dialog: SystemUIDialog): TextView {
- return dialog.requireViewById(R.id.bluetooth_auto_on_toggle_info_text)
- }
-
- private fun getProgressBarAnimation(dialog: SystemUIDialog): ProgressBar {
- return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_animation)
- }
-
- private fun getProgressBarBackground(dialog: SystemUIDialog): View {
- return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_background)
- }
-
- private fun getScrollViewContent(dialog: SystemUIDialog): View {
- return dialog.requireViewById(R.id.scroll_view)
- }
-
- private fun setupRecyclerView(dialog: SystemUIDialog) {
- getDeviceListView(dialog).apply {
- layoutManager = LinearLayoutManager(dialog.context)
- adapter = deviceItemAdapter
- }
- }
-
- private fun showProgressBar(dialog: SystemUIDialog) {
- val progressBarAnimation = getProgressBarAnimation(dialog)
- val progressBarBackground = getProgressBarBackground(dialog)
- if (progressBarAnimation.visibility != VISIBLE) {
- progressBarAnimation.visibility = VISIBLE
- progressBarBackground.visibility = INVISIBLE
- }
- }
-
- private fun hideProgressBar(dialog: SystemUIDialog) {
- val progressBarAnimation = getProgressBarAnimation(dialog)
- val progressBarBackground = getProgressBarBackground(dialog)
- if (progressBarAnimation.visibility != INVISIBLE) {
- progressBarAnimation.visibility = INVISIBLE
- progressBarBackground.visibility = VISIBLE
- }
- }
-
- internal inner class Adapter : RecyclerView.Adapter<Adapter.DeviceItemViewHolder>() {
-
- private val diffUtilCallback =
- object : DiffUtil.ItemCallback<DeviceItem>() {
- override fun areItemsTheSame(
- deviceItem1: DeviceItem,
- deviceItem2: DeviceItem,
- ): Boolean {
- return deviceItem1.cachedBluetoothDevice == deviceItem2.cachedBluetoothDevice
- }
-
- override fun areContentsTheSame(
- deviceItem1: DeviceItem,
- deviceItem2: DeviceItem,
- ): Boolean {
- return deviceItem1.type == deviceItem2.type &&
- deviceItem1.cachedBluetoothDevice == deviceItem2.cachedBluetoothDevice &&
- deviceItem1.deviceName == deviceItem2.deviceName &&
- deviceItem1.connectionSummary == deviceItem2.connectionSummary &&
- // Ignored the icon drawable
- deviceItem1.iconWithDescription?.second ==
- deviceItem2.iconWithDescription?.second &&
- deviceItem1.background == deviceItem2.background &&
- deviceItem1.isEnabled == deviceItem2.isEnabled &&
- deviceItem1.actionAccessibilityLabel == deviceItem2.actionAccessibilityLabel
- }
- }
-
- private val asyncListDiffer = AsyncListDiffer(this, diffUtilCallback)
-
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceItemViewHolder {
- val view =
- LayoutInflater.from(parent.context)
- .inflate(R.layout.bluetooth_device_item, parent, false)
- return DeviceItemViewHolder(view)
- }
-
- override fun getItemCount() = asyncListDiffer.currentList.size
-
- override fun onBindViewHolder(holder: DeviceItemViewHolder, position: Int) {
- val item = getItem(position)
- holder.bind(item)
- }
-
- internal fun getItem(position: Int) = asyncListDiffer.currentList[position]
-
- internal fun refreshDeviceItemList(updated: List<DeviceItem>, callback: () -> Unit) {
- asyncListDiffer.submitList(updated, callback)
- }
-
- internal inner class DeviceItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
- private val container = view.requireViewById<View>(R.id.bluetooth_device_row)
- private val nameView = view.requireViewById<TextView>(R.id.bluetooth_device_name)
- private val summaryView = view.requireViewById<TextView>(R.id.bluetooth_device_summary)
- private val iconView = view.requireViewById<ImageView>(R.id.bluetooth_device_icon)
- private val actionIcon = view.requireViewById<ImageView>(R.id.gear_icon_image)
- private val actionIconView = view.requireViewById<View>(R.id.gear_icon)
- private val divider = view.requireViewById<View>(R.id.divider)
-
- internal fun bind(item: DeviceItem) {
- container.apply {
- isEnabled = item.isEnabled
- background = item.background?.let { context.getDrawable(it) }
- setOnClickListener {
- mutableDeviceItemClick.tryEmit(
- DeviceItemClick(item, it, DeviceItemClick.Target.ENTIRE_ROW)
- )
- uiEventLogger.log(BluetoothTileDialogUiEvent.DEVICE_CLICKED)
- }
-
- // updating icon colors
- val tintColor =
- context.getColor(
- if (item.isActive) InternalR.color.materialColorOnPrimaryContainer
- else InternalR.color.materialColorOnSurface
- )
-
- // update icons
- iconView.apply {
- item.iconWithDescription?.let {
- setImageDrawable(it.first)
- contentDescription = it.second
- }
- }
-
- actionIcon.setImageResource(item.actionIconRes)
- actionIcon.drawable?.setTint(tintColor)
-
- divider.setBackgroundColor(tintColor)
-
- // update text styles
- nameView.setTextAppearance(
- if (item.isActive) R.style.TextAppearance_BluetoothTileDialog_Active
- else R.style.TextAppearance_BluetoothTileDialog
- )
- summaryView.setTextAppearance(
- if (item.isActive) R.style.TextAppearance_BluetoothTileDialog_Active
- else R.style.TextAppearance_BluetoothTileDialog
- )
-
- accessibilityDelegate =
- object : AccessibilityDelegate() {
- override fun onInitializeAccessibilityNodeInfo(
- host: View,
- info: AccessibilityNodeInfo,
- ) {
- super.onInitializeAccessibilityNodeInfo(host, info)
- info.addAction(
- AccessibilityAction(
- AccessibilityAction.ACTION_CLICK.id,
- item.actionAccessibilityLabel,
- )
- )
- }
- }
- }
- nameView.text = item.deviceName
- summaryView.text = item.connectionSummary
-
- actionIconView.setOnClickListener {
- mutableDeviceItemClick.tryEmit(
- DeviceItemClick(item, it, DeviceItemClick.Target.ACTION_ICON)
- )
- }
- }
- }
- }
-
- internal companion object {
- const val MIN_HEIGHT_CHANGE_INTERVAL_MS = 800L
- const val ACTION_BLUETOOTH_DEVICE_DETAILS =
- "com.android.settings.BLUETOOTH_DEVICE_DETAIL_SETTINGS"
- const val ACTION_PREVIOUSLY_CONNECTED_DEVICE =
- "com.android.settings.PREVIOUSLY_CONNECTED_DEVICE"
- const val ACTION_PAIR_NEW_DEVICE = "android.settings.BLUETOOTH_PAIRING_SETTINGS"
- const val ACTION_AUDIO_SHARING = "com.android.settings.BLUETOOTH_AUDIO_SHARING_SETTINGS"
- const val DISABLED_ALPHA = 0.3f
- const val ENABLED_ALPHA = 1f
- const val PROGRESS_BAR_ANIMATION_DURATION_MS = 1500L
-
- private fun Boolean.toInt(): Int {
- return if (this) 1 else 0
- }
+ contentManager.releaseView()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index bf04897f6d10..9492abbeb087 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.bluetooth.qsdialog
+import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
@@ -34,15 +35,16 @@ import com.android.systemui.Prefs
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
-import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_AUDIO_SHARING
-import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE
-import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE
+import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentManager.Companion.ACTION_AUDIO_SHARING
+import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentManager.Companion.ACTION_PAIR_NEW_DEVICE
+import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentManager.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE
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.plugins.ActivityStarter
import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -57,7 +59,12 @@ import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
-/** ViewModel for Bluetooth Dialog after clicking on the Bluetooth QS tile. */
+/**
+ * ViewModel for Bluetooth Dialog or Bluetooth Details View after clicking on the Bluetooth QS tile.
+ *
+ * TODO: b/378513956 Rename this class to BluetoothDetailsContentViewModel, since it's not only used
+ * by the dialog view.
+ */
@SysUISingleton
internal class BluetoothTileDialogViewModel
@Inject
@@ -78,36 +85,61 @@ constructor(
@Background private val backgroundDispatcher: CoroutineDispatcher,
@Main private val sharedPreferences: SharedPreferences,
private val bluetoothDialogDelegateFactory: BluetoothTileDialogDelegate.Factory,
+ private val bluetoothDetailsContentManagerFactory: BluetoothDetailsContentManager.Factory,
) : BluetoothTileDialogCallback {
+ lateinit var contentManager: BluetoothDetailsContentManager
private var job: Job? = null
/**
- * Shows the dialog.
+ * Shows the details content.
*
- * @param view The view from which the dialog is shown.
+ * @param view The view from which the dialog is shown. If view is null, it should show the
+ * bluetooth tile details view.
+ *
+ * TODO: b/378513956 Refactor this method into 2. One is called by the dialog to show the
+ * dialog, another is called by the details view model to bind the view.
*/
- fun showDialog(expandable: Expandable?) {
+ fun showDetailsContent(expandable: Expandable?, view: View?) {
cancelJob()
job =
coroutineScope.launch(context = mainDispatcher) {
var updateDeviceItemJob: Job?
var updateDialogUiJob: Job? = null
- val dialogDelegate = createBluetoothTileDialog()
- val dialog = dialogDelegate.createDialog()
- val context = dialog.context
-
- val controller =
- expandable?.dialogTransitionController(
- DialogCuj(
- InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG,
+ val dialog: SystemUIDialog?
+ val context: Context
+
+ if (view == null) {
+ // Render with dialog
+ val dialogDelegate = createBluetoothTileDialog()
+ dialog = dialogDelegate.createDialog()
+ context = dialog.context
+
+ val controller =
+ expandable?.dialogTransitionController(
+ DialogCuj(
+ InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
+ INTERACTION_JANK_TAG,
+ )
)
- )
- controller?.let {
- dialogTransitionAnimator.show(dialog, it, animateBackgroundBoundsChange = true)
- } ?: dialog.show()
+ controller?.let {
+ dialogTransitionAnimator.show(
+ dialog,
+ it,
+ animateBackgroundBoundsChange = true,
+ )
+ } ?: dialog.show()
+ // contentManager is created after dialog.show
+ contentManager = dialogDelegate.contentManager
+ } else {
+ // Render with tile details view
+ dialog = null
+ context = view.context
+ contentManager = createContentManager()
+ contentManager.bind(view)
+ contentManager.start()
+ }
updateDeviceItemJob = launch {
deviceItemInteractor.updateDeviceItems(context, DeviceFetchTrigger.FIRST_LOAD)
@@ -121,15 +153,14 @@ constructor(
) { deviceItem, showSeeAll ->
updateDialogUiJob?.cancel()
updateDialogUiJob = launch {
- dialogDelegate.apply {
+ contentManager.apply {
onDeviceItemUpdated(
- dialog,
deviceItem,
showSeeAll,
showPairNewDevice =
bluetoothStateInteractor.isBluetoothEnabled(),
)
- animateProgressBar(dialog, false)
+ animateProgressBar(false)
}
}
}
@@ -150,7 +181,7 @@ constructor(
},
)
.onEach {
- dialogDelegate.animateProgressBar(dialog, true)
+ contentManager.animateProgressBar(true)
updateDeviceItemJob?.cancel()
updateDeviceItemJob = launch {
deviceItemInteractor.updateDeviceItems(
@@ -171,16 +202,14 @@ constructor(
.onEach {
when (it) {
is AudioSharingButtonState.Visible -> {
- dialogDelegate.onAudioSharingButtonUpdated(
- dialog,
+ contentManager.onAudioSharingButtonUpdated(
VISIBLE,
context.getString(it.resId),
it.isActive,
)
}
is AudioSharingButtonState.Gone -> {
- dialogDelegate.onAudioSharingButtonUpdated(
- dialog,
+ contentManager.onAudioSharingButtonUpdated(
GONE,
label = null,
isActive = false,
@@ -197,8 +226,7 @@ constructor(
// the device item list.
bluetoothStateInteractor.bluetoothStateUpdate
.onEach {
- dialogDelegate.onBluetoothStateUpdated(
- dialog,
+ contentManager.onBluetoothStateUpdated(
it,
UiProperties.build(it, isAutoOnToggleFeatureAvailable()),
)
@@ -214,16 +242,17 @@ constructor(
// bluetoothStateToggle is emitted when user toggles the bluetooth state switch,
// send the new value to the bluetoothStateInteractor and animate the progress bar.
- dialogDelegate.bluetoothStateToggle
+ contentManager.bluetoothStateToggle
.filterNotNull()
.onEach {
- dialogDelegate.animateProgressBar(dialog, true)
+ contentManager.animateProgressBar(true)
bluetoothStateInteractor.setBluetoothEnabled(it)
}
.launchIn(this)
// deviceItemClick is emitted when user clicked on a device item.
- dialogDelegate.deviceItemClick
+ contentManager.deviceItemClick
+ .filterNotNull()
.onEach {
when (it.target) {
DeviceItemClick.Target.ENTIRE_ROW -> {
@@ -245,7 +274,8 @@ constructor(
.launchIn(this)
// contentHeight is emitted when the dialog is dismissed.
- dialogDelegate.contentHeight
+ contentManager.contentHeight
+ .filterNotNull()
.onEach {
withContext(backgroundDispatcher) {
sharedPreferences.edit().putInt(CONTENT_HEIGHT_PREF_KEY, it).apply()
@@ -258,8 +288,7 @@ constructor(
// changed.
bluetoothAutoOnInteractor.isEnabled
.onEach {
- dialogDelegate.onBluetoothAutoOnUpdated(
- dialog,
+ contentManager.onBluetoothAutoOnUpdated(
it,
if (it) R.string.turn_on_bluetooth_auto_info_enabled
else R.string.turn_on_bluetooth_auto_info_disabled,
@@ -269,36 +298,48 @@ constructor(
// bluetoothAutoOnToggle is emitted when user toggles the bluetooth auto on
// switch, send the new value to the bluetoothAutoOnInteractor.
- dialogDelegate.bluetoothAutoOnToggle
+ contentManager.bluetoothAutoOnToggle
.filterNotNull()
.onEach { bluetoothAutoOnInteractor.setEnabled(it) }
.launchIn(this)
}
- produce<Unit> { awaitClose { dialog.cancel() } }
+ produce<Unit> { awaitClose { dialog?.cancel() } }
}
}
private suspend fun createBluetoothTileDialog(): BluetoothTileDialogDelegate {
- val cachedContentHeight =
- withContext(backgroundDispatcher) {
- sharedPreferences.getInt(
- CONTENT_HEIGHT_PREF_KEY,
- ViewGroup.LayoutParams.WRAP_CONTENT,
- )
- }
-
return bluetoothDialogDelegateFactory.create(
- UiProperties.build(
- bluetoothStateInteractor.isBluetoothEnabled(),
- isAutoOnToggleFeatureAvailable(),
- ),
- cachedContentHeight,
+ getUiProperties(),
+ getCachedContentHeight(),
this@BluetoothTileDialogViewModel,
{ cancelJob() },
)
}
+ private suspend fun createContentManager(): BluetoothDetailsContentManager {
+ return bluetoothDetailsContentManagerFactory.create(
+ getUiProperties(),
+ getCachedContentHeight(),
+ this@BluetoothTileDialogViewModel,
+ /* isInDialog= */ false,
+ /* doneButtonCallback= */ fun() {},
+ )
+ }
+
+ private suspend fun getUiProperties(): UiProperties {
+ return UiProperties.build(
+ bluetoothStateInteractor.isBluetoothEnabled(),
+ isAutoOnToggleFeatureAvailable(),
+ )
+ }
+
+ private suspend fun getCachedContentHeight(): Int {
+ return withContext(backgroundDispatcher) {
+ sharedPreferences.getInt(CONTENT_HEIGHT_PREF_KEY, ViewGroup.LayoutParams.WRAP_CONTENT)
+ }
+ }
+
override fun onSeeAllClicked(view: View) {
uiEventLogger.log(BluetoothTileDialogUiEvent.SEE_ALL_CLICKED)
startSettingsActivity(Intent(ACTION_PREVIOUSLY_CONNECTED_DEVICE), view)
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
index cb4ec37a1a66..26996ac1db39 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
@@ -27,7 +27,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
interface DeviceItemActionInteractor {
- suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog)
+ suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog?) {}
suspend fun onActionIconClick(deviceItem: DeviceItem, onIntent: (Intent) -> Unit)
}
@@ -40,7 +40,7 @@ constructor(
private val uiEventLogger: UiEventLogger,
) : DeviceItemActionInteractor {
- override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
+ override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog?) {
withContext(backgroundDispatcher) {
deviceItem.cachedBluetoothDevice.apply {
when (deviceItem.type) {
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
index 6f2a2c4ccaaa..b13f6df3f4f5 100644
--- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
+++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt
@@ -16,49 +16,75 @@
package com.android.systemui.brightness.ui.compose
+import android.content.Context
import android.view.MotionEvent
+import androidx.annotation.VisibleForTesting
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
+import androidx.compose.material3.Slider
+import androidx.compose.material3.SliderDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
-import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.compose.PlatformSlider
import com.android.compose.ui.graphics.drawInOverlay
import com.android.systemui.Flags
+import com.android.systemui.biometrics.Utils.toBitmap
import com.android.systemui.brightness.shared.model.GammaBrightness
+import com.android.systemui.brightness.ui.compose.AnimationSpecs.IconAppearSpec
+import com.android.systemui.brightness.ui.compose.AnimationSpecs.IconDisappearSpec
+import com.android.systemui.brightness.ui.compose.Dimensions.IconPadding
+import com.android.systemui.brightness.ui.compose.Dimensions.IconSize
+import com.android.systemui.brightness.ui.compose.Dimensions.SliderBackgroundFrameSize
+import com.android.systemui.brightness.ui.compose.Dimensions.SliderBackgroundRoundedCorner
+import com.android.systemui.brightness.ui.compose.Dimensions.SliderTrackRoundedCorner
+import com.android.systemui.brightness.ui.compose.Dimensions.ThumbTrackGapSize
import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel
import com.android.systemui.brightness.ui.viewmodel.Drag
import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.common.shared.model.Text
-import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.haptics.slider.SeekableSliderTrackerConfig
import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig
@@ -67,27 +93,32 @@ import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.qs.ui.compose.borderOnFocus
import com.android.systemui.res.R
import com.android.systemui.utils.PolicyRestriction
+import platform.test.motion.compose.values.MotionTestValueKey
+import platform.test.motion.compose.values.motionTestValues
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
-private fun BrightnessSlider(
- viewModel: BrightnessSliderViewModel,
+@VisibleForTesting
+fun BrightnessSlider(
gammaValue: Int,
valueRange: IntRange,
- label: Text.Resource,
- icon: Icon,
+ iconResProvider: (Float) -> Int,
+ imageLoader: suspend (Int, Context) -> Icon.Loaded,
restriction: PolicyRestriction,
onRestrictedClick: (PolicyRestriction.Restricted) -> Unit,
onDrag: (Int) -> Unit,
onStop: (Int) -> Unit,
+ overriddenByAppState: Boolean,
modifier: Modifier = Modifier,
- formatter: (Int) -> String = { "$it" },
+ showToast: () -> Unit = {},
hapticsViewModelFactory: SliderHapticsViewModel.Factory,
) {
var value by remember(gammaValue) { mutableIntStateOf(gammaValue) }
val animatedValue by
animateFloatAsState(targetValue = value.toFloat(), label = "BrightnessSliderAnimatedValue")
val floatValueRange = valueRange.first.toFloat()..valueRange.last.toFloat()
- val isRestricted = remember(restriction) { restriction is PolicyRestriction.Restricted }
+ val isRestricted = restriction is PolicyRestriction.Restricted
+ val enabled = !isRestricted
val interactionSource = remember { MutableInteractionSource() }
val hapticsViewModel: SliderHapticsViewModel? =
if (Flags.hapticsForComposeSliders()) {
@@ -105,20 +136,56 @@ private fun BrightnessSlider(
} else {
null
}
+ val colors = SliderDefaults.colors()
- val overriddenByAppState by
- if (Flags.showToastWhenAppControlBrightness()) {
- viewModel.brightnessOverriddenByWindow.collectAsStateWithLifecycle()
- } else {
- remember { mutableStateOf(false) }
+ // The value state is recreated every time gammaValue changes, so we recreate this derivedState
+ // We have to use value as that's the value that changes when the user is dragging (gammaValue
+ // is always the starting value: actual (not temporary) brightness).
+ val iconRes by
+ remember(gammaValue, valueRange) {
+ derivedStateOf {
+ val percentage =
+ (value - valueRange.first) * 100f / (valueRange.last - valueRange.first)
+ iconResProvider(percentage)
+ }
+ }
+ val context = LocalContext.current
+ val painter: Painter by
+ produceState<Painter>(
+ initialValue = ColorPainter(Color.Transparent),
+ key1 = iconRes,
+ key2 = context,
+ ) {
+ val icon = imageLoader(iconRes, context)
+ // toBitmap is Drawable?.() -> Bitmap? and handles null internally.
+ val bitmap = icon.drawable.toBitmap()!!.asImageBitmap()
+ this@produceState.value = BitmapPainter(bitmap)
+ }
+
+ val activeIconColor = colors.activeTickColor
+ val inactiveIconColor = colors.inactiveTickColor
+ val trackIcon: DrawScope.(Offset, Color, Float) -> Unit =
+ remember(painter) {
+ { offset, color, alpha ->
+ translate(offset.x + IconPadding.toPx(), offset.y) {
+ with(painter) {
+ draw(
+ IconSize.toSize(),
+ colorFilter = ColorFilter.tint(color),
+ alpha = alpha,
+ )
+ }
+ }
+ }
}
- PlatformSlider(
+ Slider(
value = animatedValue,
valueRange = floatValueRange,
- enabled = !isRestricted,
+ enabled = enabled,
+ colors = colors,
onValueChange = {
- if (!isRestricted) {
+ if (enabled) {
if (!overriddenByAppState) {
hapticsViewModel?.onValueChange(it)
value = it.toInt()
@@ -127,7 +194,7 @@ private fun BrightnessSlider(
}
},
onValueChangeFinished = {
- if (!isRestricted) {
+ if (enabled) {
if (!overriddenByAppState) {
hapticsViewModel?.onValueChangeEnded()
onStop(value)
@@ -140,46 +207,117 @@ private fun BrightnessSlider(
onRestrictedClick(restriction)
}
},
- icon = { isDragging ->
- if (isDragging) {
- Text(text = formatter(value))
- } else {
- Icon(modifier = Modifier.size(24.dp), icon = icon)
- }
+ interactionSource = interactionSource,
+ thumb = {
+ SliderDefaults.Thumb(
+ interactionSource = interactionSource,
+ enabled = enabled,
+ thumbSize = DpSize(4.dp, 52.dp),
+ )
},
- label = {
- Text(
- text = stringResource(id = label.res),
- style = MaterialTheme.typography.titleMedium,
- maxLines = 1,
+ track = { sliderState ->
+ var showIconActive by remember { mutableStateOf(true) }
+ val iconActiveAlphaAnimatable = remember {
+ Animatable(
+ initialValue = 1f,
+ typeConverter = Float.VectorConverter,
+ label = "iconActiveAlpha",
+ )
+ }
+
+ val iconInactiveAlphaAnimatable = remember {
+ Animatable(
+ initialValue = 0f,
+ typeConverter = Float.VectorConverter,
+ label = "iconInactiveAlpha",
+ )
+ }
+
+ LaunchedEffect(iconActiveAlphaAnimatable, iconInactiveAlphaAnimatable, showIconActive) {
+ if (showIconActive) {
+ launch { iconActiveAlphaAnimatable.appear() }
+ launch { iconInactiveAlphaAnimatable.disappear() }
+ } else {
+ launch { iconActiveAlphaAnimatable.disappear() }
+ launch { iconInactiveAlphaAnimatable.appear() }
+ }
+ }
+
+ SliderDefaults.Track(
+ sliderState = sliderState,
+ modifier =
+ Modifier.motionTestValues {
+ (iconActiveAlphaAnimatable.isRunning ||
+ iconInactiveAlphaAnimatable.isRunning) exportAs
+ BrightnessSliderMotionTestKeys.AnimatingIcon
+
+ iconActiveAlphaAnimatable.value exportAs
+ BrightnessSliderMotionTestKeys.ActiveIconAlpha
+ iconInactiveAlphaAnimatable.value exportAs
+ BrightnessSliderMotionTestKeys.InactiveIconAlpha
+ }
+ .height(40.dp)
+ .drawWithContent {
+ drawContent()
+
+ val yOffset = size.height / 2 - IconSize.toSize().height / 2
+ val activeTrackStart = 0f
+ val activeTrackEnd =
+ size.width * sliderState.coercedValueAsFraction -
+ ThumbTrackGapSize.toPx()
+ val inactiveTrackStart = activeTrackEnd + ThumbTrackGapSize.toPx() * 2
+ val inactiveTrackEnd = size.width
+
+ val activeTrackWidth = activeTrackEnd - activeTrackStart
+ val inactiveTrackWidth = inactiveTrackEnd - inactiveTrackStart
+ if (
+ IconSize.toSize().width < activeTrackWidth - IconPadding.toPx() * 2
+ ) {
+ showIconActive = true
+ trackIcon(
+ Offset(activeTrackStart, yOffset),
+ activeIconColor,
+ iconActiveAlphaAnimatable.value,
+ )
+ } else if (
+ IconSize.toSize().width <
+ inactiveTrackWidth - IconPadding.toPx() * 2
+ ) {
+ showIconActive = false
+ trackIcon(
+ Offset(inactiveTrackStart, yOffset),
+ inactiveIconColor,
+ iconInactiveAlphaAnimatable.value,
+ )
+ }
+ },
+ trackCornerSize = SliderTrackRoundedCorner,
+ trackInsideCornerSize = 2.dp,
+ drawStopIndicator = null,
+ thumbTrackGapSize = ThumbTrackGapSize,
)
},
- interactionSource = interactionSource,
)
+
+ val currentShowToast by rememberUpdatedState(showToast)
// Showing the warning toast if the current running app window has controlled the
// brightness value.
if (Flags.showToastWhenAppControlBrightness()) {
- val context = LocalContext.current
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
if (interaction is DragInteraction.Start && overriddenByAppState) {
- viewModel.showToast(
- context,
- R.string.quick_settings_brightness_unable_adjust_msg,
- )
+ currentShowToast()
}
}
}
}
}
-private val sliderBackgroundFrameSize = 8.dp
-
private fun Modifier.sliderBackground(color: Color) = drawWithCache {
- val offsetAround = sliderBackgroundFrameSize.toPx()
- val newSize = Size(size.width + 2 * offsetAround, size.height + 2 * offsetAround)
- val offset = Offset(-offsetAround, -offsetAround)
- val cornerRadius = CornerRadius(offsetAround + size.height / 2)
+ val offsetAround = SliderBackgroundFrameSize.toSize()
+ val newSize = Size(size.width + 2 * offsetAround.width, size.height + 2 * offsetAround.height)
+ val offset = Offset(-offsetAround.width, -offsetAround.height)
+ val cornerRadius = CornerRadius(SliderBackgroundRoundedCorner.toPx())
onDrawBehind {
drawRoundRect(color = color, topLeft = offset, size = newSize, cornerRadius = cornerRadius)
}
@@ -192,21 +330,30 @@ fun BrightnessSliderContainer(
containerColor: Color = colorResource(R.color.shade_scrim_background_dark),
) {
val gamma = viewModel.currentBrightness.value
+ if (gamma == BrightnessSliderViewModel.initialValue.value) { // Ignore initial negative value.
+ return
+ }
+ val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val restriction by
viewModel.policyRestriction.collectAsStateWithLifecycle(
initialValue = PolicyRestriction.NoRestriction
)
+ val overriddenByAppState by
+ if (Flags.showToastWhenAppControlBrightness()) {
+ viewModel.brightnessOverriddenByWindow.collectAsStateWithLifecycle()
+ } else {
+ remember { mutableStateOf(false) }
+ }
DisposableEffect(Unit) { onDispose { viewModel.setIsDragging(false) } }
Box(modifier = modifier.fillMaxWidth().sysuiResTag("brightness_slider")) {
BrightnessSlider(
- viewModel = viewModel,
gammaValue = gamma,
valueRange = viewModel.minBrightness.value..viewModel.maxBrightness.value,
- label = viewModel.label,
- icon = viewModel.icon,
+ iconResProvider = BrightnessSliderViewModel::getIconForPercentage,
+ imageLoader = viewModel::loadImage,
restriction = restriction,
onRestrictedClick = viewModel::showPolicyRestrictionDialog,
onDrag = {
@@ -220,7 +367,7 @@ fun BrightnessSliderContainer(
modifier =
Modifier.borderOnFocus(
color = MaterialTheme.colorScheme.secondary,
- cornerSize = CornerSize(32.dp),
+ cornerSize = CornerSize(SliderTrackRoundedCorner),
)
.then(if (viewModel.showMirror) Modifier.drawInOverlay() else Modifier)
.sliderBackground(containerColor)
@@ -234,8 +381,38 @@ fun BrightnessSliderContainer(
}
false
},
- formatter = viewModel::formatValue,
hapticsViewModelFactory = viewModel.hapticsViewModelFactory,
+ overriddenByAppState = overriddenByAppState,
+ showToast = {
+ viewModel.showToast(context, R.string.quick_settings_brightness_unable_adjust_msg)
+ },
)
}
}
+
+private object Dimensions {
+ val SliderBackgroundFrameSize = DpSize(10.dp, 6.dp)
+ val SliderBackgroundRoundedCorner = 24.dp
+ val SliderTrackRoundedCorner = 12.dp
+ val IconSize = DpSize(28.dp, 28.dp)
+ val IconPadding = 6.dp
+ val ThumbTrackGapSize = 6.dp
+}
+
+private object AnimationSpecs {
+ val IconAppearSpec = tween<Float>(durationMillis = 100, delayMillis = 33)
+ val IconDisappearSpec = tween<Float>(durationMillis = 50)
+}
+
+private suspend fun Animatable<Float, AnimationVector1D>.appear() =
+ animateTo(targetValue = 1f, animationSpec = IconAppearSpec)
+
+private suspend fun Animatable<Float, AnimationVector1D>.disappear() =
+ animateTo(targetValue = 0f, animationSpec = IconDisappearSpec)
+
+@VisibleForTesting
+object BrightnessSliderMotionTestKeys {
+ val AnimatingIcon = MotionTestValueKey<Boolean>("animatingIcon")
+ val ActiveIconAlpha = MotionTestValueKey<Float>("activeIconAlpha")
+ val InactiveIconAlpha = MotionTestValueKey<Float>("inactiveIconAlpha")
+}
diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt
index 7df71550d43d..ed1cef3fccaf 100644
--- a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt
@@ -17,6 +17,8 @@
package com.android.systemui.brightness.ui.viewmodel
import android.content.Context
+import androidx.annotation.DrawableRes
+import androidx.annotation.FloatRange
import androidx.annotation.StringRes
import androidx.compose.runtime.getValue
import com.android.systemui.brightness.domain.interactor.BrightnessPolicyEnforcementInteractor
@@ -24,9 +26,9 @@ import com.android.systemui.brightness.domain.interactor.ScreenBrightnessInterac
import com.android.systemui.brightness.shared.model.GammaBrightness
import com.android.systemui.classifier.Classifier
import com.android.systemui.classifier.domain.interactor.FalsingInteractor
-import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.common.shared.model.Text
+import com.android.systemui.common.shared.model.asIcon
+import com.android.systemui.graphics.ImageLoader
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
@@ -57,6 +59,7 @@ constructor(
private val falsingInteractor: FalsingInteractor,
@Assisted private val supportsMirroring: Boolean,
private val brightnessWarningToast: BrightnessWarningToast,
+ private val imageLoader: ImageLoader,
) : ExclusiveActivatable() {
private val hydrator = Hydrator("BrightnessSliderViewModel.hydrator")
@@ -64,17 +67,13 @@ constructor(
val currentBrightness by
hydrator.hydratedStateOf(
"currentBrightness",
- GammaBrightness(0),
+ initialValue,
screenBrightnessInteractor.gammaBrightness,
)
val maxBrightness = screenBrightnessInteractor.maxGammaBrightness
val minBrightness = screenBrightnessInteractor.minGammaBrightness
- val label = Text.Resource(R.string.quick_settings_brightness_dialog_title)
-
- val icon = Icon.Resource(R.drawable.ic_brightness_full, ContentDescription.Resource(label.res))
-
val policyRestriction = brightnessPolicyEnforcementInteractor.brightnessPolicyRestriction
fun showPolicyRestrictionDialog(restriction: PolicyRestriction.Restricted) {
@@ -94,6 +93,16 @@ constructor(
falsingInteractor.isFalseTouch(Classifier.BRIGHTNESS_SLIDER)
}
+ suspend fun loadImage(@DrawableRes resId: Int, context: Context): Icon.Loaded {
+ return imageLoader
+ .loadDrawable(
+ android.graphics.drawable.Icon.createWithResource(context, resId),
+ maxHeight = 200,
+ maxWidth = 200,
+ )!!
+ .asIcon(null, resId)
+ }
+
/**
* As a brightness slider is dragged, the corresponding events should be sent using this method.
*/
@@ -104,18 +113,6 @@ constructor(
}
}
- /**
- * Format the current value of brightness as a percentage between the minimum and maximum gamma.
- */
- fun formatValue(value: Int): String {
- val min = minBrightness.value
- val max = maxBrightness.value
- val coercedValue = value.coerceIn(min, max)
- val percentage = (coercedValue - min) * 100 / (max - min)
- // This is not finalized UI so using fixed string
- return "$percentage%"
- }
-
fun setIsDragging(dragging: Boolean) {
brightnessMirrorShowingInteractor.setMirrorShowing(dragging && supportsMirroring)
}
@@ -131,6 +128,26 @@ constructor(
interface Factory {
fun create(supportsMirroring: Boolean): BrightnessSliderViewModel
}
+
+ companion object {
+ val initialValue = GammaBrightness(-1)
+
+ private val icons =
+ BrightnessIcons(
+ brightnessLow = R.drawable.ic_brightness_low,
+ brightnessMid = R.drawable.ic_brightness_medium,
+ brightnessHigh = R.drawable.ic_brightness_full,
+ )
+
+ @DrawableRes
+ fun getIconForPercentage(@FloatRange(0.0, 100.0) percentage: Float): Int {
+ return when {
+ percentage <= 20f -> icons.brightnessLow
+ percentage >= 80f -> icons.brightnessHigh
+ else -> icons.brightnessMid
+ }
+ }
+ }
}
fun BrightnessSliderViewModel.Factory.create() = create(supportsMirroring = true)
@@ -143,3 +160,9 @@ sealed interface Drag {
@JvmInline value class Stopped(override val brightness: GammaBrightness) : Drag
}
+
+private data class BrightnessIcons(
+ @DrawableRes val brightnessLow: Int,
+ @DrawableRes val brightnessMid: Int,
+ @DrawableRes val brightnessHigh: Int,
+)
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 643d185f1939..8b6322720118 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
@@ -16,7 +16,9 @@
package com.android.systemui.communal.data.repository
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.TransitionKey
import com.android.systemui.communal.dagger.Communal
@@ -25,16 +27,17 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.scene.shared.model.SceneDataSource
+import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
-import com.android.app.tracing.coroutines.launchTraced as launch
/** Encapsulates the state of communal mode. */
interface CommunalSceneRepository {
@@ -52,6 +55,9 @@ interface CommunalSceneRepository {
/** Immediately snaps to the desired scene. */
fun snapToScene(toScene: SceneKey)
+ /** Shows the hub from a power button press. */
+ suspend fun showHubFromPowerButton()
+
/**
* Updates the transition state of the hub [SceneTransitionLayout].
*
@@ -67,6 +73,7 @@ constructor(
@Application private val applicationScope: CoroutineScope,
@Background backgroundScope: CoroutineScope,
@Communal private val sceneDataSource: SceneDataSource,
+ @Communal private val delegator: SceneDataSourceDelegator,
) : CommunalSceneRepository {
override val currentScene: StateFlow<SceneKey> = sceneDataSource.currentScene
@@ -98,6 +105,18 @@ constructor(
}
}
+ override suspend fun showHubFromPowerButton() {
+ // If keyguard is not showing yet, the hub view is not ready and the
+ // [SceneDataSourceDelegator] will still be using the default [NoOpSceneDataSource]
+ // and initial key, which is Blank. This means that when the hub container loads, it
+ // will default to not showing the hub. Attempting to set the scene in this state
+ // is simply ignored by the [NoOpSceneDataSource]. Instead, we temporarily override
+ // it with a new one that defaults to Communal. This delegate will be overwritten
+ // once the [CommunalContainer] loads.
+ // TODO(b/392969914): show the hub first instead of forcing the scene.
+ delegator.setDelegate(NoOpSceneDataSource(CommunalScenes.Communal))
+ }
+
/**
* Updates the transition state of the hub [SceneTransitionLayout].
*
@@ -106,4 +125,27 @@ constructor(
override fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) {
_transitionState.value = transitionState
}
+
+ /** Noop implementation of a scene data source that always returns the initial [SceneKey]. */
+ private class NoOpSceneDataSource(initialSceneKey: SceneKey) : SceneDataSource {
+ override val currentScene: StateFlow<SceneKey> =
+ MutableStateFlow(initialSceneKey).asStateFlow()
+
+ override val currentOverlays: StateFlow<Set<OverlayKey>> =
+ MutableStateFlow(emptySet<OverlayKey>()).asStateFlow()
+
+ override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = Unit
+
+ override fun snapToScene(toScene: SceneKey) = Unit
+
+ override fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) = Unit
+
+ override fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) = Unit
+
+ override fun replaceOverlay(
+ from: OverlayKey,
+ to: OverlayKey,
+ transitionKey: TransitionKey?,
+ ) = Unit
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
index abd101693b43..4c291a0c5a2e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt
@@ -111,6 +111,18 @@ constructor(
@Named(DEFAULT_BACKGROUND_TYPE) private val defaultBackgroundType: CommunalBackgroundType,
) : CommunalSettingsRepository {
+ private val dreamsActivatedOnSleepByDefault by lazy {
+ resources.getBoolean(com.android.internal.R.bool.config_dreamsActivatedOnSleepByDefault)
+ }
+
+ private val dreamsActivatedOnDockByDefault by lazy {
+ resources.getBoolean(com.android.internal.R.bool.config_dreamsActivatedOnDockByDefault)
+ }
+
+ private val dreamsActivatedOnPosturedByDefault by lazy {
+ resources.getBoolean(com.android.internal.R.bool.config_dreamsActivatedOnPosturedByDefault)
+ }
+
override fun getFlagEnabled(): Boolean {
return if (getV2FlagEnabled()) {
true
@@ -178,27 +190,27 @@ constructor(
.emitOnStart()
.map {
if (
- secureSettings.getIntForUser(
+ secureSettings.getBoolForUser(
Settings.Secure.SCREENSAVER_ACTIVATE_ON_SLEEP,
- 0,
+ dreamsActivatedOnSleepByDefault,
user.id,
- ) == 1
+ )
) {
WhenToDream.WHILE_CHARGING
} else if (
- secureSettings.getIntForUser(
+ secureSettings.getBoolForUser(
Settings.Secure.SCREENSAVER_ACTIVATE_ON_DOCK,
- 0,
+ dreamsActivatedOnDockByDefault,
user.id,
- ) == 1
+ )
) {
WhenToDream.WHILE_DOCKED
} else if (
- secureSettings.getIntForUser(
+ secureSettings.getBoolForUser(
Settings.Secure.SCREENSAVER_ACTIVATE_ON_POSTURED,
- 0,
+ dreamsActivatedOnPosturedByDefault,
user.id,
- ) == 1
+ )
) {
WhenToDream.WHILE_POSTURED
} else {
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 237a19cb31c4..de55c92e84f9 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
@@ -545,10 +545,16 @@ constructor(
}
/** CTA tile to be displayed in the glanceable hub (view mode). */
- val ctaTileContent: Flow<List<CommunalContentModel.CtaTileInViewMode>> =
- communalPrefsInteractor.isCtaDismissed.map { isDismissed ->
- if (isDismissed) emptyList() else listOf(CommunalContentModel.CtaTileInViewMode())
+ val ctaTileContent: Flow<List<CommunalContentModel.CtaTileInViewMode>> by lazy {
+ if (communalSettingsInteractor.isV2FlagEnabled()) {
+ flowOf(listOf<CommunalContentModel.CtaTileInViewMode>())
+ } else {
+ communalPrefsInteractor.isCtaDismissed.map { isDismissed ->
+ if (isDismissed) listOf<CommunalContentModel.CtaTileInViewMode>()
+ else listOf(CommunalContentModel.CtaTileInViewMode())
+ }
}
+ }
/** A list of tutorial content to be displayed in the communal hub in tutorial mode. */
val tutorialContent: List<CommunalContentModel.Tutorial> =
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
index 476493225857..3d9e93036dbc 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
@@ -148,6 +148,29 @@ constructor(
}
}
+ fun showHubFromPowerButton() {
+ val loggingReason = "showing hub from power button"
+ applicationScope.launch("$TAG#showHubFromPowerButton") {
+ if (SceneContainerFlag.isEnabled) {
+ sceneInteractor.changeScene(
+ toScene = CommunalScenes.Communal.toSceneContainerSceneKey(),
+ loggingReason = loggingReason,
+ )
+ return@launch
+ }
+
+ if (currentScene.value == CommunalScenes.Communal) return@launch
+ logger.logSceneChangeRequested(
+ from = currentScene.value,
+ to = CommunalScenes.Communal,
+ reason = loggingReason,
+ isInstant = true,
+ )
+ notifyListeners(CommunalScenes.Communal, null)
+ repository.showHubFromPowerButton()
+ }
+ }
+
private fun notifyListeners(newScene: SceneKey, keyguardState: KeyguardState?) {
onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(newScene, keyguardState) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 099a85926020..49003a735fbd 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -45,7 +45,7 @@ abstract class BaseCommunalViewModel(
val mediaHost: MediaHost,
val mediaCarouselController: MediaCarouselController,
) {
- val currentScene: Flow<SceneKey> = communalSceneInteractor.currentScene
+ val currentScene: StateFlow<SceneKey> = communalSceneInteractor.currentScene
/** Used to animate showing or hiding the communal content. */
open val isCommunalContentVisible: Flow<Boolean> = MutableStateFlow(false)
diff --git a/packages/SystemUI/src/com/android/systemui/complication/DreamClockTimeComplication.java b/packages/SystemUI/src/com/android/systemui/complication/DreamClockTimeComplication.java
index c709e3436cd6..8e6848a87c7d 100644
--- a/packages/SystemUI/src/com/android/systemui/complication/DreamClockTimeComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/complication/DreamClockTimeComplication.java
@@ -16,10 +16,13 @@
package com.android.systemui.complication;
+import static android.text.format.DateFormat.getBestDateTimePattern;
+
import static com.android.systemui.complication.dagger.DreamClockTimeComplicationComponent.DreamClockTimeComplicationModule.DREAM_CLOCK_TIME_COMPLICATION_VIEW;
import static com.android.systemui.complication.dagger.RegisteredComplicationsModule.DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS;
import android.view.View;
+import android.widget.TextClock;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.CoreStartable;
@@ -92,18 +95,24 @@ public class DreamClockTimeComplication implements Complication {
* {@link ViewHolder} to contain value/logic associated with {@link DreamClockTimeComplication}.
*/
public static class DreamClockTimeViewHolder implements ViewHolder {
- private final View mView;
+ private final TextClock mView;
private final ComplicationLayoutParams mLayoutParams;
@Inject
DreamClockTimeViewHolder(
- @Named(DREAM_CLOCK_TIME_COMPLICATION_VIEW) View view,
+ @Named(DREAM_CLOCK_TIME_COMPLICATION_VIEW) TextClock view,
@Named(DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS)
ComplicationLayoutParams layoutParams,
DreamClockTimeViewController viewController) {
mView = view;
mLayoutParams = layoutParams;
viewController.init();
+
+ // Support localized AM/PM marker for 12h mode in content description.
+ String formatSkeleton = view.is24HourModeEnabled() ? "Hm" : "hm";
+ String pattern = getBestDateTimePattern(view.getTextLocale(), formatSkeleton);
+ view.setContentDescriptionFormat12Hour(pattern);
+ view.setContentDescriptionFormat24Hour(pattern);
}
@Override
@@ -122,7 +131,7 @@ public class DreamClockTimeComplication implements Complication {
@Inject
DreamClockTimeViewController(
- @Named(DREAM_CLOCK_TIME_COMPLICATION_VIEW) View view,
+ @Named(DREAM_CLOCK_TIME_COMPLICATION_VIEW) TextClock view,
UiEventLogger uiEventLogger) {
super(view);
diff --git a/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamClockTimeComplicationComponent.kt b/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamClockTimeComplicationComponent.kt
index 4b9ac1d58b57..9d367c91c2c3 100644
--- a/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamClockTimeComplicationComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/complication/dagger/DreamClockTimeComplicationComponent.kt
@@ -18,7 +18,6 @@
package com.android.systemui.complication.dagger
import android.view.LayoutInflater
-import android.view.View
import android.widget.TextClock
import com.android.internal.util.Preconditions
import com.android.systemui.Flags
@@ -64,7 +63,7 @@ interface DreamClockTimeComplicationComponent {
@Provides
@DreamClockTimeComplicationScope
@Named(DREAM_CLOCK_TIME_COMPLICATION_VIEW)
- fun provideComplicationView(layoutInflater: LayoutInflater): View {
+ fun provideComplicationView(layoutInflater: LayoutInflater): TextClock {
val view =
Preconditions.checkNotNull(
layoutInflater.inflate(
diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
index a5f29aa658be..f3eff607dedd 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsListingControllerImpl.kt
@@ -122,7 +122,7 @@ constructor(
init {
Log.d(TAG, "Initializing")
- dumpManager.registerDumpable(TAG, this)
+ dumpManager.registerNormalDumpable(TAG, this)
serviceListing.addCallback(serviceListingCallback)
serviceListing.setListening(true)
serviceListing.reload()
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 5b6859761705..5fc924b14814 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -16,7 +16,6 @@
package com.android.systemui.deviceentry.domain.interactor
-import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.policy.IKeyguardDismissCallback
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
@@ -36,11 +35,9 @@ import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.utils.coroutines.flow.mapLatestConflated
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
@@ -285,32 +282,7 @@ constructor(
}
/** Locks the device instantly. */
- fun lockNow() {
- deviceUnlockedInteractor.lockNow()
- }
-
- suspend fun hydrateTableLogBuffer(tableLogBuffer: TableLogBuffer) {
- coroutineScope {
- launch {
- isDeviceEntered
- .logDiffsForTable(
- tableLogBuffer = tableLogBuffer,
- columnName = "isDeviceEntered",
- initialValue = isDeviceEntered.value,
- )
- .collect()
- }
-
- launch {
- canSwipeToEnter
- .map { it?.toString() ?: "" }
- .logDiffsForTable(
- tableLogBuffer = tableLogBuffer,
- columnName = "canSwipeToEnter",
- initialValue = canSwipeToEnter.value?.toString() ?: "",
- )
- .collect()
- }
- }
+ fun lockNow(debuggingReason: String) {
+ deviceUnlockedInteractor.lockNow(debuggingReason)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
index b1be9a209a0a..c6ae35317c72 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt
@@ -59,6 +59,7 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.receiveAsFlow
@@ -70,7 +71,7 @@ class DeviceUnlockedInteractor
constructor(
private val authenticationInteractor: AuthenticationInteractor,
private val repository: DeviceEntryRepository,
- trustInteractor: TrustInteractor,
+ private val trustInteractor: TrustInteractor,
faceAuthInteractor: DeviceEntryFaceAuthInteractor,
fingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
private val powerInteractor: PowerInteractor,
@@ -181,7 +182,8 @@ constructor(
val deviceUnlockStatus: StateFlow<DeviceUnlockStatus> =
repository.deviceUnlockStatus.asStateFlow()
- private val lockNowRequests = Channel<Unit>()
+ /** A [Channel] of "lock now" requests where the values are the debugging reasons. */
+ private val lockNowRequests = Channel<String>()
override suspend fun onActivated(): Nothing {
coroutineScope {
@@ -218,8 +220,8 @@ constructor(
}
/** Locks the device instantly. */
- fun lockNow() {
- lockNowRequests.trySend(Unit)
+ fun lockNow(debuggingReason: String) {
+ lockNowRequests.trySend(debuggingReason)
}
private suspend fun handleLockAndUnlockEvents() {
@@ -244,47 +246,70 @@ constructor(
private suspend fun handleLockEvents() {
merge(
- // Device wakefulness events.
- powerInteractor.detailedWakefulness
- .map { Pair(it.isAsleep(), it.lastSleepReason) }
- .distinctUntilChangedBy { it.first }
- .map { (isAsleep, lastSleepReason) ->
- if (isAsleep) {
- if (
- (lastSleepReason == WakeSleepReason.POWER_BUTTON) &&
- authenticationInteractor.getPowerButtonInstantlyLocks()
- ) {
- LockImmediately("locked instantly from power button")
- } else if (lastSleepReason == WakeSleepReason.SLEEP_BUTTON) {
- LockImmediately("locked instantly from sleep button")
- } else {
- LockWithDelay("entering sleep")
- }
- } else {
- CancelDelayedLock("waking up")
- }
- },
+ trustInteractor.isTrusted.flatMapLatestConflated { isTrusted ->
+ if (isTrusted) {
+ // When entering a trusted environment, power-related lock events are
+ // ignored.
+ Log.d(TAG, "In trusted environment, ignoring power-related lock events")
+ flowOf(CancelDelayedLock("in trusted environment"))
+ } else {
+ // When not in a trusted environment, power-related lock events are treated
+ // as normal.
+ Log.d(
+ TAG,
+ "Not in trusted environment, power-related lock events treated as" +
+ " normal",
+ )
+ merge(
+ // Device wakefulness events.
+ powerInteractor.detailedWakefulness
+ .map { Pair(it.isAsleep(), it.lastSleepReason) }
+ .distinctUntilChangedBy { it.first }
+ .map { (isAsleep, lastSleepReason) ->
+ if (isAsleep) {
+ if (
+ (lastSleepReason == WakeSleepReason.POWER_BUTTON) &&
+ authenticationInteractor
+ .getPowerButtonInstantlyLocks()
+ ) {
+ LockImmediately("locked instantly from power button")
+ } else if (
+ lastSleepReason == WakeSleepReason.SLEEP_BUTTON
+ ) {
+ LockImmediately("locked instantly from sleep button")
+ } else {
+ LockWithDelay("entering sleep")
+ }
+ } else {
+ CancelDelayedLock("waking up")
+ }
+ },
+ // Started dreaming
+ powerInteractor.isInteractive.flatMapLatestConflated { isInteractive ->
+ // Only respond to dream state changes while the device is
+ // interactive.
+ if (isInteractive) {
+ keyguardInteractor.isDreamingAny.distinctUntilChanged().map {
+ isDreaming ->
+ if (isDreaming) {
+ LockWithDelay("started dreaming")
+ } else {
+ CancelDelayedLock("stopped dreaming")
+ }
+ }
+ } else {
+ emptyFlow()
+ }
+ },
+ )
+ }
+ },
// Device enters lockdown.
isInLockdown
.distinctUntilChanged()
.filter { it }
.map { LockImmediately("lockdown") },
- // Started dreaming
- powerInteractor.isInteractive.flatMapLatestConflated { isInteractive ->
- // Only respond to dream state changes while the device is interactive.
- if (isInteractive) {
- keyguardInteractor.isDreamingAny.distinctUntilChanged().map { isDreaming ->
- if (isDreaming) {
- LockWithDelay("started dreaming")
- } else {
- CancelDelayedLock("stopped dreaming")
- }
- }
- } else {
- emptyFlow()
- }
- },
- lockNowRequests.receiveAsFlow().map { LockImmediately("lockNow") },
+ lockNowRequests.receiveAsFlow().map { reason -> LockImmediately(reason) },
)
.collectLatest(::onLockEvent)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 1d36076347bf..c1a59f180225 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -671,7 +671,7 @@ public class KeyguardService extends Service {
checkPermission();
if (SceneContainerFlag.isEnabled()) {
- mDeviceEntryInteractorLazy.get().lockNow();
+ mDeviceEntryInteractorLazy.get().lockNow("doKeyguardTimeout");
} else if (KeyguardWmStateRefactor.isEnabled()) {
mKeyguardServiceLockNowInteractor.onKeyguardServiceDoKeyguardTimeout(options);
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index df3633be4625..869edfa2b886 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -29,12 +29,14 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInte
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.domain.interactor.WallpaperFocalAreaInteractor
import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder
+import com.android.systemui.keyguard.ui.binder.KeyguardJankBinder
import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder
import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder
import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea
import com.android.systemui.keyguard.ui.view.KeyguardRootView
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardJankViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel
@@ -65,6 +67,7 @@ class KeyguardViewConfigurator
constructor(
private val keyguardRootView: KeyguardRootView,
private val keyguardRootViewModel: KeyguardRootViewModel,
+ private val keyguardJankViewModel: KeyguardJankViewModel,
private val screenOffAnimationController: ScreenOffAnimationController,
private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel,
private val chipbarCoordinator: ChipbarCoordinator,
@@ -93,9 +96,11 @@ constructor(
) : CoreStartable {
private var rootViewHandle: DisposableHandle? = null
+ private var jankHandle: DisposableHandle? = null
override fun start() {
bindKeyguardRootView()
+ bindJankViewModel()
initializeViews()
if (lightRevealMigration()) {
@@ -145,11 +150,9 @@ constructor(
clockInteractor,
wallpaperFocalAreaInteractor,
keyguardClockViewModel,
- interactionJankMonitor,
deviceEntryHapticsInteractor,
vibratorHelper,
falsingManager,
- keyguardViewMediator,
statusBarKeyguardViewManager,
mainDispatcher,
msdlPlayer,
@@ -157,5 +160,22 @@ constructor(
)
}
+ private fun bindJankViewModel() {
+ if (SceneContainerFlag.isEnabled) {
+ return
+ }
+
+ jankHandle?.dispose()
+ jankHandle =
+ KeyguardJankBinder.bind(
+ keyguardRootView,
+ keyguardJankViewModel,
+ interactionJankMonitor,
+ clockInteractor,
+ keyguardViewMediator,
+ mainDispatcher,
+ )
+ }
+
fun getKeyguardRootView() = keyguardRootView
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 5baef915ea01..fd50485fc3a3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -16,6 +16,7 @@
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;
@@ -75,6 +76,7 @@ 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;
@@ -190,6 +192,8 @@ 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;
@@ -271,6 +275,9 @@ 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)
@@ -288,6 +295,8 @@ 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)
*/
@@ -345,6 +354,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
private final ScreenOffAnimationController mScreenOffAnimationController;
private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController;
private final Lazy<ShadeController> mShadeController;
+ /*
+ * 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;
@@ -352,7 +366,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
private boolean mShuttingDown;
private boolean mDozing;
private boolean mAnimatingScreenOff;
- private boolean mIgnoreDismiss;
private final Context mContext;
private final FalsingCollector mFalsingCollector;
@@ -615,41 +628,92 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
};
- KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() {
+ @VisibleForTesting
+ protected UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() {
@Override
- public void onKeyguardVisibilityChanged(boolean visible) {
- synchronized (KeyguardViewMediator.this) {
- if (!visible && mPendingPinLock) {
- Log.i(TAG, "PIN lock requested, starting keyguard");
+ public void onBeforeUserSwitching(int newUser, @NonNull Runnable resultCallback) {
+ mHandler.sendMessage(mHandler.obtainMessage(BEFORE_USER_SWITCHING,
+ newUser, 0, resultCallback));
+ }
- // Bring the keyguard back in order to show the PIN lock
- mPendingPinLock = false;
- doKeyguardLocked(null);
- }
- }
+ @Override
+ public void onUserChanging(int newUser, @NonNull Context userContext,
+ @NonNull Runnable resultCallback) {
+ mHandler.sendMessage(mHandler.obtainMessage(USER_SWITCHING,
+ newUser, 0, resultCallback));
}
@Override
- public void onUserSwitching(int userId) {
- Log.d(TAG, String.format("onUserSwitching %d", userId));
- synchronized (KeyguardViewMediator.this) {
- mIgnoreDismiss = true;
- notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId));
- resetKeyguardDonePendingLocked();
+ 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();
- adjustStatusBarLocked();
}
+ resultCallback.run();
}
+ }
- @Override
- public void onUserSwitchComplete(int userId) {
- mIgnoreDismiss = false;
- Log.d(TAG, String.format("onUserSwitchComplete %d", userId));
+ /**
+ * 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
+ public void onKeyguardVisibilityChanged(boolean visible) {
+ synchronized (KeyguardViewMediator.this) {
+ if (!visible && mPendingPinLock) {
+ Log.i(TAG, "PIN lock requested, starting keyguard");
+
+ // Bring the keyguard back in order to show the PIN lock
+ mPendingPinLock = false;
+ doKeyguardLocked(null);
+ }
+ }
+ }
@Override
public void onDeviceProvisioned() {
@@ -1658,7 +1722,13 @@ 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);
@@ -1707,7 +1777,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()) {
- mUpdateCallback.onUserSwitching(mUserTracker.getUserId());
+ mUserChangedCallback.onUserChanging(mUserTracker.getUserId(), mContext, () -> {});
}
}
// Most services aren't available until the system reaches the ready state, so we
@@ -2341,12 +2411,23 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
* Enable the keyguard if the settings are appropriate.
*/
private void doKeyguardLocked(Bundle options) {
+ 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;
}
@@ -2364,6 +2445,7 @@ 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;
}
@@ -2376,6 +2458,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
+ "previously hiding. It should be safe to short-circuit "
+ "here.");
resetStateLocked(/* hideBouncer= */ false);
+ notifyLockNowCallback();
return;
}
} else {
@@ -2402,6 +2485,7 @@ 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;
}
@@ -2409,6 +2493,7 @@ 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;
}
@@ -2456,11 +2541,6 @@ 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;
@@ -2478,7 +2558,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
private void resetStateLocked(boolean hideBouncer) {
- if (DEBUG) Log.e(TAG, "resetStateLocked");
+ if (DEBUG) Log.d(TAG, "resetStateLocked");
Message msg = mHandler.obtainMessage(RESET, hideBouncer ? 1 : 0, 0);
mHandler.sendMessage(msg);
}
@@ -2726,6 +2806,18 @@ 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);
}
@@ -2867,6 +2959,9 @@ 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.
@@ -2911,6 +3006,7 @@ 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");
@@ -2969,12 +3065,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable,
}
}
- private final Runnable mKeyguardGoingAwayRunnable = new Runnable() {
+ 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;
@@ -3011,6 +3106,10 @@ 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
@@ -3149,6 +3248,30 @@ 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
@@ -3393,6 +3516,13 @@ 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 "
@@ -3493,6 +3623,7 @@ 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;
}
@@ -3513,6 +3644,9 @@ 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) {
@@ -3966,6 +4100,29 @@ 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--) {
@@ -4130,4 +4287,14 @@ 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/data/repository/BiometricSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
index dd2bec143292..0f5f31302670 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepository.kt
@@ -55,6 +55,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
@@ -289,7 +290,7 @@ constructor(
}
private val areBiometricsEnabledForDeviceEntryFromUserSetting: Flow<Triple<Int, Boolean, Int>> =
- conflatedCallbackFlow {
+ callbackFlow {
val callback =
object : IBiometricEnabledOnKeyguardCallback.Stub() {
override fun onChanged(enabled: Boolean, userId: Int, modality: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
index aa28c5b90090..a9729eaad266 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
@@ -124,7 +124,7 @@ constructor(
init {
legacySettingSyncer.startSyncing()
- dumpManager.registerDumpable("KeyguardQuickAffordances", Dumpster())
+ dumpManager.registerNormalDumpable("KeyguardQuickAffordances", Dumpster())
}
/**
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
new file mode 100644
index 000000000000..0cb684a1aabe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardJankBinder.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.binder
+
+import android.view.ViewGroup
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.internal.jank.Cuj.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD
+import com.android.internal.jank.Cuj.CUJ_LOCKSCREEN_TRANSITION_TO_AOD
+import com.android.internal.jank.Cuj.CUJ_SCREEN_OFF_SHOW_AOD
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.keyguard.KeyguardViewMediator
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
+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 kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/** Jank monitoring related to keyguard and transitions. */
+@OptIn(ExperimentalCoroutinesApi::class)
+object KeyguardJankBinder {
+ @JvmStatic
+ fun bind(
+ view: ViewGroup,
+ viewModel: KeyguardJankViewModel,
+ jankMonitor: InteractionJankMonitor?,
+ clockInteractor: KeyguardClockInteractor,
+ keyguardViewMediator: KeyguardViewMediator?,
+ mainImmediateDispatcher: CoroutineDispatcher,
+ ): DisposableHandle? {
+ if (jankMonitor == null) {
+ return null
+ }
+
+ fun processStep(step: TransitionStep, cuj: Int) {
+ val clockId = clockInteractor.renderedClockId
+ when (step.transitionState) {
+ TransitionState.STARTED -> {
+ val builder =
+ InteractionJankMonitor.Configuration.Builder.withView(cuj, view)
+ .setTag(clockId)
+ jankMonitor.begin(builder)
+ }
+
+ TransitionState.CANCELED -> jankMonitor.cancel(cuj)
+
+ TransitionState.FINISHED -> jankMonitor.end(cuj)
+
+ TransitionState.RUNNING -> Unit
+ }
+ }
+
+ return view.repeatWhenAttached(mainImmediateDispatcher) {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ launch {
+ viewModel.goneToAodTransition.collect {
+ processStep(it, CUJ_SCREEN_OFF_SHOW_AOD)
+ if (it.transitionState == TransitionState.FINISHED) {
+ keyguardViewMediator?.maybeHandlePendingLock()
+ }
+ }
+ }
+
+ launch {
+ viewModel.lockscreenToAodTransition.collect {
+ processStep(it, CUJ_LOCKSCREEN_TRANSITION_TO_AOD)
+ }
+ }
+
+ launch {
+ viewModel.aodToLockscreenTransition.collect {
+ processStep(it, CUJ_LOCKSCREEN_TRANSITION_FROM_AOD)
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 017fe169ca88..e48af773497a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -37,8 +37,6 @@ import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.tracing.coroutines.launchTraced as launch
-import com.android.internal.jank.InteractionJankMonitor
-import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD
import com.android.keyguard.AuthInteractionProperties
import com.android.systemui.Flags
import com.android.systemui.Flags.msdlFeedback
@@ -51,11 +49,9 @@ import com.android.systemui.common.ui.view.onLayoutChanged
import com.android.systemui.common.ui.view.onTouchListener
import com.android.systemui.customization.R as customR
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
-import com.android.systemui.keyguard.KeyguardViewMediator
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.domain.interactor.WallpaperFocalAreaInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.ui.view.layout.sections.AodPromotedNotificationSection
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
@@ -110,11 +106,9 @@ object KeyguardRootViewBinder {
clockInteractor: KeyguardClockInteractor,
wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor,
clockViewModel: KeyguardClockViewModel,
- interactionJankMonitor: InteractionJankMonitor?,
deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?,
vibratorHelper: VibratorHelper?,
falsingManager: FalsingManager?,
- keyguardViewMediator: KeyguardViewMediator?,
statusBarKeyguardViewManager: StatusBarKeyguardViewManager?,
mainImmediateDispatcher: CoroutineDispatcher,
msdlPlayer: MSDLPlayer?,
@@ -308,35 +302,6 @@ object KeyguardRootViewBinder {
}
}
- interactionJankMonitor?.let { jankMonitor ->
- launch {
- viewModel.goneToAodTransition.collect {
- when (it.transitionState) {
- TransitionState.STARTED -> {
- val clockId = clockInteractor.renderedClockId
- val builder =
- InteractionJankMonitor.Configuration.Builder.withView(
- CUJ_SCREEN_OFF_SHOW_AOD,
- view,
- )
- .setTag(clockId)
- jankMonitor.begin(builder)
- }
-
- TransitionState.CANCELED ->
- jankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD)
-
- TransitionState.FINISHED -> {
- keyguardViewMediator?.maybeHandlePendingLock()
- jankMonitor.end(CUJ_SCREEN_OFF_SHOW_AOD)
- }
-
- TransitionState.RUNNING -> Unit
- }
- }
- }
- }
-
launch {
shadeInteractor.isAnyFullyExpanded.collect { isFullyAnyExpanded ->
view.visibility =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt
index ea4acce037b8..6c9a755148e9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodPromotedNotificationSection.kt
@@ -42,6 +42,10 @@ constructor(
) : KeyguardSection() {
var view: ComposeView? = null
+ init {
+ logger.logSectionCreated(this)
+ }
+
override fun addViews(constraintLayout: ConstraintLayout) {
if (!PromotedNotificationUiAod.isEnabled) {
return
@@ -56,7 +60,7 @@ constructor(
constraintLayout.addView(this)
}
- logger.logSectionAddedViews()
+ logger.logSectionAddedViews(this)
}
override fun bindData(constraintLayout: ConstraintLayout) {
@@ -68,7 +72,7 @@ constructor(
// Do nothing; the binding happens in the AODPromotedNotification Composable.
- logger.logSectionBoundData()
+ logger.logSectionBoundData(this)
}
override fun applyConstraints(constraintSet: ConstraintSet) {
@@ -76,7 +80,8 @@ constructor(
return
}
- checkNotNull(view)
+ // view may have been created by a different instance of the section (!), and we don't
+ // actually *need* it to set constraints, so don't check for it here.
constraintSet.apply {
val isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value
@@ -90,7 +95,7 @@ constructor(
constrainHeight(viewId, ConstraintSet.WRAP_CONTENT)
}
- logger.logSectionAppliedConstraints()
+ logger.logSectionAppliedConstraints(this)
}
override fun removeViews(constraintLayout: ConstraintLayout) {
@@ -102,7 +107,7 @@ constructor(
view = null
- logger.logSectionRemovedViews()
+ logger.logSectionRemovedViews(this)
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardJankViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardJankViewModel.kt
new file mode 100644
index 000000000000..2642505b32cc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardJankViewModel.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.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.Edge
+import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
+import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.scene.shared.model.Scenes
+import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class KeyguardJankViewModel
+@Inject
+constructor(
+ private val keyguardInteractor: KeyguardInteractor,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) {
+ val goneToAodTransition =
+ keyguardTransitionInteractor.transition(
+ edge = Edge.create(Scenes.Gone, AOD),
+ edgeWithoutSceneContainer = Edge.create(GONE, AOD),
+ )
+
+ val lockscreenToAodTransition =
+ keyguardTransitionInteractor.transition(
+ edge = Edge.create(Scenes.Lockscreen, AOD),
+ edgeWithoutSceneContainer = Edge.create(LOCKSCREEN, AOD),
+ )
+
+ val aodToLockscreenTransition =
+ keyguardTransitionInteractor.transition(
+ edge = Edge.create(AOD, Scenes.Lockscreen),
+ edgeWithoutSceneContainer = Edge.create(AOD, LOCKSCREEN),
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt
index ece97bd27df7..9e32dd8d74ae 100644
--- a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt
@@ -29,6 +29,7 @@ import java.io.PrintWriter
import java.util.Optional
import javax.inject.Inject
import javax.inject.Named
+import javax.inject.Provider
/**
* Monitors ambient light signals, applies a debouncing algorithm, and produces the current ambient
@@ -43,7 +44,7 @@ class AmbientLightModeMonitor
constructor(
private val algorithm: Optional<DebounceAlgorithm>,
private val sensorManager: AsyncSensorManager,
- @Named(LIGHT_SENSOR) private val lightSensor: Optional<Sensor>,
+ @Named(LIGHT_SENSOR) private val lightSensor: Optional<Provider<Sensor>>,
) : Dumpable {
companion object {
private const val TAG = "AmbientLightModeMonitor"
@@ -67,7 +68,7 @@ constructor(
fun start(callback: Callback) {
if (DEBUG) Log.d(TAG, "start monitoring ambient light mode")
- if (lightSensor.isEmpty) {
+ if (lightSensor.isEmpty || lightSensor.get().get() == null) {
if (DEBUG) Log.w(TAG, "light sensor not available")
return
}
@@ -80,7 +81,7 @@ constructor(
algorithm.get().start(callback)
sensorManager.registerListener(
mSensorEventListener,
- lightSensor.get(),
+ lightSensor.get().get(),
SensorManager.SENSOR_DELAY_NORMAL,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
index c08be51c0699..8469cb4ab565 100644
--- a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
@@ -16,6 +16,7 @@
package com.android.systemui.lowlightclock.dagger;
+import android.annotation.Nullable;
import android.content.res.Resources;
import android.hardware.Sensor;
@@ -100,6 +101,7 @@ public abstract class LowLightModule {
abstract LowLightDisplayController bindsLowLightDisplayController();
@BindsOptionalOf
+ @Nullable
@Named(LIGHT_SENSOR)
abstract Sensor bindsLightSensor();
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
index 3eac12d2b24c..ea5f81c75405 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt
@@ -306,7 +306,7 @@ class LegacyMediaDataManagerImpl(
}
init {
- dumpManager.registerDumpable(TAG, this)
+ dumpManager.registerNormalDumpable(TAG, this)
// Initialize the internal processing pipeline. The listeners at the front of the pipeline
// are set as internal listeners so that they receive events. From there, events are
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/resume/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/resume/MediaResumeListener.kt
index ad84a5eb74ab..0a70e78bf039 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/resume/MediaResumeListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/resume/MediaResumeListener.kt
@@ -141,7 +141,7 @@ constructor(
init {
if (useMediaResumption) {
- dumpManager.registerDumpable(TAG, this)
+ dumpManager.registerNormalDumpable(TAG, this)
val unlockFilter = IntentFilter()
unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED)
broadcastDispatcher.registerReceiver(
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
index f05029b2d55f..c604bfb2d5c1 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt
@@ -343,7 +343,7 @@ constructor(
.stateIn(applicationScope, SharingStarted.Eagerly, true)
init {
- dumpManager.registerDumpable(TAG, this)
+ dumpManager.registerNormalDumpable(TAG, this)
mediaFrame = inflateMediaCarousel()
mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
index c00e14c5957e..6501c437caf9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaViewController.kt
@@ -983,13 +983,20 @@ constructor(
val overrideSize = mediaHostStatesManager.carouselSizes[location]
var overridden = false
overrideSize?.let {
- // To be safe we're using a maximum here. The override size should always be set
- // properly though.
- if (
+ if (SceneContainerFlag.isEnabled) {
+ result.measureWidth = widthInSceneContainerPx
+ result.measureHeight = heightInSceneContainerPx
+ overridden = true
+ } else if (
result.measureHeight != it.measuredHeight || result.measureWidth != it.measuredWidth
) {
+ // To be safe we're using a maximum here. The override size should always be set
+ // properly though.
result.measureHeight = Math.max(it.measuredHeight, result.measureHeight)
result.measureWidth = Math.max(it.measuredWidth, result.measureWidth)
+ overridden = true
+ }
+ if (overridden) {
// The measureHeight and the shown height should both be set to the overridden
// height
result.height = result.measureHeight
@@ -1001,7 +1008,6 @@ constructor(
state.width = result.width
}
}
- overridden = true
}
}
if (overridden && state != null && state.squishFraction <= 1f) {
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 52b3c3ecacc6..b391cb079ec5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -156,6 +156,34 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
boolean isMutingExpectedDeviceExist = mController.hasMutingExpectedDevice();
final boolean currentlyConnected = isCurrentlyConnected(device);
boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
+ boolean isSelected = isDeviceIncluded(mController.getSelectedMediaDevice(), device);
+ boolean isDeselectable =
+ isDeviceIncluded(mController.getDeselectableMediaDevice(), device);
+ boolean isSelectable = isDeviceIncluded(mController.getSelectableMediaDevice(), device);
+ boolean isTransferable =
+ isDeviceIncluded(mController.getTransferableMediaDevices(), device);
+ boolean hasRouteListingPreferenceItem = device.hasRouteListingPreferenceItem();
+
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "["
+ + position
+ + "] "
+ + device.getName()
+ + " ["
+ + (isDeselectable ? "deselectable" : "")
+ + "] ["
+ + (isSelected ? "selected" : "")
+ + "] ["
+ + (isSelectable ? "selectable" : "")
+ + "] ["
+ + (isTransferable ? "transferable" : "")
+ + "] ["
+ + (hasRouteListingPreferenceItem ? "hasListingPreference" : "")
+ + "]");
+ }
+
if (mCurrentActivePosition == position) {
mCurrentActivePosition = -1;
}
@@ -210,8 +238,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
}
} else if (device.hasSubtext()) {
boolean isActiveWithOngoingSession =
- (device.hasOngoingSession() && (currentlyConnected || isDeviceIncluded(
- mController.getSelectedMediaDevice(), device)));
+ device.hasOngoingSession() && (currentlyConnected || isSelected);
boolean isHost = device.isHostForOngoingSession()
&& isActiveWithOngoingSession;
if (isActiveWithOngoingSession) {
@@ -266,16 +293,13 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
setSingleLineLayout(device.getName(), false /* showSeekBar*/,
true /* showProgressBar */, false /* showCheckBox */,
false /* showEndTouchArea */);
- } else if (mController.getSelectedMediaDevice().size() > 1
- && isDeviceIncluded(mController.getSelectedMediaDevice(), device)) {
+ } else if (mController.getSelectedMediaDevice().size() > 1 && isSelected) {
// selected device in group
- boolean isDeviceDeselectable = isDeviceIncluded(
- mController.getDeselectableMediaDevice(), device);
- boolean showEndArea = !Flags.enableOutputSwitcherSessionGrouping()
- || isDeviceDeselectable;
+ boolean showEndArea =
+ !Flags.enableOutputSwitcherSessionGrouping() || isDeselectable;
updateUnmutedVolumeIcon(device);
- updateGroupableCheckBox(true, isDeviceDeselectable, device);
- updateEndClickArea(device, isDeviceDeselectable);
+ updateGroupableCheckBox(true, isDeselectable, device);
+ updateEndClickArea(device, isDeselectable);
disableFocusPropertyForView(mContainerLayout);
setUpContentDescriptionForView(mSeekBar, device);
setSingleLineLayout(device.getName(), true /* showSeekBar */,
@@ -307,10 +331,8 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
//If device is connected and there's other selectable devices, layout as
// one of selected devices.
updateUnmutedVolumeIcon(device);
- boolean isDeviceDeselectable = isDeviceIncluded(
- mController.getDeselectableMediaDevice(), device);
- updateGroupableCheckBox(true, isDeviceDeselectable, device);
- updateEndClickArea(device, isDeviceDeselectable);
+ updateGroupableCheckBox(true, isDeselectable, device);
+ updateEndClickArea(device, isDeselectable);
disableFocusPropertyForView(mContainerLayout);
setUpContentDescriptionForView(mSeekBar, device);
setSingleLineLayout(device.getName(), true /* showSeekBar */,
@@ -327,12 +349,16 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter {
false /* showEndTouchArea */);
initSeekbar(device, isCurrentSeekbarInvisible);
}
- } else if (isDeviceIncluded(mController.getSelectableMediaDevice(), device)) {
+ } else if (isSelectable) {
//groupable device
setUpDeviceIcon(device);
updateGroupableCheckBox(false, true, device);
updateEndClickArea(device, true);
- updateFullItemClickListener(v -> onItemClick(v, device));
+ if (!Flags.disableTransferWhenAppsDoNotSupport()
+ || isTransferable
+ || hasRouteListingPreferenceItem) {
+ updateFullItemClickListener(v -> onItemClick(v, device));
+ }
setSingleLineLayout(device.getName(), false /* showSeekBar */,
false /* showProgressBar */, true /* showCheckBox */,
true /* showEndTouchArea */);
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 35c872f8a203..02a2befe44e5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -941,6 +941,10 @@ public class MediaSwitchingController
return mLocalMediaManager.getSelectableMediaDevice();
}
+ List<MediaDevice> getTransferableMediaDevices() {
+ return mLocalMediaManager.getTransferableMediaDevices();
+ }
+
public List<MediaDevice> getSelectedMediaDevice() {
if (!enableInputRouting()) {
return mLocalMediaManager.getSelectedMediaDevice();
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl
index 7803f229dda7..d803ebed94e3 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl
+++ b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl
@@ -26,6 +26,6 @@ interface INoteTaskBubblesService {
boolean areBubblesAvailable();
- void showOrHideAppBubble(in Intent intent, in UserHandle userHandle, in Icon icon,
+ void showOrHideNoteBubble(in Intent intent, in UserHandle userHandle, in Icon icon,
in NoteTaskBubbleExpandBehavior bubbleExpandBehavior);
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt
index 169285f6742e..e20ccfa13e17 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt
@@ -76,8 +76,8 @@ constructor(
}
}
- /** Calls the [Bubbles.showOrHideAppBubble] API as [UserHandle.USER_SYSTEM]. */
- open suspend fun showOrHideAppBubble(
+ /** Calls the [Bubbles.showOrHideNoteBubble] API as [UserHandle.USER_SYSTEM]. */
+ open suspend fun showOrHideNoteBubble(
intent: Intent,
userHandle: UserHandle,
icon: Icon,
@@ -85,7 +85,7 @@ constructor(
) {
withContext(bgDispatcher) {
serviceConnector
- .post { it.showOrHideAppBubble(intent, userHandle, icon, bubbleExpandBehavior) }
+ .post { it.showOrHideNoteBubble(intent, userHandle, icon, bubbleExpandBehavior) }
.whenComplete { _, error ->
if (error != null) {
debugLog(error = error) {
@@ -119,7 +119,7 @@ constructor(
return object : INoteTaskBubblesService.Stub() {
override fun areBubblesAvailable() = mOptionalBubbles.isPresent
- override fun showOrHideAppBubble(
+ override fun showOrHideNoteBubble(
intent: Intent,
userHandle: UserHandle,
icon: Icon,
@@ -131,12 +131,12 @@ constructor(
bubbleExpandBehavior ==
NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED &&
bubbles.isBubbleExpanded(
- Bubble.getAppBubbleKeyForApp(intent.`package`, userHandle)
+ Bubble.getNoteBubbleKeyForApp(intent.`package`, userHandle)
)
) {
return@ifPresentOrElse
}
- bubbles.showOrHideAppBubble(intent, userHandle, icon)
+ bubbles.showOrHideNoteBubble(intent, userHandle, icon)
},
{
debugLog {
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index ad1f37070c9d..7e0128ab8f13 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -96,7 +96,7 @@ constructor(
val info = infoReference.getAndSet(null) ?: return
- if (key != Bubble.getAppBubbleKeyForApp(info.packageName, info.user)) return
+ if (key != Bubble.getNoteBubbleKeyForApp(info.packageName, info.user)) return
// Safe guard mechanism, this callback should only be called for app bubbles.
if (info.launchMode !is NoteTaskLaunchMode.AppBubble) return
@@ -219,7 +219,7 @@ constructor(
val intent = createNoteTaskIntent(info, useStylusMode)
val icon =
Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget)
- noteTaskBubblesController.showOrHideAppBubble(
+ noteTaskBubblesController.showOrHideNoteBubble(
intent,
user,
icon,
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
index 8aad61a8c7cb..c7b165415aea 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeOverlayContentViewModel.kt
@@ -52,18 +52,11 @@ constructor(
private val hydrator = Hydrator("NotificationsShadeOverlayContentViewModel.hydrator")
- val isShadeLayoutWide: Boolean by
+ val showClock: Boolean by
hydrator.hydratedStateOf(
- traceName = "isShadeLayoutWide",
- initialValue = shadeInteractor.isShadeLayoutWide.value,
- source = shadeInteractor.isShadeLayoutWide,
- )
-
- val showHeader: Boolean by
- hydrator.hydratedStateOf(
- traceName = "showHeader",
+ traceName = "showClock",
initialValue =
- shouldShowHeader(
+ shouldShowClock(
isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value,
areAnyNotificationsPresent =
activeNotificationsInteractor.areAnyNotificationsPresentValue,
@@ -72,7 +65,7 @@ constructor(
combine(
shadeInteractor.isShadeLayoutWide,
activeNotificationsInteractor.areAnyNotificationsPresent,
- this::shouldShowHeader,
+ this::shouldShowClock,
),
)
@@ -110,7 +103,7 @@ constructor(
shadeInteractor.collapseNotificationsShade(loggingReason = "shade scrim clicked")
}
- private fun shouldShowHeader(
+ private fun shouldShowClock(
isShadeLayoutWide: Boolean,
areAnyNotificationsPresent: Boolean,
): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
index f8d442de0f55..25d53e6d1f1f 100644
--- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
@@ -50,7 +50,7 @@ constructor(
@FalsingCollectorActual private val falsingCollector: FalsingCollector,
private val screenOffAnimationController: ScreenOffAnimationController,
private val statusBarStateController: StatusBarStateController,
- private val cameraGestureHelper: Provider<CameraGestureHelper>,
+ private val cameraGestureHelper: Provider<CameraGestureHelper?>,
) {
/** Whether the screen is on or off. */
val isInteractive: Flow<Boolean> = repository.isInteractive
@@ -154,8 +154,9 @@ constructor(
// or onFinishedGoingToSleep(), carry that state forward. It will be reset by the next
// onStartedGoingToSleep.
val powerButtonLaunchGestureTriggered =
- powerButtonLaunchGestureTriggeredOnWakeUp ||
- repository.wakefulness.value.powerButtonLaunchGestureTriggered
+ !isPowerButtonGestureSuppressed() &&
+ (powerButtonLaunchGestureTriggeredOnWakeUp ||
+ repository.wakefulness.value.powerButtonLaunchGestureTriggered)
repository.updateWakefulness(
rawState = WakefulnessState.STARTING_TO_WAKE,
@@ -204,8 +205,9 @@ constructor(
// If the launch gesture was previously detected via onCameraLaunchGestureDetected, carry
// that state forward. It will be reset by the next onStartedGoingToSleep.
val powerButtonLaunchGestureTriggered =
- powerButtonLaunchGestureTriggeredDuringSleep ||
- repository.wakefulness.value.powerButtonLaunchGestureTriggered
+ !isPowerButtonGestureSuppressed() &&
+ (powerButtonLaunchGestureTriggeredDuringSleep ||
+ repository.wakefulness.value.powerButtonLaunchGestureTriggered)
repository.updateWakefulness(
rawState = WakefulnessState.ASLEEP,
@@ -218,11 +220,7 @@ constructor(
}
fun onCameraLaunchGestureDetected() {
- if (
- cameraGestureHelper
- .get()
- .canCameraGestureBeLaunched(statusBarStateController.getState())
- ) {
+ if (!isPowerButtonGestureSuppressed()) {
repository.updateWakefulness(powerButtonLaunchGestureTriggered = true)
}
}
@@ -240,6 +238,16 @@ constructor(
.collect()
}
+ /**
+ * Whether the power button gesture isn't allowed to launch anything even if a double tap is
+ * detected.
+ */
+ private fun isPowerButtonGestureSuppressed(): Boolean {
+ return cameraGestureHelper
+ .get()
+ ?.canCameraGestureBeLaunched(statusBarStateController.state) == false
+ }
+
companion object {
private const val FSI_WAKE_WHY = "full_screen_intent"
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyConfig.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyConfig.kt
index 67d390d4f10d..79a513f24995 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyConfig.kt
@@ -85,7 +85,7 @@ class PrivacyConfig @Inject constructor(
}
init {
- dumpManager.registerDumpable(TAG, this)
+ dumpManager.registerNormalDumpable(TAG, this)
deviceConfigProxy.addOnPropertiesChangedListener(
DeviceConfig.NAMESPACE_PRIVACY,
uiExecutor,
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt
index eb8ef9bf3e50..f9a7205c56f5 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt
@@ -98,7 +98,7 @@ class PrivacyItemController @Inject constructor(
}
init {
- dumpManager.registerDumpable(TAG, this)
+ dumpManager.registerNormalDumpable(TAG, this)
privacyConfig.addCallback(optionsCallback)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 85b677b65aeb..cf3b4969b07d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -573,8 +573,7 @@ constructor(
onDispose { qqsVisible.value = false }
}
val squishiness by
- viewModel.containerViewModel.quickQuickSettingsViewModel.squishinessViewModel
- .squishiness
+ viewModel.quickQuickSettingsViewModel.squishinessViewModel.squishiness
.collectAsStateWithLifecycle()
Column(modifier = modifier.sysuiResTag(ResIdTags.quickQsPanel)) {
@@ -607,9 +606,7 @@ constructor(
) {
val Tiles =
@Composable {
- QuickQuickSettings(
- viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel
- )
+ QuickQuickSettings(viewModel = viewModel.quickQuickSettingsViewModel)
}
val Media =
@Composable {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
index 219fc2fdc5ec..0dade7438720 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt
@@ -59,6 +59,7 @@ import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor
import com.android.systemui.qs.panels.ui.viewmodel.InFirstPageViewModel
import com.android.systemui.qs.panels.ui.viewmodel.MediaInRowInLandscapeViewModel
+import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel
import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Scenes
@@ -94,6 +95,7 @@ class QSFragmentComposeViewModel
@AssistedInject
constructor(
containerViewModelFactory: QuickSettingsContainerViewModel.Factory,
+ quickQuickSettingsViewModelFactory: QuickQuickSettingsViewModel.Factory,
@Main private val resources: Resources,
footerActionsViewModelFactory: FooterActionsViewModel.Factory,
private val footerActionsController: FooterActionsController,
@@ -102,7 +104,7 @@ constructor(
DisableFlagsInteractor: DisableFlagsInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val largeScreenShadeInterpolator: LargeScreenShadeInterpolator,
- private val shadeInteractor: ShadeInteractor,
+ shadeInteractor: ShadeInteractor,
@ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
private val squishinessInteractor: TileSquishinessInteractor,
@@ -118,6 +120,8 @@ constructor(
) : Dumpable, ExclusiveActivatable() {
val containerViewModel = containerViewModelFactory.create(true)
+ val quickQuickSettingsViewModel = quickQuickSettingsViewModelFactory.create()
+
private val qqsMediaInRowViewModel = mediaInRowInLandscapeViewModelFactory.create(LOCATION_QQS)
private val qsMediaInRowViewModel = mediaInRowInLandscapeViewModelFactory.create(LOCATION_QS)
@@ -475,6 +479,7 @@ constructor(
}
launch { hydrator.activate() }
launch { containerViewModel.activate() }
+ launch { quickQuickSettingsViewModel.activate() }
launch { qqsMediaInRowViewModel.activate() }
launch { qsMediaInRowViewModel.activate() }
awaitCancellation()
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
index 30fb50db82a2..1233a2f285d5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt
@@ -38,8 +38,17 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContent
+import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsViewModel
+import com.android.systemui.plugins.qs.TileDetailsViewModel
import com.android.systemui.qs.flags.QsDetailedView
import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel
+import com.android.systemui.qs.tiles.dialog.InternetDetailsContent
+import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel
+import com.android.systemui.qs.tiles.dialog.ModesDetailsContent
+import com.android.systemui.qs.tiles.dialog.ModesDetailsViewModel
+import com.android.systemui.qs.tiles.dialog.ScreenRecordDetailsContent
+import com.android.systemui.qs.tiles.dialog.ScreenRecordDetailsViewModel
@Composable
fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewModel) {
@@ -107,7 +116,17 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode
style = MaterialTheme.typography.titleSmall,
)
}
- tileDetailedViewModel.GetContentView()
+ MapTileDetailsContent(tileDetailedViewModel)
+ }
+}
+
+@Composable
+private fun MapTileDetailsContent(tileDetailsViewModel: TileDetailsViewModel) {
+ when (tileDetailsViewModel) {
+ is InternetDetailsViewModel -> InternetDetailsContent(tileDetailsViewModel)
+ is ScreenRecordDetailsViewModel -> ScreenRecordDetailsContent(tileDetailsViewModel)
+ is BluetoothDetailsViewModel -> BluetoothDetailsContent()
+ is ModesDetailsViewModel -> ModesDetailsContent(tileDetailsViewModel)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
index 1f4f9f98c5b2..7701b9087e23 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt
@@ -31,6 +31,7 @@ import androidx.compose.foundation.LocalOverscrollFactory
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.clipScrollableContainer
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement.spacedBy
@@ -49,7 +50,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
@@ -101,7 +101,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.customActions
-import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.text.style.TextAlign
@@ -138,7 +137,6 @@ import com.android.systemui.qs.panels.ui.compose.selection.ResizingState
import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation
import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation.FinalResizeOperation
import com.android.systemui.qs.panels.ui.compose.selection.ResizingState.ResizeOperation.TemporaryResizeOperation
-import com.android.systemui.qs.panels.ui.compose.selection.clearSelectionTile
import com.android.systemui.qs.panels.ui.compose.selection.rememberResizingState
import com.android.systemui.qs.panels.ui.compose.selection.rememberSelectionState
import com.android.systemui.qs.panels.ui.compose.selection.selectableTile
@@ -190,6 +188,7 @@ fun DefaultEditTileGrid(
columns: Int,
largeTilesSpan: Int,
modifier: Modifier,
+ onAddTile: (TileSpec) -> Unit,
onRemoveTile: (TileSpec) -> Unit,
onSetTiles: (List<TileSpec>) -> Unit,
onResize: (TileSpec, toIcon: Boolean) -> Unit,
@@ -230,20 +229,26 @@ fun DefaultEditTileGrid(
modifier
.fillMaxSize()
// Apply top padding before the scroll so the scrollable doesn't show under
- // the
- // top bar
+ // the top bar
.padding(top = innerPadding.calculateTopPadding())
.clipScrollableContainer(Orientation.Vertical)
.verticalScroll(scrollState),
) {
AnimatedContent(
- targetState = listState.dragInProgress,
- modifier = Modifier.wrapContentSize(),
+ targetState = listState.dragInProgress || selectionState.selected,
label = "QSEditHeader",
- ) { dragIsInProgress ->
- EditGridHeader(Modifier.dragAndDropRemoveZone(listState, onRemoveTile)) {
- if (dragIsInProgress) {
- RemoveTileTarget()
+ ) { showRemoveTarget ->
+ EditGridHeader(
+ Modifier.dragAndDropRemoveZone(listState, onRemoveTile)
+ .padding(bottom = 26.dp)
+ ) {
+ if (showRemoveTarget) {
+ RemoveTileTarget {
+ selectionState.selection?.let {
+ selectionState.unSelect()
+ onRemoveTile(it.tileSpec)
+ }
+ }
} else {
Text(text = stringResource(id = R.string.drag_to_rearrange_tiles))
}
@@ -283,7 +288,13 @@ fun DefaultEditTileGrid(
Text(text = stringResource(id = R.string.drag_to_add_tiles))
}
- AvailableTileGrid(otherTiles, selectionState, columns, listState)
+ AvailableTileGrid(
+ otherTiles,
+ selectionState,
+ columns,
+ onAddTile,
+ listState,
+ )
}
}
}
@@ -347,22 +358,18 @@ private fun EditGridHeader(
CompositionLocalProvider(
LocalContentColor provides MaterialTheme.colorScheme.onBackground.copy(alpha = .5f)
) {
- Box(
- contentAlignment = Alignment.Center,
- modifier = modifier.fillMaxWidth().wrapContentHeight(),
- ) {
- content()
- }
+ Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxWidth()) { content() }
}
}
@Composable
-private fun RemoveTileTarget() {
+private fun RemoveTileTarget(onClick: () -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = tileHorizontalArrangement(),
modifier =
Modifier.fillMaxHeight()
+ .clickable(onClick = onClick)
.border(1.dp, LocalContentColor.current, shape = CircleShape)
.padding(10.dp),
) {
@@ -441,6 +448,7 @@ private fun AvailableTileGrid(
tiles: List<SizedTile<EditTileViewModel>>,
selectionState: MutableSelectionState,
columns: Int,
+ onAddTile: (TileSpec) -> Unit,
dragAndDropState: DragAndDropState,
) {
// Available tiles aren't visible during drag and drop, so the row/col isn't needed
@@ -478,6 +486,7 @@ private fun AvailableTileGrid(
index = index,
dragAndDropState = dragAndDropState,
selectionState = selectionState,
+ onAddTile = onAddTile,
modifier = Modifier.weight(1f).fillMaxHeight(),
)
}
@@ -682,11 +691,16 @@ private fun AvailableTileGridCell(
index: Int,
dragAndDropState: DragAndDropState,
selectionState: MutableSelectionState,
+ onAddTile: (TileSpec) -> Unit,
modifier: Modifier = Modifier,
) {
val onClickActionName = stringResource(id = R.string.accessibility_qs_edit_tile_add_action)
val stateDescription = stringResource(id = R.string.accessibility_qs_edit_position, index + 1)
val colors = EditModeTileDefaults.editTileColors()
+ val onClick = {
+ onAddTile(cell.tile.tileSpec)
+ selectionState.select(cell.tile.tileSpec, manual = false)
+ }
// Displays the tile as an icon tile with the label underneath
Column(
@@ -697,11 +711,8 @@ private fun AvailableTileGridCell(
Box(
Modifier.fillMaxWidth()
.height(TileHeight)
- .clearSelectionTile(selectionState)
- .semantics(mergeDescendants = true) {
- onClick(onClickActionName) { false }
- this.stateDescription = stateDescription
- }
+ .clickable(onClick = onClick, onClickLabel = onClickActionName)
+ .semantics(mergeDescendants = true) { this.stateDescription = stateDescription }
.dragAndDropTileSource(
SizedTileImpl(cell.tile, cell.width),
dragAndDropState,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
index cc4c3af1dc63..1c540eed8aa0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt
@@ -42,6 +42,7 @@ import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
import com.android.systemui.qs.panels.ui.viewmodel.IconTilesViewModel
import com.android.systemui.qs.panels.ui.viewmodel.InfiniteGridViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel
+import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor.Companion.POSITION_AT_END
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.shared.ui.ElementKeys.toElementKey
import com.android.systemui.res.R
@@ -155,6 +156,7 @@ constructor(
otherTiles = otherTiles,
columns = columns,
modifier = modifier,
+ onAddTile = { onAddTile(it, POSITION_AT_END) },
onRemoveTile = onRemoveTile,
onSetTiles = onSetTiles,
onResize = iconTilesViewModel::resize,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
index c6c6dcaa896c..26dfc7224ff9 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/MutableSelectionState.kt
@@ -21,6 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import com.android.systemui.qs.pipeline.shared.TileSpec
@@ -39,17 +40,19 @@ data class Selection(val tileSpec: TileSpec, val manual: Boolean)
/** Holds the state of the current selection. */
class MutableSelectionState {
- private var _selection = mutableStateOf<Selection?>(null)
-
/** The [Selection] if a tile is selected, null if not. */
- val selection by _selection
+ var selection by mutableStateOf<Selection?>(null)
+ private set
+
+ val selected: Boolean
+ get() = selection != null
fun select(tileSpec: TileSpec, manual: Boolean) {
- _selection.value = Selection(tileSpec, manual)
+ selection = Selection(tileSpec, manual)
}
fun unSelect() {
- _selection.value = null
+ selection = null
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
index 0109e70a467e..1cfa6632a8b0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -158,7 +158,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> {
private void handleClickEvent(@Nullable Expandable expandable) {
if (mFeatureFlags.isEnabled(Flags.BLUETOOTH_QS_TILE_DIALOG)) {
- mDialogViewModel.showDialog(expandable);
+ mDialogViewModel.showDetailsContent(expandable, /* view= */ null);
} else {
// Secondary clicks are header clicks, just toggle.
toggleBluetooth();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt
new file mode 100644
index 000000000000..7d396c58630e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.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.qs.tiles.dialog
+
+import android.view.LayoutInflater
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.systemui.res.R
+
+@Composable
+fun InternetDetailsContent(viewModel: InternetDetailsViewModel) {
+ val coroutineScope = rememberCoroutineScope()
+ val context = LocalContext.current
+
+ val internetDetailsContentManager = remember {
+ viewModel.contentManagerFactory.create(
+ canConfigMobileData = viewModel.getCanConfigMobileData(),
+ canConfigWifi = viewModel.getCanConfigWifi(),
+ coroutineScope = coroutineScope,
+ context = context,
+ )
+ }
+
+ AndroidView(
+ modifier = Modifier.fillMaxSize(),
+ factory = { context ->
+ // Inflate with the existing dialog xml layout and bind it with the manager
+ val view =
+ LayoutInflater.from(context).inflate(R.layout.internet_connectivity_dialog, null)
+ internetDetailsContentManager.bind(view)
+
+ view
+ // TODO: b/377388104 - Polish the internet details view UI
+ },
+ onRelease = { internetDetailsContentManager.unBind() },
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
index df4dddbca9e6..0ed56f62ee6c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt
@@ -16,18 +16,7 @@
package com.android.systemui.qs.tiles.dialog
-import android.util.Log
-import android.view.LayoutInflater
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.viewinterop.AndroidView
import com.android.systemui.plugins.qs.TileDetailsViewModel
-import com.android.systemui.res.R
import com.android.systemui.statusbar.connectivity.AccessPointController
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -37,45 +26,9 @@ class InternetDetailsViewModel
@AssistedInject
constructor(
private val accessPointController: AccessPointController,
- private val contentManagerFactory: InternetDetailsContentManager.Factory,
+ val contentManagerFactory: InternetDetailsContentManager.Factory,
@Assisted private val onLongClick: () -> Unit,
) : TileDetailsViewModel() {
- private lateinit var internetDetailsContentManager: InternetDetailsContentManager
-
- @Composable
- override fun GetContentView() {
- val coroutineScope = rememberCoroutineScope()
- val context = LocalContext.current
-
- internetDetailsContentManager = remember {
- contentManagerFactory.create(
- canConfigMobileData = accessPointController.canConfigMobileData(),
- canConfigWifi = accessPointController.canConfigWifi(),
- coroutineScope = coroutineScope,
- context = context,
- )
- }
- AndroidView(
- modifier = Modifier.fillMaxWidth().fillMaxHeight(),
- factory = { context ->
- // Inflate with the existing dialog xml layout and bind it with the manager
- val view =
- LayoutInflater.from(context)
- .inflate(R.layout.internet_connectivity_dialog, null)
- internetDetailsContentManager.bind(view)
-
- view
- // TODO: b/377388104 - Polish the internet details view UI
- },
- onRelease = {
- internetDetailsContentManager.unBind()
- if (DEBUG) {
- Log.d(TAG, "onRelease")
- }
- },
- )
- }
-
override fun clickOnSettingsButton() {
onLongClick()
}
@@ -96,13 +49,16 @@ constructor(
return "Tab a network to connect"
}
+ fun getCanConfigMobileData(): Boolean {
+ return accessPointController.canConfigMobileData()
+ }
+
+ fun getCanConfigWifi(): Boolean {
+ return accessPointController.canConfigWifi()
+ }
+
@AssistedFactory
interface Factory {
fun create(onLongClick: () -> Unit): InternetDetailsViewModel
}
-
- companion object {
- private const val TAG = "InternetDetailsVModel"
- private val DEBUG: Boolean = Log.isLoggable(TAG, Log.DEBUG)
- }
}
diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/PackageConfig.aidl b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsContent.kt
index 013158676f79..c5ecaffdf188 100644
--- a/packages/CrashRecovery/framework/java/android/service/watchdog/PackageConfig.aidl
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsContent.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,9 +14,13 @@
* limitations under the License.
*/
-package android.service.watchdog;
+package com.android.systemui.qs.tiles.dialog
-/**
- * @hide
- */
-parcelable PackageConfig;
+import androidx.compose.runtime.Composable
+import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid
+
+@Composable
+fun ModesDetailsContent(viewModel: ModesDetailsViewModel) {
+ // TODO(b/378513940): Finish implementing this function.
+ ModeTileGrid(viewModel = viewModel.viewModel)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
index 511597d05d37..9a39c3c095ef 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt
@@ -16,22 +16,14 @@
package com.android.systemui.qs.tiles.dialog
-import androidx.compose.runtime.Composable
import com.android.systemui.plugins.qs.TileDetailsViewModel
-import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid
import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel
/** The view model used for the modes details view in the Quick Settings */
class ModesDetailsViewModel(
private val onSettingsClick: () -> Unit,
- private val viewModel: ModesDialogViewModel,
+ val viewModel: ModesDialogViewModel,
) : TileDetailsViewModel() {
- @Composable
- override fun GetContentView() {
- // TODO(b/378513940): Finish implementing this function.
- ModeTileGrid(viewModel = viewModel)
- }
-
override fun clickOnSettingsButton() {
onSettingsClick()
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsContent.kt
new file mode 100644
index 000000000000..bf1a51d8cd59
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsContent.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.qs.tiles.dialog
+
+import android.view.LayoutInflater
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.viewinterop.AndroidView
+import com.android.systemui.res.R
+import com.android.systemui.screenrecord.ScreenRecordPermissionViewBinder
+
+@Composable
+fun ScreenRecordDetailsContent(viewModel: ScreenRecordDetailsViewModel) {
+ // TODO(b/378514312): Finish implementing this function.
+
+ if (viewModel.recordingController.isScreenCaptureDisabled) {
+ // TODO(b/388345506): Show disabled page here.
+ return
+ }
+
+ val viewBinder: ScreenRecordPermissionViewBinder = remember {
+ viewModel.recordingController.createScreenRecordPermissionViewBinder(
+ viewModel.onStartRecordingClicked
+ )
+ }
+
+ AndroidView(
+ modifier = Modifier.fillMaxWidth().fillMaxHeight(),
+ factory = { context ->
+ // Inflate with the existing dialog xml layout
+ val view = LayoutInflater.from(context).inflate(R.layout.screen_share_dialog, null)
+ viewBinder.bind(view)
+
+ view
+ // TODO(b/378514473): Revamp the details view according to the spec.
+ },
+ onRelease = { viewBinder.unbind() },
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
index 54e4a521c239..c84ddb6fdb36 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt
@@ -16,49 +16,15 @@
package com.android.systemui.qs.tiles.dialog
-import android.view.LayoutInflater
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.viewinterop.AndroidView
import com.android.systemui.plugins.qs.TileDetailsViewModel
-import com.android.systemui.res.R
import com.android.systemui.screenrecord.RecordingController
-import com.android.systemui.screenrecord.ScreenRecordPermissionViewBinder
/** The view model used for the screen record details view in the Quick Settings */
class ScreenRecordDetailsViewModel(
- private val recordingController: RecordingController,
- private val onStartRecordingClicked: Runnable,
+ val recordingController: RecordingController,
+ val onStartRecordingClicked: Runnable,
) : TileDetailsViewModel() {
- private var viewBinder: ScreenRecordPermissionViewBinder =
- recordingController.createScreenRecordPermissionViewBinder(onStartRecordingClicked)
-
- @Composable
- override fun GetContentView() {
- // TODO(b/378514312): Finish implementing this function.
-
- if (recordingController.isScreenCaptureDisabled) {
- // TODO(b/388345506): Show disabled page here.
- return
- }
-
- AndroidView(
- modifier = Modifier.fillMaxWidth().fillMaxHeight(),
- factory = { context ->
- // Inflate with the existing dialog xml layout
- val view = LayoutInflater.from(context).inflate(R.layout.screen_share_dialog, null)
- viewBinder.bind(view)
-
- view
- // TODO(b/378514473): Revamp the details view according to the spec.
- },
- onRelease = { viewBinder.unbind() },
- )
- }
-
override fun clickOnSettingsButton() {
// No settings button in this tile.
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
index c7db04a6b7b2..aa8e4242f64e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt
@@ -19,43 +19,52 @@ package com.android.systemui.qs.ui.viewmodel
import androidx.compose.runtime.getValue
import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel
import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.qs.panels.ui.viewmodel.DetailsViewModel
import com.android.systemui.qs.panels.ui.viewmodel.EditModeViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel
import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel
import com.android.systemui.qs.panels.ui.viewmodel.toolbar.ToolbarViewModel
+import com.android.systemui.shade.domain.interactor.ShadeModeInteractor
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class QuickSettingsContainerViewModel
@AssistedInject
constructor(
brightnessSliderViewModelFactory: BrightnessSliderViewModel.Factory,
- quickQuickSettingsViewModelFactory: QuickQuickSettingsViewModel.Factory,
shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
@Assisted supportsBrightnessMirroring: Boolean,
val tileGridViewModel: TileGridViewModel,
val editModeViewModel: EditModeViewModel,
val detailsViewModel: DetailsViewModel,
val toolbarViewModelFactory: ToolbarViewModel.Factory,
+ shadeModeInteractor: ShadeModeInteractor,
) : ExclusiveActivatable() {
+ private val hydrator = Hydrator("QuickSettingsContainerViewModel.hydrator")
+
val brightnessSliderViewModel =
brightnessSliderViewModelFactory.create(supportsBrightnessMirroring)
- val quickQuickSettingsViewModel = quickQuickSettingsViewModelFactory.create()
-
val shadeHeaderViewModel = shadeHeaderViewModelFactory.create()
+ val showHeader: Boolean by
+ hydrator.hydratedStateOf(
+ traceName = "showHeader",
+ initialValue = !shadeModeInteractor.isShadeLayoutWide.value,
+ source = shadeModeInteractor.isShadeLayoutWide.map { !it },
+ )
+
override suspend fun onActivated(): Nothing {
coroutineScope {
+ launch { hydrator.activate() }
launch { brightnessSliderViewModel.activate() }
- launch { quickQuickSettingsViewModel.activate() }
launch { shadeHeaderViewModel.activate() }
awaitCancellation()
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt
index d9df1ef36847..0add3f515ebf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModel.kt
@@ -16,13 +16,10 @@
package com.android.systemui.qs.ui.viewmodel
-import androidx.compose.runtime.getValue
import com.android.systemui.lifecycle.ExclusiveActivatable
-import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
import dagger.assisted.AssistedFactory
@@ -31,7 +28,6 @@ import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
/**
@@ -46,39 +42,10 @@ constructor(
val shadeInteractor: ShadeInteractor,
val sceneInteractor: SceneInteractor,
val notificationStackAppearanceInteractor: NotificationStackAppearanceInteractor,
- val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
- quickSettingsContainerViewModelFactory: QuickSettingsContainerViewModel.Factory,
) : ExclusiveActivatable() {
- private val hydrator = Hydrator("QuickSettingsContainerViewModel.hydrator")
-
- val isShadeLayoutWide: Boolean by
- hydrator.hydratedStateOf(
- traceName = "isShadeLayoutWide",
- initialValue = shadeInteractor.isShadeLayoutWide.value,
- source = shadeInteractor.isShadeLayoutWide,
- )
-
- val showHeader: Boolean by
- hydrator.hydratedStateOf(
- traceName = "showHeader",
- initialValue = !shadeInteractor.isShadeLayoutWide.value,
- source = shadeInteractor.isShadeLayoutWide.map { !it },
- )
-
- val quickSettingsContainerViewModel = quickSettingsContainerViewModelFactory.create(false)
-
- val showQuickSettingsOverlayHeader: Boolean by
- hydrator.hydratedStateOf(
- traceName = "showQuickSettingsOverlayHeader",
- initialValue = shadeInteractor.isShadeLayoutWide.value,
- source = shadeInteractor.isShadeLayoutWide,
- )
-
override suspend fun onActivated(): Nothing {
coroutineScope {
- launch { hydrator.activate() }
-
launch {
sceneInteractor.currentScene.collect { currentScene ->
when (currentScene) {
@@ -101,8 +68,6 @@ constructor(
)
}
}
-
- launch { quickSettingsContainerViewModel.activate() }
}
awaitCancellation()
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index e36e40d312b5..3604c4cc4cfd 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
@@ -32,7 +32,6 @@ import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.SceneContainerTransitions
import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.shade.shared.flag.DualShade
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -90,29 +89,14 @@ interface KeyguardlessSceneContainerFrameworkModule {
@Provides
fun containerConfig(): SceneContainerConfig {
return SceneContainerConfig(
- // Note that this list is in z-order. The first one is the bottom-most and the
- // last one is top-most.
- sceneKeys =
- listOfNotNull(
- Scenes.Gone,
- Scenes.QuickSettings.takeUnless { DualShade.isEnabled },
- Scenes.Shade.takeUnless { DualShade.isEnabled },
- ),
+ // Note that this list is in z-order. The first one is the bottom-most and the last
+ // one is top-most.
+ sceneKeys = listOf(Scenes.Gone, Scenes.QuickSettings, Scenes.Shade),
initialSceneKey = Scenes.Gone,
transitions = SceneContainerTransitions,
- overlayKeys =
- listOfNotNull(
- Overlays.NotificationsShade.takeIf { DualShade.isEnabled },
- Overlays.QuickSettingsShade.takeIf { DualShade.isEnabled },
- ),
+ overlayKeys = listOf(Overlays.NotificationsShade, Overlays.QuickSettingsShade),
navigationDistances =
- mapOf(
- Scenes.Gone to 0,
- Scenes.Shade to 1.takeUnless { DualShade.isEnabled },
- Scenes.QuickSettings to 2.takeUnless { DualShade.isEnabled },
- )
- .filterValues { it != null }
- .mapValues { checkNotNull(it.value) },
+ mapOf(Scenes.Gone to 0, Scenes.Shade to 1, Scenes.QuickSettings to 2),
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
index fe014524e3da..91677b03d589 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt
@@ -32,7 +32,6 @@ import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.SceneContainerTransitions
import com.android.systemui.scene.ui.viewmodel.SplitEdgeDetector
import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.shade.shared.flag.DualShade
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -103,28 +102,24 @@ interface SceneContainerFrameworkModule {
Scenes.Dream,
Scenes.Lockscreen,
Scenes.Bouncer,
- Scenes.QuickSettings.takeUnless { DualShade.isEnabled },
- Scenes.Shade.takeUnless { DualShade.isEnabled },
+ Scenes.QuickSettings,
+ Scenes.Shade,
),
initialSceneKey = Scenes.Lockscreen,
transitions = SceneContainerTransitions,
overlayKeys =
- listOfNotNull(
- Overlays.NotificationsShade.takeIf { DualShade.isEnabled },
- Overlays.QuickSettingsShade.takeIf { DualShade.isEnabled },
- ),
+ listOfNotNull(Overlays.NotificationsShade, Overlays.QuickSettingsShade),
navigationDistances =
mapOf(
Scenes.Gone to 0,
Scenes.Lockscreen to 0,
Scenes.Communal to 1,
Scenes.Dream to 2,
- Scenes.Shade to 3.takeUnless { DualShade.isEnabled },
- Scenes.QuickSettings to 4.takeUnless { DualShade.isEnabled },
+ Scenes.Shade to 3,
+ Scenes.QuickSettings to 4,
Scenes.Bouncer to 5,
)
- .filterValues { it != null }
- .mapValues { checkNotNull(it.value) },
+ .mapValues { it.value },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 2fd584176220..94e32fcb9ac6 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -44,6 +44,7 @@ import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.domain.interactor.KeyguardEnabledInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.TrustInteractor
import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor.Companion.keyguardScenes
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.model.SceneContainerPlugin
@@ -148,6 +149,7 @@ constructor(
private val activityTransitionAnimator: ActivityTransitionAnimator,
private val shadeModeInteractor: ShadeModeInteractor,
@SceneFrameworkTableLog private val tableLogBuffer: TableLogBuffer,
+ private val trustInteractor: TrustInteractor,
) : CoreStartable {
private val centralSurfaces: CentralSurfaces?
get() = centralSurfacesOptLazy.get().getOrNull()
@@ -173,6 +175,7 @@ constructor(
notifyKeyguardDismissCancelledCallbacks()
refreshLockscreenEnabled()
hydrateActivityTransitionAnimationState()
+ lockWhenDeviceBecomesUntrusted()
} else {
sceneLogger.logFrameworkEnabled(
isEnabled = false,
@@ -998,6 +1001,18 @@ constructor(
)
}
+ private fun lockWhenDeviceBecomesUntrusted() {
+ applicationScope.launch {
+ trustInteractor.isTrusted.pairwise().collect { (wasTrusted, isTrusted) ->
+ if (wasTrusted && !isTrusted && !deviceEntryInteractor.isDeviceEntered.value) {
+ deviceEntryInteractor.lockNow(
+ "Exited trusted environment while not device not entered"
+ )
+ }
+ }
+ }
+ }
+
companion object {
private const val TAG = "SceneContainerStartable"
}
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 40e6d284cbc9..ba7979ca2120 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
@@ -30,6 +30,7 @@ import com.android.compose.animation.scene.UserAction
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.LightRevealScrimViewModel
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
@@ -66,6 +67,7 @@ constructor(
hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory,
val lightRevealScrim: LightRevealScrimViewModel,
val wallpaperViewModel: WallpaperViewModel,
+ keyguardInteractor: KeyguardInteractor,
@Assisted view: View,
@Assisted private val motionEventHandlerReceiver: (MotionEventHandler?) -> Unit,
) : ExclusiveActivatable() {
@@ -96,6 +98,14 @@ constructor(
},
)
+ /** Amount of color saturation for the Flexi🥃 ribbon. */
+ val ribbonColorSaturation: Float by
+ hydrator.hydratedStateOf(
+ traceName = "ribbonColorSaturation",
+ source = keyguardInteractor.dozeAmount.map { 1 - it },
+ initialValue = 1f,
+ )
+
override suspend fun onActivated(): Nothing {
try {
// Sends a MotionEventHandler to the owner of the view-model so they can report
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
index 3bca4e421cbd..1a91afcf6cac 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
@@ -152,8 +152,8 @@ public class AppClipsService extends Service {
return CAPTURE_CONTENT_FOR_NOTE_FAILED;
}
- if (!mOptionalBubbles.get().isAppBubbleTaskId(taskId)) {
- Log.d(TAG, String.format("Taskid %d is not app bubble task", taskId));
+ if (!mOptionalBubbles.get().isNoteBubbleTaskId(taskId)) {
+ Log.d(TAG, String.format("Taskid %d is not note bubble task", taskId));
return CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
index a48d4d4d3b5f..1f02d5a2d0bc 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt
@@ -151,7 +151,7 @@ internal constructor(
registerUserSwitchObserver()
- dumpManager.registerDumpable(TAG, this)
+ dumpManager.registerNormalDumpable(TAG, this)
}
override fun onReceive(context: Context, intent: Intent) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
index cf310dd32613..5b44c082bd72 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
@@ -39,6 +39,7 @@ import com.android.systemui.recents.LauncherProxyService.LauncherProxyListener
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shared.system.QuickStepContract
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.policy.SplitShadeStateController
import com.android.systemui.util.LargeScreenUtils
@@ -155,8 +156,7 @@ constructor(
val splitShadeEnabledChanged = newSplitShadeEnabled != splitShadeEnabled
splitShadeEnabled = newSplitShadeEnabled
largeScreenShadeHeaderActive = LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources)
- notificationsBottomMargin =
- resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom)
+ notificationsBottomMargin = NotificationStackScrollLayout.getBottomMargin(context)
largeScreenShadeHeaderHeight = calculateLargeShadeHeaderHeight()
shadeHeaderHeight = calculateShadeHeaderHeight()
panelMarginHorizontal =
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
index 661f2ae5132c..246177e0c46d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
@@ -35,6 +35,7 @@ import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -45,6 +46,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/** ShadeInteractor implementation for Scene Container. */
+@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class ShadeInteractorSceneContainerImpl
@Inject
@@ -137,20 +139,18 @@ constructor(
override fun expandNotificationsShade(loggingReason: String, transitionKey: TransitionKey?) {
if (shadeModeInteractor.isDualShade) {
- if (Overlays.QuickSettingsShade in sceneInteractor.currentOverlays.value) {
- sceneInteractor.replaceOverlay(
- from = Overlays.QuickSettingsShade,
- to = Overlays.NotificationsShade,
- loggingReason = loggingReason,
- transitionKey = transitionKey,
- )
- } else {
- sceneInteractor.showOverlay(
- overlay = Overlays.NotificationsShade,
- loggingReason = loggingReason,
- transitionKey = transitionKey,
- )
- }
+ // Collapse the quick settings shade if it's expanded (no-op if it isn't).
+ sceneInteractor.hideOverlay(
+ overlay = Overlays.QuickSettingsShade,
+ loggingReason = loggingReason,
+ transitionKey = transitionKey,
+ )
+ // Expand the notifications shade.
+ sceneInteractor.showOverlay(
+ overlay = Overlays.NotificationsShade,
+ loggingReason = loggingReason,
+ transitionKey = transitionKey,
+ )
} else {
sceneInteractor.changeScene(
toScene = Scenes.Shade,
@@ -163,20 +163,18 @@ constructor(
override fun expandQuickSettingsShade(loggingReason: String, transitionKey: TransitionKey?) {
if (shadeModeInteractor.isDualShade) {
- if (Overlays.NotificationsShade in sceneInteractor.currentOverlays.value) {
- sceneInteractor.replaceOverlay(
- from = Overlays.NotificationsShade,
- to = Overlays.QuickSettingsShade,
- loggingReason = loggingReason,
- transitionKey = transitionKey,
- )
- } else {
- sceneInteractor.showOverlay(
- overlay = Overlays.QuickSettingsShade,
- loggingReason = loggingReason,
- transitionKey = transitionKey,
- )
- }
+ // Collapse the notifications shade if it's expanded (no-op if it isn't).
+ sceneInteractor.hideOverlay(
+ overlay = Overlays.NotificationsShade,
+ loggingReason = loggingReason,
+ transitionKey = transitionKey,
+ )
+ // Expand the quick settings shade.
+ sceneInteractor.showOverlay(
+ overlay = Overlays.QuickSettingsShade,
+ loggingReason = loggingReason,
+ transitionKey = transitionKey,
+ )
} else {
val isSplitShade = shadeModeInteractor.isSplitShade
sceneInteractor.changeScene(
@@ -199,12 +197,12 @@ constructor(
// TODO(b/356596436): Define instant transition instead of snapToScene().
sceneInteractor.snapToScene(
toScene = SceneFamilies.Home,
- loggingReason = loggingReason + " (collapseNotificationsShade)",
+ loggingReason = "$loggingReason (collapseNotificationsShade)",
)
} else {
sceneInteractor.changeScene(
toScene = SceneFamilies.Home,
- loggingReason = loggingReason + " (collapseNotificationsShade)",
+ loggingReason = "$loggingReason (collapseNotificationsShade)",
transitionKey =
transitionKey ?: ToSplitShade.takeIf { shadeModeInteractor.isSplitShade },
)
@@ -233,12 +231,12 @@ constructor(
// TODO(b/356596436): Define instant transition instead of snapToScene().
sceneInteractor.snapToScene(
toScene = targetScene,
- loggingReason = loggingReason + " (collapseQuickSettingsShade)",
+ loggingReason = "$loggingReason (collapseQuickSettingsShade)",
)
} else {
sceneInteractor.changeScene(
toScene = targetScene,
- loggingReason = loggingReason + " (collapseQuickSettingsShade)",
+ loggingReason = "$loggingReason (collapseQuickSettingsShade)",
transitionKey = transitionKey ?: ToSplitShade.takeIf { isSplitShade },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
index e38e53d67f61..e5349deaa811 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ScrimShadeTransitionController.kt
@@ -43,7 +43,7 @@ constructor(
shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged)
onPanelExpansionChanged(currentState)
shadeExpansionStateManager.addStateListener(this::onPanelStateChanged)
- dumpManager.registerDumpable(
+ dumpManager.registerNormalDumpable(
ScrimShadeTransitionController::class.java.simpleName,
this::dump
)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index 96128df1b723..51fcf7da3c13 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -23,8 +23,10 @@ import android.icu.text.DateFormat
import android.icu.text.DisplayContext
import android.os.UserHandle
import android.provider.Settings
+import android.view.ViewGroup
import androidx.compose.runtime.getValue
import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
@@ -40,6 +42,10 @@ import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor
import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.phone.ui.StatusBarIconController
+import com.android.systemui.statusbar.phone.ui.TintedIconManager
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
import dagger.assisted.AssistedFactory
@@ -67,27 +73,46 @@ constructor(
val mobileIconsViewModel: MobileIconsViewModel,
private val privacyChipInteractor: PrivacyChipInteractor,
private val clockInteractor: ShadeHeaderClockInteractor,
+ private val tintedIconManagerFactory: TintedIconManager.Factory,
+ private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
+ val statusBarIconController: StatusBarIconController,
+ val notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder,
private val broadcastDispatcher: BroadcastDispatcher,
) : ExclusiveActivatable() {
private val hydrator = Hydrator("ShadeHeaderViewModel.hydrator")
- val highlightNotificationIcons: Boolean by
+ val createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager =
+ tintedIconManagerFactory::create
+
+ val createBatteryMeterViewController:
+ (ViewGroup, StatusBarLocation) -> BatteryMeterViewController =
+ batteryMeterViewControllerFactory::create
+
+ val notificationsChipHighlight: HeaderChipHighlight by
hydrator.hydratedStateOf(
- traceName = "highlightNotificationIcons",
- initialValue = false,
+ traceName = "notificationsChipHighlight",
+ initialValue = HeaderChipHighlight.None,
source =
sceneInteractor.currentOverlays.map { overlays ->
- Overlays.NotificationsShade in overlays
+ when {
+ Overlays.NotificationsShade in overlays -> HeaderChipHighlight.Strong
+ Overlays.QuickSettingsShade in overlays -> HeaderChipHighlight.Weak
+ else -> HeaderChipHighlight.None
+ }
},
)
- val highlightQuickSettingsIcons: Boolean by
+ val quickSettingsChipHighlight: HeaderChipHighlight by
hydrator.hydratedStateOf(
- traceName = "highlightQuickSettingsIcons",
- initialValue = false,
+ traceName = "quickSettingsChipHighlight",
+ initialValue = HeaderChipHighlight.None,
source =
sceneInteractor.currentOverlays.map { overlays ->
- Overlays.QuickSettingsShade in overlays
+ when {
+ Overlays.QuickSettingsShade in overlays -> HeaderChipHighlight.Strong
+ Overlays.NotificationsShade in overlays -> HeaderChipHighlight.Weak
+ else -> HeaderChipHighlight.None
+ }
},
)
@@ -225,6 +250,15 @@ constructor(
)
}
+ /** Represents the background highlight of a header icons chip. */
+ sealed interface HeaderChipHighlight {
+ data object None : HeaderChipHighlight
+
+ data object Weak : HeaderChipHighlight
+
+ data object Strong : HeaderChipHighlight
+ }
+
private fun updateDateTexts(invalidateFormats: Boolean) {
if (invalidateFormats) {
longerDateFormat.value = getFormatFromPattern(longerPattern)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index ead8f6a1123e..0dfc63ea8619 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -16,13 +16,9 @@
package com.android.systemui.statusbar;
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD;
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_TO_AOD;
import static com.android.systemui.keyguard.shared.model.KeyguardState.GONE;
import static com.android.systemui.util.kotlin.JavaAdapterKt.combineFlows;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.os.SystemProperties;
@@ -30,7 +26,6 @@ import android.os.Trace;
import android.text.format.DateFormat;
import android.util.FloatProperty;
import android.util.Log;
-import android.view.Choreographer;
import android.view.View;
import android.view.animation.Interpolator;
@@ -42,8 +37,6 @@ import com.android.compose.animation.scene.OverlayKey;
import com.android.compose.animation.scene.SceneKey;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.jank.InteractionJankMonitor;
-import com.android.internal.jank.InteractionJankMonitor.Configuration;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.DejankUtils;
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
@@ -54,7 +47,6 @@ 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.plugins.statusbar.StatusBarStateController.StateListener;
-import com.android.systemui.res.R;
import com.android.systemui.scene.data.model.SceneStack;
import com.android.systemui.scene.data.model.SceneStackKt;
import com.android.systemui.scene.domain.interactor.SceneBackInteractor;
@@ -113,7 +105,6 @@ public class StatusBarStateControllerImpl implements
private final ArrayList<RankedListener> mListeners = new ArrayList<>();
private final UiEventLogger mUiEventLogger;
- private final Lazy<InteractionJankMonitor> mInteractionJankMonitorLazy;
private final JavaAdapter mJavaAdapter;
private final Lazy<KeyguardInteractor> mKeyguardInteractorLazy;
private final Lazy<KeyguardTransitionInteractor> mKeyguardTransitionInteractorLazy;
@@ -184,7 +175,6 @@ public class StatusBarStateControllerImpl implements
@Inject
public StatusBarStateControllerImpl(
UiEventLogger uiEventLogger,
- Lazy<InteractionJankMonitor> interactionJankMonitorLazy,
JavaAdapter javaAdapter,
Lazy<KeyguardInteractor> keyguardInteractor,
Lazy<KeyguardTransitionInteractor> keyguardTransitionInteractor,
@@ -196,7 +186,6 @@ public class StatusBarStateControllerImpl implements
Lazy<SceneBackInteractor> sceneBackInteractorLazy,
Lazy<AlternateBouncerInteractor> alternateBouncerInteractorLazy) {
mUiEventLogger = uiEventLogger;
- mInteractionJankMonitorLazy = interactionJankMonitorLazy;
mJavaAdapter = javaAdapter;
mKeyguardInteractorLazy = keyguardInteractor;
mKeyguardTransitionInteractorLazy = keyguardTransitionInteractor;
@@ -470,22 +459,6 @@ public class StatusBarStateControllerImpl implements
this, SET_DARK_AMOUNT_PROPERTY, mDozeAmountTarget);
darkAnimator.setInterpolator(Interpolators.LINEAR);
darkAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP);
- darkAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationCancel(Animator animation) {
- cancelInteractionJankMonitor();
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- endInteractionJankMonitor();
- }
-
- @Override
- public void onAnimationStart(Animator animation) {
- beginInteractionJankMonitor();
- }
- });
darkAnimator.start();
return darkAnimator;
}
@@ -511,42 +484,6 @@ public class StatusBarStateControllerImpl implements
return mKeyguardClockInteractorLazy.get().getRenderedClockId();
}
- private void beginInteractionJankMonitor() {
- final boolean shouldPost =
- (mIsDozing && mDozeAmount == 0) || (!mIsDozing && mDozeAmount == 1);
- InteractionJankMonitor monitor = mInteractionJankMonitorLazy.get();
- if (monitor != null && mView != null && mView.isAttachedToWindow()) {
- if (shouldPost) {
- Choreographer.getInstance().postCallback(
- Choreographer.CALLBACK_ANIMATION, this::beginInteractionJankMonitor, null);
- } else {
- Configuration.Builder builder = Configuration.Builder.withView(getCujType(), mView)
- .setTag(getClockId())
- .setDeferMonitorForAnimationStart(false);
- monitor.begin(builder);
- }
- }
- }
-
- private void endInteractionJankMonitor() {
- InteractionJankMonitor monitor = mInteractionJankMonitorLazy.get();
- if (monitor == null) {
- return;
- }
- monitor.end(getCujType());
- }
-
- private void cancelInteractionJankMonitor() {
- InteractionJankMonitor monitor = mInteractionJankMonitorLazy.get();
- if (monitor == null) {
- return;
- }
- monitor.cancel(getCujType());
- }
-
- private int getCujType() {
- return mIsDozing ? CUJ_LOCKSCREEN_TRANSITION_TO_AOD : CUJ_LOCKSCREEN_TRANSITION_FROM_AOD;
- }
@Override
public boolean goingToFullShade() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
index 6ea72b97cb3a..521539866c9c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegate.kt
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.view
import android.content.Context
import android.os.Bundle
+import android.view.View
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel.Companion.CAST_TO_OTHER_DEVICE_ICON
@@ -48,6 +49,11 @@ class EndCastScreenToOtherDeviceDialogDelegate(
R.string.cast_to_other_device_stop_dialog_button,
endMediaProjectionDialogHelper.wrapStopAction(stopAction),
)
+ if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) {
+ window
+ ?.decorView
+ ?.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES)
+ }
}
}
@@ -82,9 +88,7 @@ class EndCastScreenToOtherDeviceDialogDelegate(
hostDeviceName,
)
} else {
- context.getString(
- R.string.cast_to_other_device_stop_dialog_message_entire_screen,
- )
+ context.getString(R.string.cast_to_other_device_stop_dialog_message_entire_screen)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
index b0c832172776..8644c53f7849 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegate.kt
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.chips.casttootherdevice.ui.view
import android.content.Context
import android.os.Bundle
+import android.view.View
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.casttootherdevice.ui.viewmodel.CastToOtherDeviceChipViewModel.Companion.CAST_TO_OTHER_DEVICE_ICON
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
@@ -59,6 +60,11 @@ class EndGenericCastToOtherDeviceDialogDelegate(
R.string.cast_to_other_device_stop_dialog_button,
endMediaProjectionDialogHelper.wrapStopAction(stopAction),
)
+ if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) {
+ window
+ ?.decorView
+ ?.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/MediaProjectionStopDialogModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/MediaProjectionStopDialogModel.kt
index b37c76232f01..52f55fca55bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/MediaProjectionStopDialogModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/model/MediaProjectionStopDialogModel.kt
@@ -34,6 +34,13 @@ sealed interface MediaProjectionStopDialogModel {
*/
fun createAndShowDialog() {
val dialog = dialogDelegate.createDialog()
+ // Prevents the dialog from being dismissed by tapping outside its boundary.
+ // This is specifically required for the stop dialog shown at call end (i.e.,
+ // PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL event) to disallow remote
+ // dismissal by external devices. Other media projection stop dialogs do not require
+ // this since they are triggered explicitly by tapping the status bar chip, in which
+ // case the full screen containing the dialog is not remote dismissible.
+ dialog.setCanceledOnTouchOutside(/* cancel= */ false)
dialog.setOnCancelListener { onDismissAction.invoke() }
dialog.setOnDismissListener { onDismissAction.invoke() }
dialog.show()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
index 72656ca1934c..4e0117ea3709 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.chips.screenrecord.ui.view
import android.app.ActivityManager
import android.content.Context
import android.os.Bundle
+import android.view.View
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
import com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel
@@ -56,6 +57,11 @@ class EndScreenRecordingDialogDelegate(
R.string.screenrecord_stop_dialog_button,
endMediaProjectionDialogHelper.wrapStopAction(stopAction),
)
+ if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) {
+ window
+ ?.decorView
+ ?.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt
index 8ec05677107e..b8db6136e4c4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndGenericShareToAppDialogDelegate.kt
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.view
import android.content.Context
import android.os.Bundle
+import android.view.View
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
import com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel.ShareToAppChipViewModel.Companion.SHARE_TO_APP_ICON
@@ -49,6 +50,11 @@ class EndGenericShareToAppDialogDelegate(
R.string.share_to_app_stop_dialog_button,
endMediaProjectionDialogHelper.wrapStopAction(stopAction),
)
+ if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) {
+ window
+ ?.decorView
+ ?.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt
index 053016e3109d..11a15555aef1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareScreenToAppDialogDelegate.kt
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.chips.sharetoapp.ui.view
import android.content.Context
import android.os.Bundle
+import android.view.View
import com.android.systemui.mediaprojection.data.model.MediaProjectionState
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel
@@ -48,6 +49,11 @@ class EndShareScreenToAppDialogDelegate(
R.string.share_to_app_stop_dialog_button,
endMediaProjectionDialogHelper.wrapStopAction(stopAction),
)
+ if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) {
+ window
+ ?.decorView
+ ?.setAccessibilityDataSensitive(View.ACCESSIBILITY_DATA_SENSITIVE_YES)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
index 375e02989a3d..cf2ec47a36d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt
@@ -16,12 +16,20 @@
package com.android.systemui.statusbar.chips.ui.compose
-import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
@@ -29,12 +37,15 @@ import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.constrain
import androidx.compose.ui.unit.dp
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.viewmodel.rememberChronometerState
+import kotlin.math.min
@Composable
fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier = Modifier) {
@@ -43,6 +54,9 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier =
val hasEmbeddedIcon =
viewModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarView ||
viewModel.icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon
+ val textStyle = MaterialTheme.typography.labelLarge
+ val textColor = Color(viewModel.colors.text(context))
+ val maxTextWidth = dimensionResource(id = R.dimen.ongoing_activity_chip_max_text_width)
val startPadding =
if (isTextOnly || hasEmbeddedIcon) {
0.dp
@@ -57,38 +71,69 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Shown, modifier: Modifier =
} else {
0.dp
}
- val textStyle = MaterialTheme.typography.labelLarge
- val textColor = Color(viewModel.colors.text(context))
+ val textMeasurer = rememberTextMeasurer()
when (viewModel) {
is OngoingActivityChipModel.Shown.Timer -> {
val timerState = rememberChronometerState(startTimeMillis = viewModel.startTimeMs)
+ val text = timerState.currentTimeText
Text(
- text = timerState.currentTimeText,
+ text = text,
style = textStyle,
color = textColor,
+ softWrap = false,
modifier =
- modifier.padding(start = startPadding, end = endPadding).neverDecreaseWidth(),
+ modifier
+ .customTextContentLayout(
+ maxTextWidth = maxTextWidth,
+ startPadding = startPadding,
+ endPadding = endPadding,
+ ) { constraintWidth ->
+ val intrinsicWidth =
+ textMeasurer.measure(text, textStyle, softWrap = false).size.width
+ intrinsicWidth <= constraintWidth
+ }
+ .neverDecreaseWidth(),
)
}
is OngoingActivityChipModel.Shown.Countdown -> {
- ChipText(
- text = viewModel.secondsUntilStarted.toString(),
+ val text = viewModel.secondsUntilStarted.toString()
+ Text(
+ text = text,
style = textStyle,
color = textColor,
- modifier =
- modifier.padding(start = startPadding, end = endPadding).neverDecreaseWidth(),
- backgroundColor = Color(viewModel.colors.background(context).defaultColor),
+ softWrap = false,
+ modifier = modifier.neverDecreaseWidth(),
)
}
is OngoingActivityChipModel.Shown.Text -> {
- ChipText(
- text = viewModel.text,
- style = textStyle,
+ var hasOverflow by remember { mutableStateOf(false) }
+ val text = viewModel.text
+ Text(
+ text = text,
color = textColor,
- modifier = modifier.padding(start = startPadding, end = endPadding),
- backgroundColor = Color(viewModel.colors.background(context).defaultColor),
+ style = textStyle,
+ softWrap = false,
+ modifier =
+ modifier
+ .customTextContentLayout(
+ maxTextWidth = maxTextWidth,
+ startPadding = startPadding,
+ endPadding = endPadding,
+ ) { constraintWidth ->
+ val intrinsicWidth =
+ textMeasurer.measure(text, textStyle, softWrap = false).size.width
+ hasOverflow = intrinsicWidth > constraintWidth
+ constraintWidth.toFloat() / intrinsicWidth.toFloat() > 0.5f
+ }
+ .overflowFadeOut(
+ hasOverflow = { hasOverflow },
+ fadeLength =
+ dimensionResource(
+ id = R.dimen.ongoing_activity_chip_text_fading_edge_length
+ ),
+ ),
)
}
@@ -133,3 +178,83 @@ private class NeverDecreaseWidthNode : Modifier.Node(), LayoutModifierNode {
return layout(width, height) { placeable.place(0, 0) }
}
}
+
+/**
+ * A custom layout modifier for text that ensures its text is only visible if a provided
+ * [shouldShow] callback returns true. Imposes a provided [maxTextWidthPx]. Also, accounts for
+ * provided padding values if provided and ensures its text is placed with the provided padding
+ * included around it.
+ */
+private fun Modifier.customTextContentLayout(
+ maxTextWidth: Dp,
+ startPadding: Dp = 0.dp,
+ endPadding: Dp = 0.dp,
+ shouldShow: (constraintWidth: Int) -> Boolean,
+): Modifier {
+ return this.then(
+ CustomTextContentLayoutElement(maxTextWidth, startPadding, endPadding, shouldShow)
+ )
+}
+
+private data class CustomTextContentLayoutElement(
+ val maxTextWidth: Dp,
+ val startPadding: Dp,
+ val endPadding: Dp,
+ val shouldShow: (constrainedWidth: Int) -> Boolean,
+) : ModifierNodeElement<CustomTextContentLayoutNode>() {
+ override fun create(): CustomTextContentLayoutNode {
+ return CustomTextContentLayoutNode(maxTextWidth, startPadding, endPadding, shouldShow)
+ }
+
+ override fun update(node: CustomTextContentLayoutNode) {
+ node.shouldShow = shouldShow
+ node.maxTextWidth = maxTextWidth
+ node.startPadding = startPadding
+ node.endPadding = endPadding
+ }
+}
+
+private class CustomTextContentLayoutNode(
+ var maxTextWidth: Dp,
+ var startPadding: Dp,
+ var endPadding: Dp,
+ var shouldShow: (constrainedWidth: Int) -> Boolean,
+) : Modifier.Node(), LayoutModifierNode {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints,
+ ): MeasureResult {
+ val horizontalPadding = startPadding + endPadding
+ val maxWidth =
+ min(maxTextWidth.roundToPx(), (constraints.maxWidth - horizontalPadding.roundToPx()))
+ .coerceAtLeast(constraints.minWidth)
+ val placeable = measurable.measure(constraints.copy(maxWidth = maxWidth))
+
+ val height = placeable.height
+ val width = placeable.width
+ return if (shouldShow(maxWidth)) {
+ layout(width + horizontalPadding.roundToPx(), height) {
+ placeable.place(startPadding.roundToPx(), 0)
+ }
+ } else {
+ layout(0, 0) {}
+ }
+ }
+}
+
+private fun Modifier.overflowFadeOut(hasOverflow: () -> Boolean, fadeLength: Dp): Modifier {
+ return graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen).drawWithCache {
+ val width = size.width
+ val start = (width - fadeLength.toPx()).coerceAtLeast(0f)
+ val gradient =
+ Brush.horizontalGradient(
+ colors = listOf(Color.Black, Color.Transparent),
+ startX = start,
+ endX = width,
+ )
+ onDrawWithContent {
+ drawContent()
+ if (hasOverflow()) drawRect(brush = gradient, blendMode = BlendMode.DstIn)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt
deleted file mode 100644
index 3d768d2d3e1e..000000000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipText.kt
+++ /dev/null
@@ -1,114 +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.chips.ui.compose
-
-import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.material3.LocalTextStyle
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithContent
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Brush
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.res.dimensionResource
-import androidx.compose.ui.text.TextLayoutResult
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.rememberTextMeasurer
-import com.android.systemui.res.R
-
-/**
- * Renders text within a status bar chip. The text is only displayed if more than 50% of its width
- * can fit inside the bounds of the chip. If there is any overflow,
- * [R.dimen.ongoing_activity_chip_text_fading_edge_length] is used to fade out the edge of the text.
- */
-@Composable
-fun ChipText(
- text: String,
- backgroundColor: Color,
- modifier: Modifier = Modifier,
- color: Color = Color.Unspecified,
- style: TextStyle = LocalTextStyle.current,
- minimumVisibleRatio: Float = 0.5f,
-) {
- val density = LocalDensity.current
- val textMeasurer = rememberTextMeasurer()
-
- val textFadeLength =
- dimensionResource(id = R.dimen.ongoing_activity_chip_text_fading_edge_length)
- val maxTextWidthDp = dimensionResource(id = R.dimen.ongoing_activity_chip_max_text_width)
- val maxTextWidthPx = with(density) { maxTextWidthDp.toPx() }
-
- val textLayoutResult = remember(text, style) { textMeasurer.measure(text, style) }
- val willOverflowWidth = textLayoutResult.size.width > maxTextWidthPx
-
- if (isSufficientlyVisible(maxTextWidthPx, minimumVisibleRatio, textLayoutResult)) {
- Text(
- text = text,
- style = style,
- softWrap = false,
- color = color,
- modifier =
- modifier
- .sizeIn(maxWidth = maxTextWidthDp)
- .then(
- if (willOverflowWidth) {
- Modifier.overflowFadeOut(
- with(density) { textFadeLength.roundToPx() },
- backgroundColor,
- )
- } else {
- Modifier
- }
- ),
- )
- }
-}
-
-private fun Modifier.overflowFadeOut(fadeLength: Int, color: Color): Modifier = drawWithContent {
- drawContent()
-
- val brush =
- Brush.horizontalGradient(
- colors = listOf(Color.Transparent, color),
- startX = size.width - fadeLength,
- endX = size.width,
- )
- drawRect(
- brush = brush,
- topLeft = Offset(size.width - fadeLength, 0f),
- size = Size(fadeLength.toFloat(), size.height),
- )
-}
-
-/**
- * Returns `true` if at least [minimumVisibleRatio] of the text width fits within the given
- * [maxAvailableWidthPx].
- */
-@Composable
-private fun isSufficientlyVisible(
- maxAvailableWidthPx: Float,
- minimumVisibleRatio: Float,
- textLayoutResult: TextLayoutResult,
-): Boolean {
- val widthPx = textLayoutResult.size.width
-
- return (maxAvailableWidthPx / widthPx) > minimumVisibleRatio
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
index 816f291b9273..b49d46c4a05b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt
@@ -35,6 +35,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.semantics.contentDescription
@@ -42,6 +43,7 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.Expandable
+import com.android.compose.modifiers.thenIf
import com.android.systemui.animation.Expandable
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.common.ui.compose.load
@@ -79,12 +81,11 @@ fun OngoingActivityChip(model: OngoingActivityChipModel.Shown, modifier: Modifie
private fun ChipBody(
model: OngoingActivityChipModel.Shown,
modifier: Modifier = Modifier,
- onClick: () -> Unit = {},
+ onClick: (() -> Unit)? = null,
) {
val context = LocalContext.current
- val isClickable = onClick != {}
+ val isClickable = onClick != null
val hasEmbeddedIcon = model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView
-
val contentDescription =
when (val icon = model.icon) {
is OngoingActivityChipModel.ChipIcon.StatusBarView -> icon.contentDescription.load()
@@ -93,17 +94,28 @@ private fun ChipBody(
is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> null
null -> null
}
-
+ val chipSidePadding = dimensionResource(id = R.dimen.ongoing_activity_chip_side_padding)
+ val minWidth =
+ if (isClickable) {
+ dimensionResource(id = R.dimen.min_clickable_item_size)
+ } else if (model.icon != null) {
+ dimensionResource(id = R.dimen.ongoing_activity_chip_icon_size) + chipSidePadding
+ } else {
+ dimensionResource(id = R.dimen.ongoing_activity_chip_min_text_width) + chipSidePadding
+ }
// Use a Box with `fillMaxHeight` to create a larger click surface for the chip. The visible
// height of the chip is determined by the height of the background of the Row below.
Box(
contentAlignment = Alignment.Center,
modifier =
- modifier.fillMaxHeight().clickable(enabled = isClickable, onClick = onClick).semantics {
- if (contentDescription != null) {
- this.contentDescription = contentDescription
- }
- },
+ modifier
+ .fillMaxHeight()
+ .clickable(enabled = isClickable, onClick = onClick ?: {})
+ .semantics {
+ if (contentDescription != null) {
+ this.contentDescription = contentDescription
+ }
+ },
) {
Row(
horizontalArrangement = Arrangement.Center,
@@ -115,14 +127,15 @@ private fun ChipBody(
)
)
.height(dimensionResource(R.dimen.ongoing_appops_chip_height))
- .widthIn(
- min =
- if (isClickable) {
- dimensionResource(id = R.dimen.min_clickable_item_size)
- } else {
- 0.dp
+ .thenIf(isClickable) { Modifier.widthIn(min = minWidth) }
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ if (constraints.maxWidth >= minWidth.roundToPx()) {
+ placeable.place(0, 0)
}
- )
+ }
+ }
.background(Color(model.colors.background(context).defaultColor))
.padding(
horizontal =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt
index 7358c513eaff..9d5d87f6db7c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/layout/StatusBarContentInsetsProvider.kt
@@ -185,7 +185,7 @@ constructor(
override fun start() {
configurationController.addCallback(this)
- dumpManager.registerDumpable(dumpableName, this)
+ dumpManager.registerNormalDumpable(dumpableName, this)
commandRegistry.registerCommand(commandName) {
StatusBarInsetsCommand(
object : StatusBarInsetsCommand.Callback {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
index 33c71d4a9c5a..098d537fc225 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.promoted
import android.app.Flags
+import android.app.Notification
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
@@ -49,6 +50,7 @@ import com.android.internal.widget.CachingIconView
import com.android.internal.widget.ImageFloatingTextView
import com.android.internal.widget.NotificationExpandButton
import com.android.internal.widget.NotificationProgressBar
+import com.android.internal.widget.NotificationProgressModel
import com.android.internal.widget.NotificationRowIconView
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R as systemuiR
@@ -176,17 +178,17 @@ private class AODPromotedNotificationViewUpdater(root: View) {
private val verificationIcon: ImageView? = root.findViewById(R.id.verification_icon)
private val verificationText: TextView? = root.findViewById(R.id.verification_text)
- private var oldProgressStub = root.findViewById<View>(R.id.progress) as? ViewStub
- private var oldProgress: ProgressBar? = null
- private val newProgress = root.findViewById<View>(R.id.progress) as? NotificationProgressBar
+ private var oldProgressBarStub = root.findViewById<View>(R.id.progress) as? ViewStub
+ private var oldProgressBar: ProgressBar? = null
+ private val newProgressBar = root.findViewById<View>(R.id.progress) as? NotificationProgressBar
fun update(content: PromotedNotificationContentModel) {
when (content.style) {
Style.Base -> updateBase(content)
- Style.BigPicture -> updateBigPicture(content)
- Style.BigText -> updateBigText(content)
- Style.Call -> updateCall(content)
- Style.Progress -> updateProgress(content)
+ Style.BigPicture -> updateBigPictureStyle(content)
+ Style.BigText -> updateBigTextStyle(content)
+ Style.Call -> updateCallStyle(content)
+ Style.Progress -> updateProgressStyle(content)
Style.Ineligible -> {}
}
}
@@ -194,42 +196,72 @@ private class AODPromotedNotificationViewUpdater(root: View) {
private fun updateBase(
content: PromotedNotificationContentModel,
textView: ImageFloatingTextView? = null,
+ showOldProgress: Boolean = true,
) {
updateHeader(content)
updateTitle(title, content)
updateText(textView ?: text, content)
+
+ if (showOldProgress) {
+ updateOldProgressBar(content)
+ }
}
- private fun updateBigPicture(content: PromotedNotificationContentModel) {
+ private fun updateBigPictureStyle(content: PromotedNotificationContentModel) {
updateBase(content)
bigPicture?.visibility = GONE
}
- private fun updateBigText(content: PromotedNotificationContentModel) {
+ private fun updateBigTextStyle(content: PromotedNotificationContentModel) {
updateBase(content, textView = bigText)
}
- private fun updateCall(content: PromotedNotificationContentModel) {
+ private fun updateCallStyle(content: PromotedNotificationContentModel) {
updateConversationHeader(content)
updateText(text, content)
}
- private fun updateProgress(content: PromotedNotificationContentModel) {
- updateBase(content)
+ private fun updateProgressStyle(content: PromotedNotificationContentModel) {
+ updateBase(content, showOldProgress = false)
updateNewProgressBar(content)
}
+ private fun updateOldProgressBar(content: PromotedNotificationContentModel) {
+ if (
+ content.oldProgress == null ||
+ content.oldProgress.max == 0 ||
+ content.oldProgress.isIndeterminate
+ ) {
+ oldProgressBar?.visibility = GONE
+ return
+ }
+
+ inflateOldProgressBar()
+
+ val oldProgressBar = oldProgressBar ?: return
+
+ oldProgressBar.progress = content.oldProgress.progress
+ oldProgressBar.max = content.oldProgress.max
+ oldProgressBar.isIndeterminate = content.oldProgress.isIndeterminate
+ oldProgressBar.visibility = VISIBLE
+ }
+
private fun updateNewProgressBar(content: PromotedNotificationContentModel) {
notificationProgressStartIcon?.visibility = GONE
notificationProgressEndIcon?.visibility = GONE
- content.progress?.let {
- newProgress?.setProgressModel(it.toBundle())
- newProgress?.visibility = VISIBLE
- } ?: run { newProgress?.visibility = GONE }
+
+ val newProgressBar = newProgressBar ?: return
+
+ if (content.newProgress != null && !content.newProgress.isIndeterminate) {
+ newProgressBar.setProgressModel(content.newProgress.toSkeleton().toBundle())
+ newProgressBar.visibility = VISIBLE
+ } else {
+ newProgressBar.visibility = GONE
+ }
}
private fun updateHeader(content: PromotedNotificationContentModel) {
@@ -335,6 +367,15 @@ private class AODPromotedNotificationViewUpdater(root: View) {
chronometerStub = null
}
+ private fun inflateOldProgressBar() {
+ if (oldProgressBar != null) {
+ return
+ }
+
+ oldProgressBar = oldProgressBarStub?.inflate() as ProgressBar
+ oldProgressBarStub = null
+ }
+
private fun updateText(
view: ImageFloatingTextView?,
content: PromotedNotificationContentModel,
@@ -351,7 +392,7 @@ private class AODPromotedNotificationViewUpdater(root: View) {
setTextViewColor(view, color)
if (text != null && text.isNotEmpty()) {
- view?.text = text
+ view?.text = text.toSkeleton()
view?.visibility = VISIBLE
} else {
view?.text = ""
@@ -364,6 +405,38 @@ private class AODPromotedNotificationViewUpdater(root: View) {
}
}
+private fun CharSequence.toSkeleton(): CharSequence {
+ return this.toString()
+}
+
+private fun NotificationProgressModel.toSkeleton(): NotificationProgressModel {
+ if (isIndeterminate) {
+ return NotificationProgressModel(/* indeterminateColor= */ SecondaryText.colorInt)
+ }
+
+ return NotificationProgressModel(
+ listOf(Notification.ProgressStyle.Segment(progressMax).toSkeleton()),
+ points.map { it.toSkeleton() }.toList(),
+ progress,
+ /* isStyledByProgress = */ true,
+ /* segmentsFallbackColor = */ SecondaryText.colorInt,
+ )
+}
+
+private fun Notification.ProgressStyle.Segment.toSkeleton(): Notification.ProgressStyle.Segment {
+ return Notification.ProgressStyle.Segment(length).also {
+ it.id = id
+ it.color = SecondaryText.colorInt
+ }
+}
+
+private fun Notification.ProgressStyle.Point.toSkeleton(): Notification.ProgressStyle.Point {
+ return Notification.ProgressStyle.Point(position).also {
+ it.id = id
+ it.color = SecondaryText.colorInt
+ }
+}
+
private enum class AodPromotedNotificationColor(colorUInt: UInt) {
Background(0x00000000u),
PrimaryText(0xFFFFFFFFu),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
index 24d071c83a5e..035edd9711bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt
@@ -21,6 +21,9 @@ import android.app.Notification.BigPictureStyle
import android.app.Notification.BigTextStyle
import android.app.Notification.CallStyle
import android.app.Notification.EXTRA_CHRONOMETER_COUNT_DOWN
+import android.app.Notification.EXTRA_PROGRESS
+import android.app.Notification.EXTRA_PROGRESS_INDETERMINATE
+import android.app.Notification.EXTRA_PROGRESS_MAX
import android.app.Notification.EXTRA_SUB_TEXT
import android.app.Notification.EXTRA_TEXT
import android.app.Notification.EXTRA_TITLE
@@ -34,6 +37,7 @@ import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCo
import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator.Companion.EXTRA_WAS_AUTOMATICALLY_PROMOTED
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Companion.isPromotedForStatusBarChip
+import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.OldProgress
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When
import javax.inject.Inject
@@ -90,6 +94,7 @@ constructor(
contentBuilder.title = notification.title()
contentBuilder.text = notification.text()
contentBuilder.skeletonLargeIcon = null // TODO
+ contentBuilder.oldProgress = notification.oldProgress()
val colorsFromNotif = recoveredBuilder.getColors(/* header= */ false)
contentBuilder.colors =
@@ -126,6 +131,21 @@ private fun Notification.shortCriticalText(): String? {
private fun Notification.chronometerCountDown(): Boolean =
extras?.getBoolean(EXTRA_CHRONOMETER_COUNT_DOWN, /* defaultValue= */ false) ?: false
+private fun Notification.oldProgress(): OldProgress? {
+ val progress = progress() ?: return null
+ val max = progressMax() ?: return null
+ val isIndeterminate = progressIndeterminate() ?: return null
+
+ return OldProgress(progress = progress, max = max, isIndeterminate = isIndeterminate)
+}
+
+private fun Notification.progress(): Int? = extras?.getInt(EXTRA_PROGRESS)
+
+private fun Notification.progressMax(): Int? = extras?.getInt(EXTRA_PROGRESS_MAX)
+
+private fun Notification.progressIndeterminate(): Boolean? =
+ extras?.getBoolean(EXTRA_PROGRESS_INDETERMINATE)
+
private fun Notification.extractWhen(): When? {
val time = `when`
val showsTime = showsTime()
@@ -191,5 +211,5 @@ private fun CallStyle.extractContent(contentBuilder: PromotedNotificationContent
private fun ProgressStyle.extractContent(contentBuilder: PromotedNotificationContentModel.Builder) {
// TODO: Create NotificationProgressModel.toSkeleton, or something similar.
- contentBuilder.progress = createProgressModel(0xffffffff.toInt(), 0xff000000.toInt())
+ contentBuilder.newProgress = createProgressModel(0xffffffff.toInt(), 0xff000000.toInt())
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt
index 4ccdc65ba91a..468934791525 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.promoted
import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.keyguard.ui.view.layout.sections.AodPromotedNotificationSection
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel.ERROR
import com.android.systemui.log.core.LogLevel.INFO
@@ -25,6 +26,7 @@ import com.android.systemui.statusbar.notification.logKey
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import javax.inject.Inject
+@OptIn(ExperimentalStdlibApi::class)
class PromotedNotificationLogger
@Inject
constructor(@PromotedNotificationLog private val buffer: LogBuffer) {
@@ -92,20 +94,44 @@ constructor(@PromotedNotificationLog private val buffer: LogBuffer) {
buffer.log(AOD_VIEW_BINDER_TAG, INFO, "binder unbound notification")
}
- fun logSectionAddedViews() {
- buffer.log(AOD_SECTION_TAG, INFO, "section added views")
+ fun logSectionCreated(section: AodPromotedNotificationSection) {
+ buffer.log(
+ AOD_SECTION_TAG,
+ INFO,
+ "section ${System.identityHashCode(section).toHexString()} created",
+ )
}
- fun logSectionBoundData() {
- buffer.log(AOD_SECTION_TAG, INFO, "section bound data")
+ fun logSectionAddedViews(section: AodPromotedNotificationSection) {
+ buffer.log(
+ AOD_SECTION_TAG,
+ INFO,
+ "section ${System.identityHashCode(section).toHexString()} added views",
+ )
}
- fun logSectionAppliedConstraints() {
- buffer.log(AOD_SECTION_TAG, INFO, "section applied constraints")
+ fun logSectionBoundData(section: AodPromotedNotificationSection) {
+ buffer.log(
+ AOD_SECTION_TAG,
+ INFO,
+ "section ${System.identityHashCode(section).toHexString()} bound data",
+ )
}
- fun logSectionRemovedViews() {
- buffer.log(AOD_SECTION_TAG, INFO, "section removed views")
+ fun logSectionAppliedConstraints(section: AodPromotedNotificationSection) {
+ buffer.log(
+ AOD_SECTION_TAG,
+ INFO,
+ "section ${System.identityHashCode(section).toHexString()} applied constraints",
+ )
+ }
+
+ fun logSectionRemovedViews(section: AodPromotedNotificationSection) {
+ buffer.log(
+ AOD_SECTION_TAG,
+ INFO,
+ "section ${System.identityHashCode(section).toHexString()} removed views",
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
index 3dacae2114b0..58da5286dd71 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt
@@ -51,6 +51,7 @@ data class PromotedNotificationContentModel(
val title: CharSequence?,
val text: CharSequence?,
val skeletonLargeIcon: Icon?, // TODO(b/377568176): Make into an IconModel.
+ val oldProgress: OldProgress?,
val colors: Colors,
val style: Style,
@@ -61,7 +62,7 @@ data class PromotedNotificationContentModel(
val verificationText: CharSequence?,
// for ProgressStyle:
- val progress: NotificationProgressModel?,
+ val newProgress: NotificationProgressModel?,
) {
class Builder(val key: String) {
var wasPromotedAutomatically: Boolean = false
@@ -75,6 +76,7 @@ data class PromotedNotificationContentModel(
var title: CharSequence? = null
var text: CharSequence? = null
var skeletonLargeIcon: Icon? = null
+ var oldProgress: OldProgress? = null
var style: Style = Style.Ineligible
var colors: Colors = Colors(backgroundColor = 0, primaryTextColor = 0)
@@ -85,7 +87,7 @@ data class PromotedNotificationContentModel(
var verificationText: CharSequence? = null
// for ProgressStyle:
- var progress: NotificationProgressModel? = null
+ var newProgress: NotificationProgressModel? = null
fun build() =
PromotedNotificationContentModel(
@@ -101,13 +103,14 @@ data class PromotedNotificationContentModel(
title = title,
text = text,
skeletonLargeIcon = skeletonLargeIcon,
+ oldProgress = oldProgress,
colors = colors,
style = style,
personIcon = personIcon,
personName = personName,
verificationIcon = verificationIcon,
verificationText = verificationText,
- progress = progress,
+ newProgress = newProgress,
)
}
@@ -129,6 +132,9 @@ data class PromotedNotificationContentModel(
/** The colors used to display the notification. */
data class Colors(@ColorInt val backgroundColor: Int, @ColorInt val primaryTextColor: Int)
+ /** The fields needed to render the old-style progress bar. */
+ data class OldProgress(val progress: Int, val max: Int, val isIndeterminate: Boolean)
+
/** The promotion-eligible style of a notification, or [Style.Ineligible] if not. */
enum class Style {
Base, // style == null
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt
index f265e0ff33f8..56057fb00e45 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt
@@ -54,5 +54,5 @@ class PromotedNotificationViewModel(
val verificationText: Flow<CharSequence?> = content.map { it.verificationText }
// for ProgressStyle:
- val progress: Flow<NotificationProgressModel?> = content.map { it.progress }
+ val progress: Flow<NotificationProgressModel?> = content.map { it.newProgress }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 876090101f6e..ce1fc97cbffe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -26,6 +26,7 @@ import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_
import static com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_CLEAR_ALL;
import static com.android.systemui.Flags.magneticNotificationSwipes;
import static com.android.systemui.Flags.notificationOverExpansionClippingFix;
+import static com.android.systemui.Flags.notificationsRedesignFooterView;
import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT;
import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_SWIPE;
import static com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent.SCROLL_DOWN;
@@ -694,11 +695,28 @@ public class NotificationStackScrollLayout
protected void onFinishInflate() {
super.onFinishInflate();
+ if (notificationsRedesignFooterView()) {
+ int bottomMargin = getBottomMargin(getContext());
+ MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
+ lp.setMargins(lp.leftMargin, lp.topMargin, lp.rightMargin, bottomMargin);
+ setLayoutParams(lp);
+ }
+
if (!ModesEmptyShadeFix.isEnabled()) {
inflateEmptyShadeView();
}
}
+ /** Get the pixel value of the bottom margin, taking flags into account. */
+ public static int getBottomMargin(Context context) {
+ Resources res = context.getResources();
+ if (notificationsRedesignFooterView()) {
+ return res.getDimensionPixelSize(R.dimen.notification_2025_panel_margin_bottom);
+ } else {
+ return res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom);
+ }
+ }
+
/**
* Sets whether keyguard bypass is enabled. If true, this layout will be rendered in bypass
* mode when it is on the keyguard.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
index 06b989aaab57..673eb9f85ec3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java
@@ -107,7 +107,7 @@ public class StackScrollAlgorithm {
mGapHeightOnLockscreen = res.getDimensionPixelSize(
R.dimen.notification_section_divider_height_lockscreen);
mNotificationScrimPadding = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
- mMarginBottom = res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom);
+ mMarginBottom = NotificationStackScrollLayout.getBottomMargin(context);
mQuickQsOffsetHeight = SystemBarUtils.getQuickQsOffsetHeight(context);
mSmallCornerRadius = res.getDimension(R.dimen.notification_corner_radius_small);
mLargeCornerRadius = res.getDimension(R.dimen.notification_corner_radius);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
index 107875df6bfa..c8b3341a0240 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
@@ -26,6 +26,7 @@ import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.LargeScreenHeaderHelper
import com.android.systemui.shade.ShadeDisplayAware
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.policy.SplitShadeStateController
import dagger.Lazy
import javax.inject.Inject
@@ -79,8 +80,7 @@ constructor(
getBoolean(R.bool.config_use_large_screen_shade_header),
marginHorizontal =
getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal),
- marginBottom =
- getDimensionPixelSize(R.dimen.notification_panel_margin_bottom),
+ marginBottom = NotificationStackScrollLayout.getBottomMargin(context),
marginTop = getDimensionPixelSize(R.dimen.notification_panel_margin_top),
marginTopLargeScreen =
largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight(),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index b6d89ecf29f2..d6e4add4eee3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -80,6 +80,7 @@ import com.android.systemui.shade.shared.model.ShadeMode.Dual
import com.android.systemui.shade.shared.model.ShadeMode.Single
import com.android.systemui.shade.shared.model.ShadeMode.Split
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
@@ -263,8 +264,7 @@ constructor(
horizontalPosition = horizontalPosition,
marginStart = if (shadeMode is Split) 0 else marginHorizontal,
marginEnd = marginHorizontal,
- marginBottom =
- getDimensionPixelSize(R.dimen.notification_panel_margin_bottom),
+ marginBottom = NotificationStackScrollLayout.getBottomMargin(context),
// y position of the NSSL in the window needs to be 0 under scene
// container
marginTop = 0,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 5a63c0cd84e6..bd1d7f755a74 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -30,6 +30,7 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.util.concurrency.DelayableExecutor
import dagger.Lazy
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
/** Handles start activity logic in SystemUI. */
@SysUISingleton
@@ -52,9 +53,10 @@ constructor(
override fun registerTransition(
cookie: ActivityTransitionAnimator.TransitionCookie,
controllerFactory: ActivityTransitionAnimator.ControllerFactory,
+ scope: CoroutineScope,
) {
if (!TransitionAnimator.longLivedReturnAnimationsEnabled()) return
- activityStarterInternal.registerTransition(cookie, controllerFactory)
+ activityStarterInternal.registerTransition(cookie, controllerFactory, scope)
}
override fun unregisterTransition(cookie: ActivityTransitionAnimator.TransitionCookie) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt
index 5e427fbf1f7e..015ec3052134 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternal.kt
@@ -25,15 +25,17 @@ import android.view.View
import com.android.systemui.ActivityIntentHelper
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.plugins.ActivityStarter
+import kotlinx.coroutines.CoroutineScope
interface ActivityStarterInternal {
/**
* Registers the given [controllerFactory] for launching and closing transitions matching the
- * [cookie] and the [ComponentName] that it contains.
+ * [cookie] and the [ComponentName] that it contains, within the given [scope].
*/
fun registerTransition(
cookie: ActivityTransitionAnimator.TransitionCookie,
controllerFactory: ActivityTransitionAnimator.ControllerFactory,
+ scope: CoroutineScope,
)
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
index 7289c2ed5897..6e82d7f7401a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterInternalImpl.kt
@@ -66,6 +66,7 @@ import com.android.systemui.util.kotlin.getOrNull
import dagger.Lazy
import java.util.Optional
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
/**
* Encapsulates the activity logic for activity starter when the SceneContainerFlag is enabled.
@@ -105,6 +106,7 @@ constructor(
override fun registerTransition(
cookie: ActivityTransitionAnimator.TransitionCookie,
controllerFactory: ActivityTransitionAnimator.ControllerFactory,
+ scope: CoroutineScope,
) {
check(TransitionAnimator.longLivedReturnAnimationsEnabled())
@@ -116,7 +118,7 @@ constructor(
controllerFactory.launchCujType,
controllerFactory.returnCujType,
) {
- override fun createController(
+ override suspend fun createController(
forLaunch: Boolean
): ActivityTransitionAnimator.Controller {
val baseController = controllerFactory.createController(forLaunch)
@@ -132,7 +134,7 @@ constructor(
}
}
- activityTransitionAnimator.register(cookie, factory)
+ activityTransitionAnimator.register(cookie, factory, scope)
}
override fun unregisterTransition(cookie: ActivityTransitionAnimator.TransitionCookie) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index 09531c3b18bd..01f2e9b8371d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -230,7 +230,11 @@ public final class DozeServiceHost implements DozeHost {
mDozingRequested = true;
updateDozing();
mDozeLog.traceDozing(mStatusBarStateController.isDozing());
- mCentralSurfaces.updateIsKeyguard();
+ // This is initialized in a CoreStartable, but binder calls from DreamManagerService can
+ // arrive earlier
+ if (mCentralSurfaces != null) {
+ mCentralSurfaces.updateIsKeyguard();
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
index d7a29c36f2ce..76f67dc6c146 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyActivityStarterInternalImpl.kt
@@ -64,6 +64,7 @@ import com.android.systemui.util.kotlin.getOrNull
import dagger.Lazy
import java.util.Optional
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
/** Encapsulates the activity logic for activity starter. */
@SysUISingleton
@@ -102,6 +103,7 @@ constructor(
override fun registerTransition(
cookie: ActivityTransitionAnimator.TransitionCookie,
controllerFactory: ActivityTransitionAnimator.ControllerFactory,
+ scope: CoroutineScope,
) {
check(TransitionAnimator.longLivedReturnAnimationsEnabled())
@@ -113,7 +115,7 @@ constructor(
controllerFactory.launchCujType,
controllerFactory.returnCujType,
) {
- override fun createController(
+ override suspend fun createController(
forLaunch: Boolean
): ActivityTransitionAnimator.Controller {
val baseController = controllerFactory.createController(forLaunch)
@@ -129,7 +131,7 @@ constructor(
}
}
- activityTransitionAnimator.register(cookie, factory)
+ activityTransitionAnimator.register(cookie, factory, scope)
}
override fun unregisterTransition(cookie: ActivityTransitionAnimator.TransitionCookie) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt
index 6258a55c374f..34ba767c227e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt
@@ -22,7 +22,7 @@ import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
import com.android.systemui.common.shared.model.Text
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon
import com.android.systemui.res.R
@@ -61,7 +61,7 @@ constructor(
mobileIconsInteractor: MobileIconsInteractor,
wifiInteractor: WifiInteractor,
private val context: Context,
- @Application scope: CoroutineScope,
+ @Background scope: CoroutineScope,
) {
private val internetLabel: String = context.getString(R.string.quick_settings_internet_label)
@@ -111,17 +111,16 @@ constructor(
if (it == null) {
notConnectedFlow
} else {
- combine(
- it.networkName,
- it.signalLevelIcon,
- mobileDataContentName,
- ) { networkNameModel, signalIcon, dataContentDescription ->
+ combine(it.networkName, it.signalLevelIcon, mobileDataContentName) {
+ networkNameModel,
+ signalIcon,
+ dataContentDescription ->
when (signalIcon) {
is SignalIconModel.Cellular -> {
val secondary =
mobileDataContentConcat(
networkNameModel.name,
- dataContentDescription
+ dataContentDescription,
)
InternetTileModel.Active(
secondaryTitle = secondary,
@@ -147,7 +146,7 @@ constructor(
private fun mobileDataContentConcat(
networkName: String?,
- dataContentDescription: CharSequence?
+ dataContentDescription: CharSequence?,
): CharSequence {
if (dataContentDescription == null) {
return networkName ?: ""
@@ -160,9 +159,9 @@ constructor(
context.getString(
R.string.mobile_carrier_text_format,
networkName,
- dataContentDescription
+ dataContentDescription,
),
- 0
+ 0,
)
}
@@ -191,10 +190,9 @@ constructor(
}
private val notConnectedFlow: StateFlow<InternetTileModel> =
- combine(
- wifiInteractor.areNetworksAvailable,
- airplaneModeRepository.isAirplaneMode,
- ) { networksAvailable, isAirplaneMode ->
+ combine(wifiInteractor.areNetworksAvailable, airplaneModeRepository.isAirplaneMode) {
+ networksAvailable,
+ isAirplaneMode ->
when {
isAirplaneMode -> {
val secondary = context.getString(R.string.status_bar_airplane)
@@ -213,7 +211,7 @@ constructor(
iconId = R.drawable.ic_qs_no_internet_available,
stateDescription = null,
contentDescription =
- ContentDescription.Loaded("$internetLabel,$secondary")
+ ContentDescription.Loaded("$internetLabel,$secondary"),
)
}
else -> {
diff --git a/packages/SystemUI/tests/goldens/brightnessSlider_iconAlphaChanges.json b/packages/SystemUI/tests/goldens/brightnessSlider_iconAlphaChanges.json
new file mode 100644
index 000000000000..cefada71686c
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/brightnessSlider_iconAlphaChanges.json
@@ -0,0 +1,168 @@
+{
+ "frame_ids": [
+ "before",
+ 0,
+ 16,
+ 32,
+ 48,
+ 64,
+ 80,
+ 96,
+ 112,
+ 128,
+ 144,
+ 160,
+ 176,
+ 192,
+ 208,
+ 224,
+ 240,
+ 256,
+ 272,
+ 288,
+ 304,
+ 320,
+ 336,
+ 352,
+ 368,
+ 384,
+ 400,
+ 416,
+ 432,
+ 448,
+ 464,
+ 480,
+ 496,
+ 512,
+ 528,
+ 544,
+ 560,
+ 576,
+ 592,
+ 608,
+ 624,
+ 640,
+ 656,
+ 672,
+ 688,
+ 704,
+ 720,
+ 736,
+ 752,
+ "after"
+ ],
+ "features": [
+ {
+ "name": "activeIconAlpha_activeIconAlpha",
+ "type": "float",
+ "data_points": [
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 0.5782508,
+ 0.09543866,
+ 8.595586E-4,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0
+ ]
+ },
+ {
+ "name": "inactiveIconAlpha_inactiveIconAlpha",
+ "type": "float",
+ "data_points": [
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0.065971695,
+ 0.3946195,
+ 0.7348632,
+ 0.8979182,
+ 0.97246534,
+ 0.9986459,
+ 1
+ ]
+ }
+ ]
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index bac2c47f51c7..f822ee97807e 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -95,7 +95,6 @@ import org.mockito.kotlin.clearInvocations
@RunWith(AndroidJUnit4::class)
@SmallTest
-@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
class ClockEventControllerTest : SysuiTestCase() {
private val kosmos = testKosmos()
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 312d2ffd74e4..4110a05170b3 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -18,7 +18,6 @@ package com.android.keyguard;
import static android.app.StatusBarManager.SESSION_KEYGUARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.hardware.biometrics.BiometricAuthenticator.TYPE_ANY_BIOMETRIC;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT;
@@ -2717,7 +2716,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase {
() -> mAlternateBouncerInteractor,
() -> mJavaAdapter,
() -> mSceneInteractor,
- mCommunalSceneInteractor);
+ () -> mCommunalSceneInteractor);
setAlternateBouncerVisibility(false);
setPrimaryBouncerVisibility(false);
setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index ad5f96044c4c..a0f5b2214f80 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -242,8 +242,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
targetCenterX, targetCenterY, mAnimationCallback2);
mCurrentScale.set(mController.getScale());
- mCurrentCenterX.set(mController.getCenterX());
- mCurrentCenterY.set(mController.getCenterY());
+ mCurrentCenterX.set(mController.getMagnificationFrameCenterX());
+ mCurrentCenterY.set(mController.getMagnificationFrameCenterY());
advanceTimeBy(mWaitAnimationDuration);
});
@@ -297,8 +297,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
targetCenterX, targetCenterY, mAnimationCallback);
mCurrentScale.set(mController.getScale());
- mCurrentCenterX.set(mController.getCenterX());
- mCurrentCenterY.set(mController.getCenterY());
+ mCurrentCenterX.set(mController.getMagnificationFrameCenterX());
+ mCurrentCenterY.set(mController.getMagnificationFrameCenterY());
advanceTimeBy(mWaitAnimationDuration);
});
@@ -339,8 +339,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
targetCenterX, targetCenterY, mAnimationCallback);
mCurrentScale.set(mController.getScale());
- mCurrentCenterX.set(mController.getCenterX());
- mCurrentCenterY.set(mController.getCenterY());
+ mCurrentCenterX.set(mController.getMagnificationFrameCenterX());
+ mCurrentCenterY.set(mController.getMagnificationFrameCenterY());
advanceTimeBy(mWaitAnimationDuration);
});
@@ -375,8 +375,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
targetCenterX, targetCenterY, mAnimationCallback);
mCurrentScale.set(mController.getScale());
- mCurrentCenterX.set(mController.getCenterX());
- mCurrentCenterY.set(mController.getCenterY());
+ mCurrentCenterX.set(mController.getMagnificationFrameCenterX());
+ mCurrentCenterY.set(mController.getMagnificationFrameCenterY());
advanceTimeBy(mWaitAnimationDuration);
});
@@ -463,8 +463,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
targetCenterX, targetCenterY, mAnimationCallback2);
mCurrentScale.set(mController.getScale());
- mCurrentCenterX.set(mController.getCenterX());
- mCurrentCenterY.set(mController.getCenterY());
+ mCurrentCenterX.set(mController.getMagnificationFrameCenterX());
+ mCurrentCenterY.set(mController.getMagnificationFrameCenterY());
});
// Current spec shouldn't match given spec.
@@ -548,8 +548,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
mWindowMagnificationAnimationController.enableWindowMagnification(targetScale,
targetCenterX, targetCenterY, mAnimationCallback2);
mCurrentScale.set(mController.getScale());
- mCurrentCenterX.set(mController.getCenterX());
- mCurrentCenterY.set(mController.getCenterY());
+ mCurrentCenterX.set(mController.getMagnificationFrameCenterX());
+ mCurrentCenterY.set(mController.getMagnificationFrameCenterY());
advanceTimeBy(mWaitAnimationDuration);
});
@@ -777,8 +777,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
mWindowMagnificationAnimationController.deleteWindowMagnification(
mAnimationCallback2);
mCurrentScale.set(mController.getScale());
- mCurrentCenterX.set(mController.getCenterX());
- mCurrentCenterY.set(mController.getCenterY());
+ mCurrentCenterX.set(mController.getMagnificationFrameCenterX());
+ mCurrentCenterY.set(mController.getMagnificationFrameCenterY());
// ValueAnimator.reverse() could not work correctly with the AnimatorTestRule since it
// is using SystemClock in reverse() (b/305731398). Therefore, we call end() on the
// animator directly to verify the result of animation is correct instead of querying
@@ -940,8 +940,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
private void verifyFinalSpec(float expectedScale, float expectedCenterX,
float expectedCenterY) {
assertEquals(expectedScale, mController.getScale(), 0f);
- assertEquals(expectedCenterX, mController.getCenterX(), 0f);
- assertEquals(expectedCenterY, mController.getCenterY(), 0f);
+ assertEquals(expectedCenterX, mController.getMagnificationFrameCenterX(), 0f);
+ assertEquals(expectedCenterY, mController.getMagnificationFrameCenterY(), 0f);
}
private void enableWindowMagnificationWithoutAnimation() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index 8552e48a2024..7cf93277bb5b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -63,6 +63,8 @@ import android.graphics.Rect;
import android.os.Handler;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.provider.Settings;
import android.testing.TestableLooper;
import android.testing.TestableResources;
@@ -88,6 +90,7 @@ import androidx.test.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
+import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.AnimatorTestRule;
import com.android.systemui.kosmos.KosmosJavaAdapter;
@@ -128,6 +131,7 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(/* test= */ null);
private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000;
+ private static final int INSET_BOTTOM = 10;
@Mock
private MirrorWindowControl mMirrorWindowControl;
@Mock
@@ -329,9 +333,9 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class);
verify(mWindowMagnifierCallback, atLeast(2)).onSourceBoundsChanged(
(eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
- assertThat(mWindowMagnificationController.getCenterX())
+ assertThat(mWindowMagnificationController.getMagnificationFrameCenterX())
.isEqualTo(sourceBoundsCaptor.getValue().exactCenterX());
- assertThat(mWindowMagnificationController.getCenterY())
+ assertThat(mWindowMagnificationController.getMagnificationFrameCenterY())
.isEqualTo(sourceBoundsCaptor.getValue().exactCenterY());
}
@@ -382,6 +386,7 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
}
@Test
+ @DisableFlags(Flags.FLAG_UPDATE_WINDOW_MAGNIFIER_BOTTOM_BOUNDARY)
public void deleteWindowMagnification_enableAtTheBottom_overlapFlagIsFalse() {
final WindowManager wm = mContext.getSystemService(WindowManager.class);
final Rect bounds = wm.getCurrentWindowMetrics().getBounds();
@@ -457,12 +462,14 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
verify(mAnimationCallback, never()).onResult(eq(false));
verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
.onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
- assertThat(mWindowMagnificationController.getCenterX())
+ assertThat(mWindowMagnificationController.getMagnificationFrameCenterX())
.isEqualTo(sourceBoundsCaptor.getValue().exactCenterX());
- assertThat(mWindowMagnificationController.getCenterY())
+ assertThat(mWindowMagnificationController.getMagnificationFrameCenterY())
.isEqualTo(sourceBoundsCaptor.getValue().exactCenterY());
- assertThat(mWindowMagnificationController.getCenterX()).isEqualTo(targetCenterX);
- assertThat(mWindowMagnificationController.getCenterY()).isEqualTo(targetCenterY);
+ assertThat(mWindowMagnificationController.getMagnificationFrameCenterX())
+ .isEqualTo(targetCenterX);
+ assertThat(mWindowMagnificationController.getMagnificationFrameCenterY())
+ .isEqualTo(targetCenterY);
}
@Test
@@ -498,12 +505,14 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
verify(mAnimationCallback, times(3)).onResult(eq(false));
verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS))
.onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture());
- assertThat(mWindowMagnificationController.getCenterX())
+ assertThat(mWindowMagnificationController.getMagnificationFrameCenterX())
.isEqualTo(sourceBoundsCaptor.getValue().exactCenterX());
- assertThat(mWindowMagnificationController.getCenterY())
+ assertThat(mWindowMagnificationController.getMagnificationFrameCenterY())
.isEqualTo(sourceBoundsCaptor.getValue().exactCenterY());
- assertThat(mWindowMagnificationController.getCenterX()).isEqualTo(centerX + 40);
- assertThat(mWindowMagnificationController.getCenterY()).isEqualTo(centerY + 40);
+ assertThat(mWindowMagnificationController.getMagnificationFrameCenterX())
+ .isEqualTo(centerX + 40);
+ assertThat(mWindowMagnificationController.getMagnificationFrameCenterY())
+ .isEqualTo(centerY + 40);
}
@Test
@@ -545,8 +554,8 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
mWindowMagnificationController.updateWindowMagnificationInternal(Float.NaN,
magnifiedCenter.x, magnifiedCenter.y);
// Get the center again in case the center we set is out of screen.
- magnifiedCenter.set(mWindowMagnificationController.getCenterX(),
- mWindowMagnificationController.getCenterY());
+ magnifiedCenter.set(mWindowMagnificationController.getMagnificationFrameCenterX(),
+ mWindowMagnificationController.getMagnificationFrameCenterY());
});
// Rotate the window clockwise 90 degree.
windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom,
@@ -559,8 +568,9 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
assertThat(mWindowMagnificationController.mRotation).isEqualTo(newRotation);
final PointF expectedCenter = new PointF(magnifiedCenter.y,
displayWidth - magnifiedCenter.x);
- final PointF actualCenter = new PointF(mWindowMagnificationController.getCenterX(),
- mWindowMagnificationController.getCenterY());
+ final PointF actualCenter =
+ new PointF(mWindowMagnificationController.getMagnificationFrameCenterX(),
+ mWindowMagnificationController.getMagnificationFrameCenterY());
assertThat(actualCenter).isEqualTo(expectedCenter);
}
@@ -603,10 +613,10 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
});
// The ratio of center to window size should be the same.
- assertThat(mWindowMagnificationController.getCenterX() / testWindowBounds.width())
- .isEqualTo(expectedRatio);
- assertThat(mWindowMagnificationController.getCenterY() / testWindowBounds.height())
- .isEqualTo(expectedRatio);
+ assertThat(mWindowMagnificationController.getMagnificationFrameCenterX()
+ / testWindowBounds.width()).isEqualTo(expectedRatio);
+ assertThat(mWindowMagnificationController.getMagnificationFrameCenterY()
+ / testWindowBounds.height()).isEqualTo(expectedRatio);
}
@Test
@@ -1175,6 +1185,7 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
}
@Test
+ @DisableFlags(Flags.FLAG_UPDATE_WINDOW_MAGNIFIER_BOTTOM_BOUNDARY)
public void moveWindowMagnificationToTheBottom_enabledWithGestureInset_overlapFlagIsTrue() {
final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
setSystemGestureInsets();
@@ -1191,6 +1202,30 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
}
@Test
+ @EnableFlags(Flags.FLAG_UPDATE_WINDOW_MAGNIFIER_BOTTOM_BOUNDARY)
+ public void moveWindowMagnificationToTheBottom_stopsAtSystemGestureTop() {
+ final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+ setSystemGestureInsets();
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.updateWindowMagnificationInternal(Float.NaN, Float.NaN,
+ Float.NaN);
+ });
+
+ ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams();
+ final int mOuterBorderSize = mResources.getDimensionPixelSize(
+ R.dimen.magnification_outer_border_margin);
+
+ final float expectedY =
+ (float) (bounds.bottom - INSET_BOTTOM - params.height + mOuterBorderSize);
+
+ mInstrumentation.runOnMainSync(() -> {
+ mWindowMagnificationController.moveWindowMagnifier(0, bounds.height());
+ });
+
+ assertThat(mWindowMagnificationController.getMagnifierWindowY()).isEqualTo(expectedY);
+ }
+
+ @Test
public void moveWindowMagnificationToRightEdge_dragHandleMovesToLeftAndUpdatesTapExcludeRegion()
throws RemoteException {
final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds();
@@ -1445,8 +1480,10 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
mInstrumentation.runOnMainSync(() -> {
mWindowMagnificationController.setWindowSizeAndCenter(minimumWindowSize,
minimumWindowSize, bounds.right, bounds.bottom);
- magnificationCenterX.set((int) mWindowMagnificationController.getCenterX());
- magnificationCenterY.set((int) mWindowMagnificationController.getCenterY());
+ magnificationCenterX.set(
+ (int) mWindowMagnificationController.getMagnificationFrameCenterX());
+ magnificationCenterY.set(
+ (int) mWindowMagnificationController.getMagnificationFrameCenterY());
});
assertThat(magnificationCenterX.get()).isLessThan(bounds.right);
@@ -1501,7 +1538,7 @@ public class WindowMagnificationControllerTest extends SysuiTestCase {
private void setSystemGestureInsets() {
final WindowInsets testInsets = new WindowInsets.Builder()
- .setInsets(systemGestures(), Insets.of(0, 0, 0, 10))
+ .setInsets(systemGestures(), Insets.of(0, 0, 0, INSET_BOTTOM))
.build();
mWindowManager.setWindowInsets(testInsets);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplForDeviceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplForDeviceTest.kt
index 4e0ebae6a902..c6ede0c8c602 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplForDeviceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplForDeviceTest.kt
@@ -36,7 +36,6 @@ import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.utils.FieldSetter
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
@@ -55,7 +54,6 @@ import org.mockito.junit.MockitoRule
* can't mock the AccessibilityShortcutInfo for test. MultiValentTest doesn't compile when using
* newly introduced methods and constants.
*/
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class AccessibilityQsShortcutsRepositoryImplForDeviceTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
index fd751d9cc7c3..845be0252581 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ActivityTransitionAnimatorTest.kt
@@ -27,7 +27,10 @@ import android.window.WindowAnimationState
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
import com.android.systemui.shared.Flags
+import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.wm.shell.shared.ShellTransitions
import com.google.common.truth.Truth.assertThat
@@ -38,6 +41,9 @@ import junit.framework.Assert.assertTrue
import junit.framework.AssertionFailedError
import kotlin.concurrent.thread
import kotlin.test.assertEquals
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertThrows
import org.junit.Before
@@ -54,10 +60,12 @@ import org.mockito.Mockito.`when`
import org.mockito.Spy
import org.mockito.junit.MockitoJUnit
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper
class ActivityTransitionAnimatorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
private val transitionContainer = LinearLayout(mContext)
private val mainExecutor = context.mainExecutor
private val testTransitionAnimator = fakeTransitionAnimator(mainExecutor)
@@ -67,12 +75,12 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
@Spy private val controller = TestTransitionAnimatorController(transitionContainer)
@Mock lateinit var iCallback: IRemoteAnimationFinishedCallback
- private lateinit var activityTransitionAnimator: ActivityTransitionAnimator
+ private lateinit var underTest: ActivityTransitionAnimator
@get:Rule val rule = MockitoJUnit.rule()
@Before
fun setup() {
- activityTransitionAnimator =
+ underTest =
ActivityTransitionAnimator(
mainExecutor,
ActivityTransitionAnimator.TransitionRegister.fromShellTransitions(
@@ -82,17 +90,17 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
testTransitionAnimator,
disableWmTimeout = true,
)
- activityTransitionAnimator.callback = callback
- activityTransitionAnimator.addListener(listener)
+ underTest.callback = callback
+ underTest.addListener(listener)
}
@After
fun tearDown() {
- activityTransitionAnimator.removeListener(listener)
+ underTest.removeListener(listener)
}
private fun startIntentWithAnimation(
- animator: ActivityTransitionAnimator = this.activityTransitionAnimator,
+ animator: ActivityTransitionAnimator = underTest,
controller: ActivityTransitionAnimator.Controller? = this.controller,
animate: Boolean = true,
intentStarter: (RemoteAnimationAdapter?) -> Int,
@@ -157,7 +165,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
val willAnimateCaptor = ArgumentCaptor.forClass(Boolean::class.java)
var animationAdapter: RemoteAnimationAdapter? = null
- startIntentWithAnimation(activityTransitionAnimator) { adapter ->
+ startIntentWithAnimation(underTest) { adapter ->
animationAdapter = adapter
ActivityManager.START_DELIVERED_TO_TOP
}
@@ -185,9 +193,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
fun registersReturnIffCookieIsPresent() {
`when`(callback.isOnKeyguard()).thenReturn(false)
- startIntentWithAnimation(activityTransitionAnimator, controller) { _ ->
- ActivityManager.START_DELIVERED_TO_TOP
- }
+ startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP }
waitForIdleSync()
assertTrue(testShellTransitions.remotes.isEmpty())
@@ -199,9 +205,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
get() = ActivityTransitionAnimator.TransitionCookie("testCookie")
}
- startIntentWithAnimation(activityTransitionAnimator, controller) { _ ->
- ActivityManager.START_DELIVERED_TO_TOP
- }
+ startIntentWithAnimation(underTest, controller) { ActivityManager.START_DELIVERED_TO_TOP }
waitForIdleSync()
assertEquals(1, testShellTransitions.remotes.size)
@@ -214,13 +218,15 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
)
@Test
fun registersLongLivedTransition() {
- var factory = controllerFactory()
- activityTransitionAnimator.register(factory.cookie, factory)
- assertEquals(2, testShellTransitions.remotes.size)
-
- factory = controllerFactory()
- activityTransitionAnimator.register(factory.cookie, factory)
- assertEquals(4, testShellTransitions.remotes.size)
+ kosmos.runTest {
+ var factory = controllerFactory()
+ underTest.register(factory.cookie, factory, testScope)
+ assertEquals(2, testShellTransitions.remotes.size)
+
+ factory = controllerFactory()
+ underTest.register(factory.cookie, factory, testScope)
+ assertEquals(4, testShellTransitions.remotes.size)
+ }
}
@EnableFlags(
@@ -229,49 +235,55 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
)
@Test
fun registersLongLivedTransitionOverridingPreviousRegistration() {
- val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie")
- var factory = controllerFactory(cookie)
- activityTransitionAnimator.register(cookie, factory)
- val transitions = testShellTransitions.remotes.values.toList()
-
- factory = controllerFactory(cookie)
- activityTransitionAnimator.register(cookie, factory)
- assertEquals(2, testShellTransitions.remotes.size)
- for (transition in transitions) {
- assertThat(testShellTransitions.remotes.values).doesNotContain(transition)
+ kosmos.runTest {
+ val cookie = ActivityTransitionAnimator.TransitionCookie("test_cookie")
+ var factory = controllerFactory(cookie)
+ underTest.register(cookie, factory, testScope)
+ val transitions = testShellTransitions.remotes.values.toList()
+
+ factory = controllerFactory(cookie)
+ underTest.register(cookie, factory, testScope)
+ assertEquals(2, testShellTransitions.remotes.size)
+ for (transition in transitions) {
+ assertThat(testShellTransitions.remotes.values).doesNotContain(transition)
+ }
}
}
@DisableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
@Test
fun doesNotRegisterLongLivedTransitionIfFlagIsDisabled() {
- val factory = controllerFactory(component = null)
- assertThrows(IllegalStateException::class.java) {
- activityTransitionAnimator.register(factory.cookie, factory)
+ kosmos.runTest {
+ val factory = controllerFactory(component = null)
+ assertThrows(IllegalStateException::class.java) {
+ underTest.register(factory.cookie, factory, testScope)
+ }
}
}
@EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED)
@Test
fun doesNotRegisterLongLivedTransitionIfMissingRequiredProperties() {
- // No ComponentName
- var factory = controllerFactory(component = null)
- assertThrows(IllegalStateException::class.java) {
- activityTransitionAnimator.register(factory.cookie, factory)
- }
+ kosmos.runTest {
+ // No ComponentName
+ var factory = controllerFactory(component = null)
+ assertThrows(IllegalStateException::class.java) {
+ underTest.register(factory.cookie, factory, testScope)
+ }
- // No TransitionRegister
- activityTransitionAnimator =
- ActivityTransitionAnimator(
- mainExecutor,
- transitionRegister = null,
- testTransitionAnimator,
- testTransitionAnimator,
- disableWmTimeout = true,
- )
- factory = controllerFactory()
- assertThrows(IllegalStateException::class.java) {
- activityTransitionAnimator.register(factory.cookie, factory)
+ // No TransitionRegister
+ val activityTransitionAnimator =
+ ActivityTransitionAnimator(
+ mainExecutor,
+ transitionRegister = null,
+ testTransitionAnimator,
+ testTransitionAnimator,
+ disableWmTimeout = true,
+ )
+ factory = controllerFactory()
+ assertThrows(IllegalStateException::class.java) {
+ activityTransitionAnimator.register(factory.cookie, factory, testScope)
+ }
}
}
@@ -281,27 +293,29 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
)
@Test
fun unregistersLongLivedTransition() {
- val cookies = arrayOfNulls<ActivityTransitionAnimator.TransitionCookie>(3)
+ kosmos.runTest {
+ val cookies = arrayOfNulls<ActivityTransitionAnimator.TransitionCookie>(3)
- for (index in 0 until 3) {
- cookies[index] = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
- val factory = controllerFactory(cookies[index]!!)
- activityTransitionAnimator.register(factory.cookie, factory)
- }
+ for (index in 0 until 3) {
+ cookies[index] = mock(ActivityTransitionAnimator.TransitionCookie::class.java)
+ val factory = controllerFactory(cookies[index]!!)
+ underTest.register(factory.cookie, factory, testScope)
+ }
- activityTransitionAnimator.unregister(cookies[0]!!)
- assertEquals(4, testShellTransitions.remotes.size)
+ underTest.unregister(cookies[0]!!)
+ assertEquals(4, testShellTransitions.remotes.size)
- activityTransitionAnimator.unregister(cookies[2]!!)
- assertEquals(2, testShellTransitions.remotes.size)
+ underTest.unregister(cookies[2]!!)
+ assertEquals(2, testShellTransitions.remotes.size)
- activityTransitionAnimator.unregister(cookies[1]!!)
- assertThat(testShellTransitions.remotes).isEmpty()
+ underTest.unregister(cookies[1]!!)
+ assertThat(testShellTransitions.remotes).isEmpty()
+ }
}
@Test
fun doesNotStartIfAnimationIsCancelled() {
- val runner = activityTransitionAnimator.createEphemeralRunner(controller)
+ val runner = underTest.createEphemeralRunner(controller)
runner.onAnimationCancelled()
runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback)
@@ -315,7 +329,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
@Test
fun cancelsIfNoOpeningWindowIsFound() {
- val runner = activityTransitionAnimator.createEphemeralRunner(controller)
+ val runner = underTest.createEphemeralRunner(controller)
runner.onAnimationStart(TRANSIT_NONE, emptyArray(), emptyArray(), emptyArray(), iCallback)
waitForIdleSync()
@@ -328,7 +342,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
@Test
fun startsAnimationIfWindowIsOpening() {
- val runner = activityTransitionAnimator.createEphemeralRunner(controller)
+ val runner = underTest.createEphemeralRunner(controller)
runner.onAnimationStart(
TRANSIT_NONE,
arrayOf(fakeWindow()),
@@ -354,9 +368,11 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
)
@Test
fun creatingRunnerWithLazyInitializationThrows_whenTheFlagsAreDisabled() {
- assertThrows(IllegalStateException::class.java) {
- val factory = controllerFactory()
- activityTransitionAnimator.createLongLivedRunner(factory, forLaunch = true)
+ kosmos.runTest {
+ assertThrows(IllegalStateException::class.java) {
+ val factory = controllerFactory()
+ underTest.createLongLivedRunner(factory, testScope, forLaunch = true)
+ }
}
}
@@ -365,44 +381,34 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
)
@Test
- fun runnerCreatesDelegateLazily_whenPostingTimeouts() {
- val factory = controllerFactory()
- val runner = activityTransitionAnimator.createLongLivedRunner(factory, forLaunch = true)
- assertNull(runner.delegate)
- runner.postTimeouts()
- assertNotNull(runner.delegate)
- }
-
- @EnableFlags(
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY,
- Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED,
- )
- @Test
fun runnerCreatesDelegateLazily_onAnimationStart() {
- val factory = controllerFactory()
- val runner = activityTransitionAnimator.createLongLivedRunner(factory, forLaunch = true)
- assertNull(runner.delegate)
-
- var delegateInitialized = false
- activityTransitionAnimator.addListener(
- object : ActivityTransitionAnimator.Listener {
- override fun onTransitionAnimationStart() {
- // This is called iff the delegate was initialized, so it's a good proxy for
- // checking the initialization.
- delegateInitialized = true
+ kosmos.runTest {
+ val factory = controllerFactory()
+ val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = true)
+ assertNull(runner.delegate)
+
+ var delegateInitialized = false
+ underTest.addListener(
+ object : ActivityTransitionAnimator.Listener {
+ override fun onTransitionAnimationStart() {
+ // This is called iff the delegate was initialized, so it's a good proxy for
+ // checking the initialization.
+ delegateInitialized = true
+ }
}
- }
- )
- runner.onAnimationStart(
- TRANSIT_NONE,
- arrayOf(fakeWindow()),
- emptyArray(),
- emptyArray(),
- iCallback,
- )
+ )
+ runner.onAnimationStart(
+ TRANSIT_NONE,
+ arrayOf(fakeWindow()),
+ emptyArray(),
+ emptyArray(),
+ iCallback,
+ )
+ testScope.advanceUntilIdle()
+ waitForIdleSync()
- waitForIdleSync()
- assertTrue(delegateInitialized)
+ assertTrue(delegateInitialized)
+ }
}
@EnableFlags(
@@ -411,29 +417,32 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
)
@Test
fun runnerCreatesDelegateLazily_onAnimationTakeover() {
- val factory = controllerFactory()
- val runner = activityTransitionAnimator.createLongLivedRunner(factory, forLaunch = false)
- assertNull(runner.delegate)
-
- var delegateInitialized = false
- activityTransitionAnimator.addListener(
- object : ActivityTransitionAnimator.Listener {
- override fun onTransitionAnimationStart() {
- // This is called iff the delegate was initialized, so it's a good proxy for
- // checking the initialization.
- delegateInitialized = true
+ kosmos.runTest {
+ val factory = controllerFactory()
+ val runner = underTest.createLongLivedRunner(factory, testScope, forLaunch = false)
+ assertNull(runner.delegate)
+
+ var delegateInitialized = false
+ underTest.addListener(
+ object : ActivityTransitionAnimator.Listener {
+ override fun onTransitionAnimationStart() {
+ // This is called iff the delegate was initialized, so it's a good proxy for
+ // checking the initialization.
+ delegateInitialized = true
+ }
}
- }
- )
- runner.takeOverAnimation(
- arrayOf(fakeWindow(MODE_CLOSING)),
- arrayOf(WindowAnimationState()),
- SurfaceControl.Transaction(),
- iCallback,
- )
+ )
+ runner.takeOverAnimation(
+ arrayOf(fakeWindow(MODE_CLOSING)),
+ arrayOf(WindowAnimationState()),
+ SurfaceControl.Transaction(),
+ iCallback,
+ )
+ testScope.advanceUntilIdle()
+ waitForIdleSync()
- waitForIdleSync()
- assertTrue(delegateInitialized)
+ assertTrue(delegateInitialized)
+ }
}
@DisableFlags(
@@ -442,7 +451,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
)
@Test
fun animationTakeoverThrows_whenTheFlagsAreDisabled() {
- val runner = activityTransitionAnimator.createEphemeralRunner(controller)
+ val runner = underTest.createEphemeralRunner(controller)
assertThrows(IllegalStateException::class.java) {
runner.takeOverAnimation(
arrayOf(fakeWindow()),
@@ -459,7 +468,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
)
@Test
fun disposeRunner_delegateDereferenced() {
- val runner = activityTransitionAnimator.createEphemeralRunner(controller)
+ val runner = underTest.createEphemeralRunner(controller)
assertNotNull(runner.delegate)
runner.dispose()
waitForIdleSync()
@@ -469,13 +478,13 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
@Test
fun concurrentListenerModification_doesNotThrow() {
// Need a second listener to trigger the concurrent modification.
- activityTransitionAnimator.addListener(object : ActivityTransitionAnimator.Listener {})
+ underTest.addListener(object : ActivityTransitionAnimator.Listener {})
`when`(listener.onTransitionAnimationStart()).thenAnswer {
- activityTransitionAnimator.removeListener(listener)
+ underTest.removeListener(listener)
listener
}
- val runner = activityTransitionAnimator.createEphemeralRunner(controller)
+ val runner = underTest.createEphemeralRunner(controller)
runner.onAnimationStart(
TRANSIT_NONE,
arrayOf(fakeWindow()),
@@ -494,7 +503,7 @@ class ActivityTransitionAnimatorTest : SysuiTestCase() {
component: ComponentName? = mock(ComponentName::class.java),
): ActivityTransitionAnimator.ControllerFactory {
return object : ActivityTransitionAnimator.ControllerFactory(cookie, component) {
- override fun createController(forLaunch: Boolean) =
+ override suspend fun createController(forLaunch: Boolean) =
object : DelegateTransitionAnimatorController(controller) {
override val isLaunching: Boolean
get() = forLaunch
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index 4baca713e19f..5249620dbdd0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -63,7 +63,6 @@ import com.android.systemui.testKosmos
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.google.common.truth.Truth.assertThat
import dagger.Lazy
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -89,7 +88,6 @@ private const val DISPLAY_HEIGHT = 1920
private const val SENSOR_WIDTH = 30
private const val SENSOR_HEIGHT = 60
-@ExperimentalCoroutinesApi
@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper(setAsMainLooper = true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt
index 655b2cc2dece..f3aaa3d3c7aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt
@@ -32,7 +32,6 @@ import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
@@ -43,7 +42,6 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
-@ExperimentalCoroutinesApi
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
index e61acc4e1d0b..9ae57153f3ef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
@@ -33,7 +33,6 @@ import com.android.systemui.plugins.activityStarter
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.After
@@ -56,7 +55,6 @@ import org.mockito.kotlin.whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
-@OptIn(ExperimentalCoroutinesApi::class)
class AudioSharingDeviceItemActionInteractorTest : SysuiTestCase() {
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt
new file mode 100644
index 000000000000..6ed990d513cb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt
@@ -0,0 +1,461 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.graphics.drawable.Drawable
+import android.testing.TestableLooper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.model.SysUiState
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.SystemUIDialogManager
+import com.android.systemui.testKosmos
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class BluetoothDetailsContentManagerTest : SysuiTestCase() {
+ companion object {
+ const val DEVICE_NAME = "device"
+ const val DEVICE_CONNECTION_SUMMARY = "active"
+ const val ENABLED = true
+ const val CONTENT_HEIGHT = WRAP_CONTENT
+ }
+
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ private val cachedBluetoothDevice = mock<CachedBluetoothDevice>()
+
+ private val bluetoothTileDialogCallback = mock<BluetoothTileDialogCallback>()
+
+ private val drawable = mock<Drawable>()
+
+ private val uiEventLogger = mock<UiEventLogger>()
+
+ private val logger = mock<BluetoothTileDialogLogger>()
+
+ private val sysuiDialogFactory = mock<SystemUIDialog.Factory>()
+ private val dialogManager = mock<SystemUIDialogManager>()
+ private val sysuiState = mock<SysUiState>()
+ private val dialogTransitionAnimator = mock<DialogTransitionAnimator>()
+
+ private val fakeSystemClock = FakeSystemClock()
+
+ private val uiProperties =
+ BluetoothTileDialogViewModel.UiProperties.build(
+ isBluetoothEnabled = ENABLED,
+ isAutoOnToggleFeatureAvailable = ENABLED,
+ )
+
+ private lateinit var icon: Pair<Drawable, String>
+ private lateinit var mBluetoothDetailsContentManager: BluetoothDetailsContentManager
+ private lateinit var deviceItem: DeviceItem
+ private lateinit var contentView: View
+
+ private val kosmos = testKosmos()
+
+ @Before
+ fun setUp() {
+ with(kosmos) {
+ contentView =
+ LayoutInflater.from(mContext).inflate(R.layout.bluetooth_tile_dialog, null)
+
+ whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState)
+
+ mBluetoothDetailsContentManager =
+ BluetoothDetailsContentManager(
+ uiProperties,
+ CONTENT_HEIGHT,
+ bluetoothTileDialogCallback,
+ /* isInDialog= */ true,
+ {},
+ testDispatcher,
+ fakeSystemClock,
+ uiEventLogger,
+ logger,
+ )
+
+ whenever(sysuiDialogFactory.create(any<SystemUIDialog.Delegate>(), any())).thenAnswer {
+ SystemUIDialog(
+ mContext,
+ 0,
+ SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
+ dialogManager,
+ sysuiState,
+ fakeBroadcastDispatcher,
+ dialogTransitionAnimator,
+ it.getArgument(0),
+ )
+ }
+
+ icon = Pair(drawable, DEVICE_NAME)
+ deviceItem =
+ DeviceItem(
+ type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+ cachedBluetoothDevice = cachedBluetoothDevice,
+ deviceName = DEVICE_NAME,
+ connectionSummary = DEVICE_CONNECTION_SUMMARY,
+ iconWithDescription = icon,
+ background = null,
+ )
+ whenever(cachedBluetoothDevice.isBusy).thenReturn(false)
+ }
+ }
+
+ @Test
+ fun testShowDialog_createRecyclerViewWithAdapter() {
+ mBluetoothDetailsContentManager.bind(contentView)
+ mBluetoothDetailsContentManager.start()
+
+ val recyclerView = contentView.requireViewById<RecyclerView>(R.id.device_list)
+
+ assertThat(recyclerView).isNotNull()
+ assertThat(recyclerView.visibility).isEqualTo(VISIBLE)
+ assertThat(recyclerView.adapter).isNotNull()
+ assertThat(recyclerView.layoutManager is LinearLayoutManager).isTrue()
+ mBluetoothDetailsContentManager.releaseView()
+ }
+
+ @Test
+ fun testShowDialog_displayBluetoothDevice() {
+ with(kosmos) {
+ testScope.runTest {
+ mBluetoothDetailsContentManager.bind(contentView)
+ mBluetoothDetailsContentManager.start()
+ fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
+ mBluetoothDetailsContentManager.onDeviceItemUpdated(
+ listOf(deviceItem),
+ showSeeAll = false,
+ showPairNewDevice = false,
+ )
+
+ val recyclerView = contentView.requireViewById<RecyclerView>(R.id.device_list)
+ val adapter = recyclerView?.adapter as BluetoothDetailsContentManager.Adapter
+ assertThat(adapter.itemCount).isEqualTo(1)
+ assertThat(adapter.getItem(0).deviceName).isEqualTo(DEVICE_NAME)
+ assertThat(adapter.getItem(0).connectionSummary)
+ .isEqualTo(DEVICE_CONNECTION_SUMMARY)
+ assertThat(adapter.getItem(0).iconWithDescription).isEqualTo(icon)
+ mBluetoothDetailsContentManager.releaseView()
+ }
+ }
+ }
+
+ @Test
+ fun testDeviceItemViewHolder_cachedDeviceNotBusy() {
+ with(kosmos) {
+ testScope.runTest {
+ deviceItem.isEnabled = true
+
+ val view =
+ LayoutInflater.from(mContext)
+ .inflate(R.layout.bluetooth_device_item, null, false)
+ val viewHolder =
+ mBluetoothDetailsContentManager.Adapter().DeviceItemViewHolder(view)
+ viewHolder.bind(deviceItem)
+ val container = view.requireViewById<View>(R.id.bluetooth_device_row)
+
+ assertThat(container).isNotNull()
+ assertThat(container.isEnabled).isTrue()
+ assertThat(container.hasOnClickListeners()).isTrue()
+ val value by collectLastValue(mBluetoothDetailsContentManager.deviceItemClick)
+ runCurrent()
+ container.performClick()
+ runCurrent()
+ assertThat(value).isNotNull()
+ value?.let {
+ assertThat(it.target).isEqualTo(DeviceItemClick.Target.ENTIRE_ROW)
+ assertThat(it.clickedView).isEqualTo(container)
+ assertThat(it.deviceItem).isEqualTo(deviceItem)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testDeviceItemViewHolder_cachedDeviceBusy() {
+ with(kosmos) {
+ deviceItem.isEnabled = false
+
+ val view =
+ LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
+ val viewHolder =
+ BluetoothDetailsContentManager(
+ uiProperties,
+ CONTENT_HEIGHT,
+ bluetoothTileDialogCallback,
+ /* isInDialog= */ true,
+ {},
+ testDispatcher,
+ fakeSystemClock,
+ uiEventLogger,
+ logger,
+ )
+ .Adapter()
+ .DeviceItemViewHolder(view)
+ viewHolder.bind(deviceItem)
+ val container = view.requireViewById<View>(R.id.bluetooth_device_row)
+
+ assertThat(container).isNotNull()
+ assertThat(container.isEnabled).isFalse()
+ assertThat(container.hasOnClickListeners()).isTrue()
+ }
+ }
+
+ @Test
+ fun testDeviceItemViewHolder_clickActionIcon() {
+ with(kosmos) {
+ testScope.runTest {
+ deviceItem.isEnabled = true
+
+ val view =
+ LayoutInflater.from(mContext)
+ .inflate(R.layout.bluetooth_device_item, null, false)
+ val viewHolder =
+ mBluetoothDetailsContentManager.Adapter().DeviceItemViewHolder(view)
+ viewHolder.bind(deviceItem)
+ val actionIconView = view.requireViewById<View>(R.id.gear_icon)
+
+ assertThat(actionIconView).isNotNull()
+ assertThat(actionIconView.hasOnClickListeners()).isTrue()
+ val value by collectLastValue(mBluetoothDetailsContentManager.deviceItemClick)
+ runCurrent()
+ actionIconView.performClick()
+ runCurrent()
+ assertThat(value).isNotNull()
+ value?.let {
+ assertThat(it.target).isEqualTo(DeviceItemClick.Target.ACTION_ICON)
+ assertThat(it.clickedView).isEqualTo(actionIconView)
+ assertThat(it.deviceItem).isEqualTo(deviceItem)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testOnDeviceUpdated_hideSeeAll_showPairNew() {
+ with(kosmos) {
+ testScope.runTest {
+ mBluetoothDetailsContentManager.bind(contentView)
+ mBluetoothDetailsContentManager.start()
+ fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
+ mBluetoothDetailsContentManager.onDeviceItemUpdated(
+ listOf(deviceItem),
+ showSeeAll = false,
+ showPairNewDevice = true,
+ )
+
+ val seeAllButton = contentView.requireViewById<View>(R.id.see_all_button)
+ val pairNewButton = contentView.requireViewById<View>(R.id.pair_new_device_button)
+ val recyclerView = contentView.requireViewById<RecyclerView>(R.id.device_list)
+ val adapter = recyclerView?.adapter as BluetoothDetailsContentManager.Adapter
+ val scrollViewContent = contentView.requireViewById<View>(R.id.scroll_view)
+
+ assertThat(seeAllButton).isNotNull()
+ assertThat(seeAllButton.visibility).isEqualTo(GONE)
+ assertThat(pairNewButton).isNotNull()
+ assertThat(pairNewButton.visibility).isEqualTo(VISIBLE)
+ assertThat(adapter.itemCount).isEqualTo(1)
+ assertThat(scrollViewContent.layoutParams.height).isEqualTo(WRAP_CONTENT)
+ mBluetoothDetailsContentManager.releaseView()
+ }
+ }
+ }
+
+ @Test
+ fun testShowDialog_cachedHeightLargerThanMinHeight_displayFromCachedHeight() {
+ with(kosmos) {
+ testScope.runTest {
+ val cachedHeight = Int.MAX_VALUE
+ val contentManager =
+ BluetoothDetailsContentManager(
+ BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
+ cachedHeight,
+ bluetoothTileDialogCallback,
+ /* isInDialog= */ true,
+ {},
+ testDispatcher,
+ fakeSystemClock,
+ uiEventLogger,
+ logger,
+ )
+ contentManager.bind(contentView)
+ contentManager.start()
+ assertThat(contentView.requireViewById<View>(R.id.scroll_view).layoutParams.height)
+ .isEqualTo(cachedHeight)
+ contentManager.releaseView()
+ }
+ }
+ }
+
+ @Test
+ fun testShowDialog_cachedHeightLessThanMinHeight_displayFromUiProperties() {
+ with(kosmos) {
+ testScope.runTest {
+ val contentManager =
+ BluetoothDetailsContentManager(
+ BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
+ MATCH_PARENT,
+ bluetoothTileDialogCallback,
+ /* isInDialog= */ true,
+ {},
+ testDispatcher,
+ fakeSystemClock,
+ uiEventLogger,
+ logger,
+ )
+ contentManager.bind(contentView)
+ contentManager.start()
+ assertThat(contentView.requireViewById<View>(R.id.scroll_view).layoutParams.height)
+ .isGreaterThan(MATCH_PARENT)
+ contentManager.releaseView()
+ }
+ }
+ }
+
+ @Test
+ fun testShowDialog_bluetoothEnabled_autoOnToggleGone() {
+ with(kosmos) {
+ testScope.runTest {
+ val contentManager =
+ BluetoothDetailsContentManager(
+ BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
+ MATCH_PARENT,
+ bluetoothTileDialogCallback,
+ /* isInDialog= */ true,
+ {},
+ testDispatcher,
+ fakeSystemClock,
+ uiEventLogger,
+ logger,
+ )
+ contentManager.bind(contentView)
+ contentManager.start()
+ assertThat(
+ contentView
+ .requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout)
+ .visibility
+ )
+ .isEqualTo(GONE)
+ contentManager.releaseView()
+ }
+ }
+ }
+
+ @Test
+ fun testOnAudioSharingButtonUpdated_visibleActive_activateButton() {
+ with(kosmos) {
+ testScope.runTest {
+ mBluetoothDetailsContentManager.bind(contentView)
+ mBluetoothDetailsContentManager.start()
+ fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
+ mBluetoothDetailsContentManager.onAudioSharingButtonUpdated(
+ visibility = VISIBLE,
+ label = null,
+ isActive = true,
+ )
+
+ val audioSharingButton =
+ contentView.requireViewById<View>(R.id.audio_sharing_button)
+
+ assertThat(audioSharingButton).isNotNull()
+ assertThat(audioSharingButton.visibility).isEqualTo(VISIBLE)
+ assertThat(audioSharingButton.isActivated).isTrue()
+ mBluetoothDetailsContentManager.releaseView()
+ }
+ }
+ }
+
+ @Test
+ fun testOnAudioSharingButtonUpdated_visibleNotActive_inactivateButton() {
+ with(kosmos) {
+ testScope.runTest {
+ mBluetoothDetailsContentManager.bind(contentView)
+ mBluetoothDetailsContentManager.start()
+ fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
+ mBluetoothDetailsContentManager.onAudioSharingButtonUpdated(
+ visibility = VISIBLE,
+ label = null,
+ isActive = false,
+ )
+
+ val audioSharingButton =
+ contentView.requireViewById<View>(R.id.audio_sharing_button)
+
+ assertThat(audioSharingButton).isNotNull()
+ assertThat(audioSharingButton.visibility).isEqualTo(VISIBLE)
+ assertThat(audioSharingButton.isActivated).isFalse()
+ mBluetoothDetailsContentManager.releaseView()
+ }
+ }
+ }
+
+ @Test
+ fun testOnAudioSharingButtonUpdated_gone_inactivateButton() {
+ with(kosmos) {
+ testScope.runTest {
+ mBluetoothDetailsContentManager.bind(contentView)
+ mBluetoothDetailsContentManager.start()
+ fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
+ mBluetoothDetailsContentManager.onAudioSharingButtonUpdated(
+ visibility = GONE,
+ label = null,
+ isActive = false,
+ )
+
+ val audioSharingButton =
+ contentView.requireViewById<View>(R.id.audio_sharing_button)
+
+ assertThat(audioSharingButton).isNotNull()
+ assertThat(audioSharingButton.visibility).isEqualTo(GONE)
+ assertThat(audioSharingButton.isActivated).isFalse()
+ mBluetoothDetailsContentManager.releaseView()
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
index 4396b0a42ae6..ffc75188ffa1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt
@@ -16,47 +16,34 @@
package com.android.systemui.bluetooth.qsdialog
-import android.graphics.drawable.Drawable
import android.testing.TestableLooper
-import android.view.LayoutInflater
-import android.view.View
-import android.view.View.GONE
-import android.view.View.VISIBLE
-import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.model.SysUiState
-import com.android.systemui.res.R
import com.android.systemui.shade.data.repository.shadeDialogContextInteractor
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.SystemUIDialogManager
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.time.FakeSystemClock
-import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mock
-import org.mockito.Mockito.`when`
+import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
@@ -73,33 +60,31 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() {
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
- @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
+ @Mock
+ private lateinit var bluetoothDetailsContentManagerFactory:
+ BluetoothDetailsContentManager.Factory
- @Mock private lateinit var bluetoothTileDialogCallback: BluetoothTileDialogCallback
+ @Mock private lateinit var bluetoothDetailsContentManager: BluetoothDetailsContentManager
- @Mock private lateinit var drawable: Drawable
+ @Mock private lateinit var bluetoothTileDialogCallback: BluetoothTileDialogCallback
@Mock private lateinit var uiEventLogger: UiEventLogger
- @Mock private lateinit var logger: BluetoothTileDialogLogger
+ @Mock private lateinit var sysuiDialogFactory: SystemUIDialog.Factory
+ @Mock private lateinit var dialogManager: SystemUIDialogManager
+ @Mock private lateinit var sysuiState: SysUiState
+ @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator
private val uiProperties =
BluetoothTileDialogViewModel.UiProperties.build(
isBluetoothEnabled = ENABLED,
isAutoOnToggleFeatureAvailable = ENABLED,
)
- @Mock private lateinit var sysuiDialogFactory: SystemUIDialog.Factory
- @Mock private lateinit var dialogManager: SystemUIDialogManager
- @Mock private lateinit var sysuiState: SysUiState
- @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator
-
- private val fakeSystemClock = FakeSystemClock()
+ private lateinit var scheduler: TestCoroutineScheduler
private lateinit var dispatcher: CoroutineDispatcher
private lateinit var testScope: TestScope
- private lateinit var icon: Pair<Drawable, String>
private lateinit var mBluetoothTileDialogDelegate: BluetoothTileDialogDelegate
- private lateinit var deviceItem: DeviceItem
private val kosmos = testKosmos()
@@ -116,12 +101,10 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() {
CONTENT_HEIGHT,
bluetoothTileDialogCallback,
{},
- dispatcher,
- fakeSystemClock,
uiEventLogger,
- logger,
sysuiDialogFactory,
kosmos.shadeDialogContextInteractor,
+ bluetoothDetailsContentManagerFactory,
)
whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java), any()))
@@ -138,17 +121,16 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() {
)
}
- icon = Pair(drawable, DEVICE_NAME)
- deviceItem =
- DeviceItem(
- type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
- cachedBluetoothDevice = cachedBluetoothDevice,
- deviceName = DEVICE_NAME,
- connectionSummary = DEVICE_CONNECTION_SUMMARY,
- iconWithDescription = icon,
- background = null,
+ whenever(
+ bluetoothDetailsContentManagerFactory.create(
+ any(),
+ anyInt(),
+ any(),
+ anyBoolean(),
+ any(),
+ )
)
- `when`(cachedBluetoothDevice.isBusy).thenReturn(false)
+ .thenReturn(bluetoothDetailsContentManager)
}
@Test
@@ -156,287 +138,9 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() {
val dialog = mBluetoothTileDialogDelegate.createDialog()
dialog.show()
- val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list)
-
- assertThat(recyclerView).isNotNull()
- assertThat(recyclerView.visibility).isEqualTo(VISIBLE)
- assertThat(recyclerView.adapter).isNotNull()
- assertThat(recyclerView.layoutManager is LinearLayoutManager).isTrue()
+ verify(bluetoothDetailsContentManager).bind(any())
+ verify(bluetoothDetailsContentManager).start()
dialog.dismiss()
- }
-
- @Test
- fun testShowDialog_displayBluetoothDevice() {
- testScope.runTest {
- val dialog = mBluetoothTileDialogDelegate.createDialog()
- dialog.show()
- fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
- mBluetoothTileDialogDelegate.onDeviceItemUpdated(
- dialog,
- listOf(deviceItem),
- showSeeAll = false,
- showPairNewDevice = false,
- )
-
- val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list)
- val adapter = recyclerView?.adapter as BluetoothTileDialogDelegate.Adapter
- assertThat(adapter.itemCount).isEqualTo(1)
- assertThat(adapter.getItem(0).deviceName).isEqualTo(DEVICE_NAME)
- assertThat(adapter.getItem(0).connectionSummary).isEqualTo(DEVICE_CONNECTION_SUMMARY)
- assertThat(adapter.getItem(0).iconWithDescription).isEqualTo(icon)
- dialog.dismiss()
- }
- }
-
- @Test
- fun testDeviceItemViewHolder_cachedDeviceNotBusy() {
- testScope.runTest {
- deviceItem.isEnabled = true
-
- val view =
- LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
- val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view)
- viewHolder.bind(deviceItem)
- val container = view.requireViewById<View>(R.id.bluetooth_device_row)
-
- assertThat(container).isNotNull()
- assertThat(container.isEnabled).isTrue()
- assertThat(container.hasOnClickListeners()).isTrue()
- val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick)
- runCurrent()
- container.performClick()
- runCurrent()
- assertThat(value).isNotNull()
- value?.let {
- assertThat(it.target).isEqualTo(DeviceItemClick.Target.ENTIRE_ROW)
- assertThat(it.clickedView).isEqualTo(container)
- assertThat(it.deviceItem).isEqualTo(deviceItem)
- }
- }
- }
-
- @Test
- fun testDeviceItemViewHolder_cachedDeviceBusy() {
- deviceItem.isEnabled = false
-
- val view =
- LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
- val viewHolder =
- BluetoothTileDialogDelegate(
- uiProperties,
- CONTENT_HEIGHT,
- bluetoothTileDialogCallback,
- {},
- dispatcher,
- fakeSystemClock,
- uiEventLogger,
- logger,
- sysuiDialogFactory,
- kosmos.shadeDialogContextInteractor,
- )
- .Adapter()
- .DeviceItemViewHolder(view)
- viewHolder.bind(deviceItem)
- val container = view.requireViewById<View>(R.id.bluetooth_device_row)
-
- assertThat(container).isNotNull()
- assertThat(container.isEnabled).isFalse()
- assertThat(container.hasOnClickListeners()).isTrue()
- }
-
- @Test
- fun testDeviceItemViewHolder_clickActionIcon() {
- testScope.runTest {
- deviceItem.isEnabled = true
-
- val view =
- LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false)
- val viewHolder = mBluetoothTileDialogDelegate.Adapter().DeviceItemViewHolder(view)
- viewHolder.bind(deviceItem)
- val actionIconView = view.requireViewById<View>(R.id.gear_icon)
-
- assertThat(actionIconView).isNotNull()
- assertThat(actionIconView.hasOnClickListeners()).isTrue()
- val value by collectLastValue(mBluetoothTileDialogDelegate.deviceItemClick)
- runCurrent()
- actionIconView.performClick()
- runCurrent()
- assertThat(value).isNotNull()
- value?.let {
- assertThat(it.target).isEqualTo(DeviceItemClick.Target.ACTION_ICON)
- assertThat(it.clickedView).isEqualTo(actionIconView)
- assertThat(it.deviceItem).isEqualTo(deviceItem)
- }
- }
- }
-
- @Test
- fun testOnDeviceUpdated_hideSeeAll_showPairNew() {
- testScope.runTest {
- val dialog = mBluetoothTileDialogDelegate.createDialog()
- dialog.show()
- fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
- mBluetoothTileDialogDelegate.onDeviceItemUpdated(
- dialog,
- listOf(deviceItem),
- showSeeAll = false,
- showPairNewDevice = true,
- )
-
- val seeAllButton = dialog.requireViewById<View>(R.id.see_all_button)
- val pairNewButton = dialog.requireViewById<View>(R.id.pair_new_device_button)
- val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list)
- val adapter = recyclerView?.adapter as BluetoothTileDialogDelegate.Adapter
- val scrollViewContent = dialog.requireViewById<View>(R.id.scroll_view)
-
- assertThat(seeAllButton).isNotNull()
- assertThat(seeAllButton.visibility).isEqualTo(GONE)
- assertThat(pairNewButton).isNotNull()
- assertThat(pairNewButton.visibility).isEqualTo(VISIBLE)
- assertThat(adapter.itemCount).isEqualTo(1)
- assertThat(scrollViewContent.layoutParams.height).isEqualTo(WRAP_CONTENT)
- dialog.dismiss()
- }
- }
-
- @Test
- fun testShowDialog_cachedHeightLargerThanMinHeight_displayFromCachedHeight() {
- testScope.runTest {
- val cachedHeight = Int.MAX_VALUE
- val dialog =
- BluetoothTileDialogDelegate(
- BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
- cachedHeight,
- bluetoothTileDialogCallback,
- {},
- dispatcher,
- fakeSystemClock,
- uiEventLogger,
- logger,
- sysuiDialogFactory,
- kosmos.shadeDialogContextInteractor,
- )
- .createDialog()
- dialog.show()
- assertThat(dialog.requireViewById<View>(R.id.scroll_view).layoutParams.height)
- .isEqualTo(cachedHeight)
- dialog.dismiss()
- }
- }
-
- @Test
- fun testShowDialog_cachedHeightLessThanMinHeight_displayFromUiProperties() {
- testScope.runTest {
- val dialog =
- BluetoothTileDialogDelegate(
- BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
- MATCH_PARENT,
- bluetoothTileDialogCallback,
- {},
- dispatcher,
- fakeSystemClock,
- uiEventLogger,
- logger,
- sysuiDialogFactory,
- kosmos.shadeDialogContextInteractor,
- )
- .createDialog()
- dialog.show()
- assertThat(dialog.requireViewById<View>(R.id.scroll_view).layoutParams.height)
- .isGreaterThan(MATCH_PARENT)
- dialog.dismiss()
- }
- }
-
- @Test
- fun testShowDialog_bluetoothEnabled_autoOnToggleGone() {
- testScope.runTest {
- val dialog =
- BluetoothTileDialogDelegate(
- BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
- MATCH_PARENT,
- bluetoothTileDialogCallback,
- {},
- dispatcher,
- fakeSystemClock,
- uiEventLogger,
- logger,
- sysuiDialogFactory,
- kosmos.shadeDialogContextInteractor,
- )
- .createDialog()
- dialog.show()
- assertThat(
- dialog.requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout).visibility
- )
- .isEqualTo(GONE)
- dialog.dismiss()
- }
- }
-
- @Test
- fun testOnAudioSharingButtonUpdated_visibleActive_activateButton() {
- testScope.runTest {
- val dialog = mBluetoothTileDialogDelegate.createDialog()
- dialog.show()
- fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
- mBluetoothTileDialogDelegate.onAudioSharingButtonUpdated(
- dialog,
- visibility = VISIBLE,
- label = null,
- isActive = true,
- )
-
- val audioSharingButton = dialog.requireViewById<View>(R.id.audio_sharing_button)
-
- assertThat(audioSharingButton).isNotNull()
- assertThat(audioSharingButton.visibility).isEqualTo(VISIBLE)
- assertThat(audioSharingButton.isActivated).isTrue()
- dialog.dismiss()
- }
- }
-
- @Test
- fun testOnAudioSharingButtonUpdated_visibleNotActive_inactivateButton() {
- testScope.runTest {
- val dialog = mBluetoothTileDialogDelegate.createDialog()
- dialog.show()
- fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
- mBluetoothTileDialogDelegate.onAudioSharingButtonUpdated(
- dialog,
- visibility = VISIBLE,
- label = null,
- isActive = false,
- )
-
- val audioSharingButton = dialog.requireViewById<View>(R.id.audio_sharing_button)
-
- assertThat(audioSharingButton).isNotNull()
- assertThat(audioSharingButton.visibility).isEqualTo(VISIBLE)
- assertThat(audioSharingButton.isActivated).isFalse()
- dialog.dismiss()
- }
- }
-
- @Test
- fun testOnAudioSharingButtonUpdated_gone_inactivateButton() {
- testScope.runTest {
- val dialog = mBluetoothTileDialogDelegate.createDialog()
- dialog.show()
- fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE)
- mBluetoothTileDialogDelegate.onAudioSharingButtonUpdated(
- dialog,
- visibility = GONE,
- label = null,
- isActive = false,
- )
-
- val audioSharingButton = dialog.requireViewById<View>(R.id.audio_sharing_button)
-
- assertThat(audioSharingButton).isNotNull()
- assertThat(audioSharingButton.visibility).isEqualTo(GONE)
- assertThat(audioSharingButton.isActivated).isFalse()
- dialog.dismiss()
- }
+ verify(bluetoothDetailsContentManager).releaseView()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
index a56c2cb25542..47a834be9b9c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
@@ -45,7 +45,6 @@ import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -67,7 +66,6 @@ import org.mockito.junit.MockitoRule
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
-@OptIn(ExperimentalCoroutinesApi::class)
@EnableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE)
class BluetoothTileDialogViewModelTest : SysuiTestCase() {
@@ -78,8 +76,6 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
private lateinit var bluetoothTileDialogViewModel: BluetoothTileDialogViewModel
- @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
-
@Mock private lateinit var bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor
@Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
@@ -108,9 +104,16 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
@Mock private lateinit var bluetoothTileDialogDelegate: BluetoothTileDialogDelegate
+ @Mock
+ private lateinit var bluetoothDetailsContentManagerFactory:
+ BluetoothDetailsContentManager.Factory
+
+ @Mock private lateinit var bluetoothDetailsContentManager: BluetoothDetailsContentManager
+
@Mock private lateinit var sysuiDialog: SystemUIDialog
@Mock private lateinit var expandable: Expandable
@Mock private lateinit var controller: DialogTransitionAnimator.Controller
+ @Mock private lateinit var mockView: View
private val sharedPreferences = FakeSharedPreferences()
@@ -131,7 +134,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
localBluetoothManager,
bluetoothTileDialogLogger,
testScope.backgroundScope,
- dispatcher
+ dispatcher,
),
// TODO(b/316822488): Create FakeBluetoothAutoOnInteractor.
BluetoothAutoOnInteractor(
@@ -139,7 +142,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
localBluetoothManager,
bluetoothAdapter,
testScope.backgroundScope,
- dispatcher
+ dispatcher,
)
),
kosmos.audioSharingInteractor,
@@ -153,7 +156,8 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
dispatcher,
dispatcher,
sharedPreferences,
- mBluetoothTileDialogDelegateDelegateFactory
+ mBluetoothTileDialogDelegateDelegateFactory,
+ bluetoothDetailsContentManagerFactory,
)
whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow())
whenever(deviceItemInteractor.deviceItemUpdateRequest)
@@ -163,20 +167,34 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
whenever(mBluetoothTileDialogDelegateDelegateFactory.create(any(), anyInt(), any(), any()))
.thenReturn(bluetoothTileDialogDelegate)
whenever(bluetoothTileDialogDelegate.createDialog()).thenReturn(sysuiDialog)
+ whenever(bluetoothTileDialogDelegate.contentManager)
+ .thenReturn(bluetoothDetailsContentManager)
+ whenever(
+ bluetoothDetailsContentManagerFactory.create(
+ any(),
+ anyInt(),
+ any(),
+ anyBoolean(),
+ any(),
+ )
+ )
+ .thenReturn(bluetoothDetailsContentManager)
whenever(sysuiDialog.context).thenReturn(mContext)
- whenever(bluetoothTileDialogDelegate.bluetoothStateToggle)
+ whenever(bluetoothDetailsContentManager.bluetoothStateToggle)
.thenReturn(getMutableStateFlow(false))
- whenever(bluetoothTileDialogDelegate.deviceItemClick).thenReturn(MutableSharedFlow())
- whenever(bluetoothTileDialogDelegate.contentHeight).thenReturn(getMutableStateFlow(0))
- whenever(bluetoothTileDialogDelegate.bluetoothAutoOnToggle)
+ whenever(bluetoothDetailsContentManager.deviceItemClick)
+ .thenReturn(getMutableStateFlow(null))
+ whenever(bluetoothDetailsContentManager.contentHeight).thenReturn(getMutableStateFlow(0))
+ whenever(bluetoothDetailsContentManager.bluetoothAutoOnToggle)
.thenReturn(getMutableStateFlow(false))
whenever(expandable.dialogTransitionController(any())).thenReturn(controller)
+ whenever(mockView.context).thenReturn(mContext)
}
@Test
- fun testShowDialog_noAnimation() {
+ fun testShowDetailsContent_noAnimation() {
testScope.runTest {
- bluetoothTileDialogViewModel.showDialog(null)
+ bluetoothTileDialogViewModel.showDetailsContent(null, null)
runCurrent()
verify(mDialogTransitionAnimator, never()).show(any(), any(), any())
@@ -184,9 +202,9 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
}
@Test
- fun testShowDialog_animated() {
+ fun testShowDetailsContent_animated() {
testScope.runTest {
- bluetoothTileDialogViewModel.showDialog(expandable)
+ bluetoothTileDialogViewModel.showDetailsContent(expandable, null)
runCurrent()
verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
@@ -194,10 +212,21 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
}
@Test
- fun testShowDialog_animated_callInBackgroundThread() {
+ fun testShowDetailsContent_animated_inDetailsView() {
+ testScope.runTest {
+ bluetoothTileDialogViewModel.showDetailsContent(expandable, mockView)
+ runCurrent()
+
+ verify(bluetoothDetailsContentManager).bind(mockView)
+ verify(bluetoothDetailsContentManager).start()
+ }
+ }
+
+ @Test
+ fun testShowDetailsContent_animated_callInBackgroundThread() {
testScope.runTest {
backgroundExecutor.execute {
- bluetoothTileDialogViewModel.showDialog(expandable)
+ bluetoothTileDialogViewModel.showDetailsContent(expandable, null)
runCurrent()
verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
@@ -206,9 +235,22 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
}
@Test
- fun testShowDialog_fetchDeviceItem() {
+ fun testShowDetailsContent_animated_callInBackgroundThread_inDetailsView() {
+ testScope.runTest {
+ backgroundExecutor.execute {
+ bluetoothTileDialogViewModel.showDetailsContent(expandable, mockView)
+ runCurrent()
+
+ verify(bluetoothDetailsContentManager).bind(mockView)
+ verify(bluetoothDetailsContentManager).start()
+ }
+ }
+ }
+
+ @Test
+ fun testShowDetailsContent_fetchDeviceItem() {
testScope.runTest {
- bluetoothTileDialogViewModel.showDialog(null)
+ bluetoothTileDialogViewModel.showDetailsContent(null, null)
runCurrent()
verify(deviceItemInteractor).deviceItemUpdate
@@ -219,7 +261,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
fun testStartSettingsActivity_activityLaunched_dialogDismissed() {
testScope.runTest {
whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
- bluetoothTileDialogViewModel.showDialog(null)
+ bluetoothTileDialogViewModel.showDetailsContent(null, null)
runCurrent()
val clickedView = View(context)
@@ -236,7 +278,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
val actual =
BluetoothTileDialogViewModel.UiProperties.build(
isBluetoothEnabled = true,
- isAutoOnToggleFeatureAvailable = true
+ isAutoOnToggleFeatureAvailable = true,
)
assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE)
}
@@ -248,7 +290,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
val actual =
BluetoothTileDialogViewModel.UiProperties.build(
isBluetoothEnabled = false,
- isAutoOnToggleFeatureAvailable = true
+ isAutoOnToggleFeatureAvailable = true,
)
assertThat(actual.autoOnToggleVisibility).isEqualTo(VISIBLE)
}
@@ -260,7 +302,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() {
val actual =
BluetoothTileDialogViewModel.UiProperties.build(
isBluetoothEnabled = false,
- isAutoOnToggleFeatureAvailable = false
+ isAutoOnToggleFeatureAvailable = false,
)
assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/brightness/ui/compose/BrightnessSliderMotionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/brightness/ui/compose/BrightnessSliderMotionTest.kt
new file mode 100644
index 000000000000..9dab9d735603
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/brightness/ui/compose/BrightnessSliderMotionTest.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.brightness.ui.compose
+
+import android.platform.test.annotations.MotionTest
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.swipeLeft
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.android.compose.theme.PlatformTheme
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel
+import com.android.systemui.common.shared.model.asIcon
+import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.motion.createSysUiComposeMotionTestRule
+import com.android.systemui.utils.PolicyRestriction
+import kotlin.test.Test
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.joinAll
+import org.junit.Rule
+import org.junit.runner.RunWith
+import platform.test.motion.compose.ComposeRecordingSpec
+import platform.test.motion.compose.MotionControl
+import platform.test.motion.compose.MotionControlScope
+import platform.test.motion.compose.feature
+import platform.test.motion.compose.motionTestValueOfNode
+import platform.test.motion.compose.recordMotion
+import platform.test.motion.compose.runTest
+import platform.test.motion.compose.values.MotionTestValueKey
+import platform.test.motion.golden.FeatureCapture
+import platform.test.motion.golden.TimeSeriesCaptureScope
+import platform.test.motion.golden.asDataPoint
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays.Phone
+
+@RunWith(AndroidJUnit4::class)
+@LargeTest
+@MotionTest
+class BrightnessSliderMotionTest : SysuiTestCase() {
+
+ private val deviceSpec = DeviceEmulationSpec(Phone)
+ private val kosmos = Kosmos()
+
+ @get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos, deviceSpec)
+
+ @Composable
+ private fun BrightnessSliderUnderTest(startingValue: Int) {
+ PlatformTheme {
+ BrightnessSlider(
+ gammaValue = startingValue,
+ modifier = Modifier.wrapContentHeight().fillMaxWidth(),
+ valueRange = 0..100,
+ iconResProvider = BrightnessSliderViewModel::getIconForPercentage,
+ imageLoader = { resId, context -> context.getDrawable(resId)!!.asIcon(null) },
+ restriction = PolicyRestriction.NoRestriction,
+ onRestrictedClick = {},
+ onDrag = {},
+ onStop = {},
+ overriddenByAppState = false,
+ hapticsViewModelFactory = kosmos.sliderHapticsViewModelFactory,
+ )
+ }
+ }
+
+ @Test
+ fun iconAlphaChanges() {
+ motionTestRule.runTest(timeout = 30.seconds) {
+ val motion =
+ recordMotion(
+ content = { BrightnessSliderUnderTest(100) },
+ ComposeRecordingSpec(
+ MotionControl(delayReadyToPlay = { awaitCondition { !isAnimating } }) {
+ coroutineScope {
+ val gesture = async {
+ performTouchInputAsync(
+ onNode(hasTestTag("com.android.systemui:id/slider"))
+ ) {
+ swipeLeft(startX = right, endX = left, durationMillis = 500)
+ }
+ }
+ val animationEnd = async {
+ awaitCondition { isAnimating }
+ awaitCondition { !isAnimating }
+ }
+ joinAll(gesture, animationEnd)
+ }
+ }
+ ) {
+ featureFloat(BrightnessSliderMotionTestKeys.ActiveIconAlpha)
+ featureFloat(BrightnessSliderMotionTestKeys.InactiveIconAlpha)
+ },
+ )
+ assertThat(motion).timeSeriesMatchesGolden("brightnessSlider_iconAlphaChanges")
+ }
+ }
+
+ private companion object {
+
+ val MotionControlScope.isAnimating: Boolean
+ get() = motionTestValueOfNode(BrightnessSliderMotionTestKeys.AnimatingIcon)
+
+ fun TimeSeriesCaptureScope<SemanticsNodeInteractionsProvider>.featureFloat(
+ motionTestValueKey: MotionTestValueKey<Float>
+ ) {
+ feature(
+ motionTestValueKey = motionTestValueKey,
+ capture =
+ FeatureCapture(motionTestValueKey.semanticsPropertyKey.name) {
+ it.asDataPoint()
+ },
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderContentProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderContentProviderTest.kt
index 8d9fa6ad6e08..e50035d0067e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderContentProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderContentProviderTest.kt
@@ -41,7 +41,6 @@ const val AUTHORITY = "exception.provider.authority"
val TEST_URI = Uri.Builder().scheme("content").authority(AUTHORITY).path("path").build()
@SmallTest
-@kotlinx.coroutines.ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class ImageLoaderContentProviderTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderTest.kt
index 76c3349c25ba..de70bca7e96c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/graphics/ImageLoaderTest.kt
@@ -29,7 +29,6 @@ import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
-@kotlinx.coroutines.ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class ImageLoaderTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
index a3c3d2cdbb43..32b8bb49de96 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
@@ -70,7 +70,6 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -84,7 +83,6 @@ import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
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 38acd23d282c..0c9213c3a722 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -206,6 +206,7 @@ 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>
@@ -280,7 +281,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
() -> mShadeInteractor,
mShadeWindowLogger,
() -> mSelectedUserInteractor,
- mUserTracker,
+ mock(UserTracker.class),
mKosmos.getNotificationShadeWindowModel(),
mSecureSettings,
mKosmos::getCommunalInteractor,
@@ -318,7 +319,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
} catch (Exception e) {
// Just so we don't have to add the exception signature to every test.
- fail();
+ fail(e.getMessage());
}
}
@@ -330,18 +331,156 @@ 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(mViewMediator.mUpdateCallback, never()).onUserSwitching(userId);
+ verify(mUserTrackerCallback, never()).onUserChanging(eq(userId), eq(mContext),
+ any(Runnable.class));
/* Next test user switching is already in progress when started */
when(mUserTracker.isUserSwitching()).thenReturn(true);
mViewMediator.onSystemReady();
TestableLooper.get(this).processAllMessages();
- verify(mViewMediator.mUpdateCallback).onUserSwitching(userId);
+ 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());
}
@Test
@@ -1105,7 +1244,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
processAllMessagesAndBgExecutorMessages();
verify(mStatusBarKeyguardViewManager, never()).reset(anyBoolean());
- assertATMSAndKeyguardViewMediatorStatesMatch();
+
}
@Test
@@ -1149,6 +1288,7 @@ 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();
@@ -1203,13 +1343,6 @@ 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());
- }
}
/**
@@ -1370,6 +1503,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase {
mKeyguardInteractor,
mKeyguardTransitionBootInteractor,
mock(WindowManagerOcclusionManager.class));
+ mViewMediator.mUserChangedCallback = mUserTrackerCallback;
mViewMediator.start();
mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null);
@@ -1383,4 +1517,10 @@ 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/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index ab691c630f97..a17125388ed6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -59,7 +59,6 @@ import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -79,7 +78,6 @@ import platform.test.runner.parameterized.Parameter
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
@DisableSceneContainer
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
index caf08efc4b32..2648c9c6496e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt
@@ -60,7 +60,6 @@ import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -79,7 +78,6 @@ import platform.test.runner.parameterized.Parameter
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
-@OptIn(ExperimentalCoroutinesApi::class)
@FlakyTest(
bugId = 292574995,
detail = "on certain architectures all permutations with startActivity=true is causing failures",
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
index 418972055324..df24bff43c08 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt
@@ -38,7 +38,6 @@ import com.android.systemui.shade.NotificationPanelView
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import org.junit.Before
@@ -49,7 +48,6 @@ import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
-@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
@SmallTest
class DefaultDeviceEntrySectionTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
index 051aba3d593f..26c13f2a2519 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
@@ -77,7 +77,6 @@ import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth
import kotlin.math.min
import kotlin.test.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
@@ -90,7 +89,6 @@ import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class KeyguardQuickAffordancesCombinedViewModelTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt
index e3aeaa85873d..fe5acb5668ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt
@@ -28,7 +28,6 @@ import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch
@@ -52,7 +51,6 @@ import org.mockito.junit.MockitoJUnit
import org.mockito.kotlin.KArgumentCaptor
import org.mockito.kotlin.argumentCaptor
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class RepeatWhenAttachedTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt
index 43ee388e44a7..8a2dc15d7545 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/lowlightclock/AmbientLightModeMonitorTest.kt
@@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.sensors.AsyncSensorManager
import java.util.Optional
+import javax.inject.Provider
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -50,7 +51,11 @@ class AmbientLightModeMonitorTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this)
ambientLightModeMonitor =
- AmbientLightModeMonitor(Optional.of(algorithm), sensorManager, Optional.of(sensor))
+ AmbientLightModeMonitor(
+ Optional.of(algorithm),
+ sensorManager,
+ Optional.of(Provider { sensor }),
+ )
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index 676d8fa06d82..122af0639030 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -73,7 +73,6 @@ import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
@@ -117,7 +116,6 @@ private const val SESSION_EMPTY_TITLE = ""
private const val USER_ID = 0
private val DISMISS_INTENT = Intent().apply { action = "dismiss" }
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWithLooper(setAsMainLooper = true)
@RunWith(ParameterizedAndroidJunit4::class)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
index 811d2e2b2b06..b731c4f18c7f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt
@@ -49,7 +49,6 @@ import com.android.systemui.testKosmos
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -81,7 +80,6 @@ private const val SMARTSPACE_KEY = "SMARTSPACE_KEY"
private const val SMARTSPACE_PACKAGE = "SMARTSPACE_PKG"
private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!!
-@ExperimentalCoroutinesApi
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index 496b31990b9d..f9e8cbfc491c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -80,7 +80,6 @@ import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runCurrent
@@ -123,7 +122,6 @@ private const val SESSION_EMPTY_TITLE = ""
private const val USER_ID = 0
private val DISMISS_INTENT = Intent().apply { action = "dismiss" }
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWithLooper(setAsMainLooper = true)
@RunWith(ParameterizedAndroidJunit4::class)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index 6c8a46f19f2b..a2bd5ec28f08 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -79,7 +79,6 @@ import java.util.Locale
import javax.inject.Provider
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertTrue
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
@@ -112,7 +111,6 @@ private val SMARTSPACE_KEY = "smartspace"
private const val PAUSED_LOCAL = "paused local"
private const val PLAYING_LOCAL = "playing local"
-@ExperimentalCoroutinesApi
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(ParameterizedAndroidJunit4::class)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt
index 072caa74df20..6ca4ae2d2259 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerTest.kt
@@ -61,7 +61,6 @@ import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.utils.os.FakeHandler
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -86,7 +85,6 @@ import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.lastValue
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -458,7 +456,6 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue()
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
fun isCurrentlyInGuidedTransformation_hostsVisible_expandImmediateEnabled_returnsFalse() =
testScope.runTest {
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 2715cb31ca8b..86094d1a0fef 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
@@ -891,6 +891,13 @@ public class MediaSwitchingControllerTest extends SysuiTestCase {
}
@Test
+ public void getTransferableMediaDevice_triggersFromLocalMediaManager() {
+ mMediaSwitchingController.getTransferableMediaDevices();
+
+ verify(mLocalMediaManager).getTransferableMediaDevices();
+ }
+
+ @Test
public void getDeselectableMediaDevice_triggersFromLocalMediaManager() {
mMediaSwitchingController.getDeselectableMediaDevice();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
index 7849ea5ab7ed..042eb871e3c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt
@@ -70,7 +70,6 @@ import com.android.wm.shell.bubbles.Bubbles
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlin.test.assertNotNull
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runCurrent
@@ -90,7 +89,6 @@ import org.mockito.MockitoAnnotations
import org.mockito.kotlin.whenever
/** atest SystemUITests:NoteTaskControllerTest */
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
internal class NoteTaskControllerTest : SysuiTestCase() {
@@ -179,7 +177,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
.apply { infoReference.set(expectedInfo) }
.onBubbleExpandChanged(
isExpanding = true,
- key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
+ key = Bubble.getNoteBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
)
verify(eventLogger).logNoteTaskOpened(expectedInfo)
@@ -194,7 +192,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
.apply { infoReference.set(expectedInfo) }
.onBubbleExpandChanged(
isExpanding = false,
- key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
+ key = Bubble.getNoteBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
)
verify(eventLogger).logNoteTaskClosed(expectedInfo)
@@ -209,7 +207,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
.apply { infoReference.set(expectedInfo) }
.onBubbleExpandChanged(
isExpanding = true,
- key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
+ key = Bubble.getNoteBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
)
verifyNoMoreInteractions(bubbles, keyguardManager, userManager, eventLogger)
@@ -223,14 +221,14 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
.apply { infoReference.set(expectedInfo) }
.onBubbleExpandChanged(
isExpanding = false,
- key = Bubble.getAppBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
+ key = Bubble.getNoteBubbleKeyForApp(expectedInfo.packageName, expectedInfo.user),
)
verifyNoMoreInteractions(bubbles, keyguardManager, userManager, eventLogger)
}
@Test
- fun onBubbleExpandChanged_notKeyAppBubble_shouldDoNothing() {
+ fun onBubbleExpandChanged_notKeyNoteBubble_shouldDoNothing() {
createNoteTaskController().onBubbleExpandChanged(isExpanding = true, key = "any other key")
verifyNoMoreInteractions(bubbles, keyguardManager, userManager, eventLogger)
@@ -241,7 +239,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
createNoteTaskController(isEnabled = false)
.onBubbleExpandChanged(
isExpanding = true,
- key = Bubble.getAppBubbleKeyForApp(NOTE_TASK_INFO.packageName, NOTE_TASK_INFO.user),
+ key = Bubble.getNoteBubbleKeyForApp(NOTE_TASK_INFO.packageName, NOTE_TASK_INFO.user),
)
verifyNoMoreInteractions(bubbles, keyguardManager, userManager, eventLogger)
@@ -740,7 +738,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() {
val intentCaptor = argumentCaptor<Intent>()
val iconCaptor = argumentCaptor<Icon>()
verify(bubbles)
- .showOrHideAppBubble(capture(intentCaptor), eq(userHandle), capture(iconCaptor))
+ .showOrHideNoteBubble(capture(intentCaptor), eq(userHandle), capture(iconCaptor))
assertThat(intentCaptor.value).run {
hasAction(ACTION_CREATE_NOTE)
hasPackage(NOTE_TASK_PACKAGE_NAME)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index 266cb51cc855..8a4f1ad13b78 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -47,7 +47,6 @@ import com.android.systemui.util.time.FakeSystemClock
import com.android.wm.shell.bubbles.Bubbles
import com.google.common.truth.Truth.assertThat
import java.util.Optional
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -60,7 +59,7 @@ import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations.initMocks
/** atest SystemUITests:NoteTaskInitializerTest */
-@OptIn(ExperimentalCoroutinesApi::class, InternalNoteTaskApi::class)
+@OptIn(InternalNoteTaskApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
internal class NoteTaskInitializerTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
index b7fb759eb138..8d7de7e04a03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
package com.android.systemui.notetask.quickaffordance
import android.app.role.RoleManager
@@ -48,7 +46,6 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.After
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
index fc720b836f72..26cf4a261289 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
@@ -68,6 +68,7 @@ class DragAndDropTest : SysuiTestCase() {
columns = 4,
largeTilesSpan = 4,
modifier = Modifier.fillMaxSize(),
+ onAddTile = {},
onRemoveTile = {},
onSetTiles = onSetTiles,
onResize = { _, _ -> },
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt
new file mode 100644
index 000000000000..4e8b0bcd374c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/EditModeTest.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.ui.compose
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.filter
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChildren
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.text.AnnotatedString
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.panels.shared.model.SizedTile
+import com.android.systemui.qs.panels.shared.model.SizedTileImpl
+import com.android.systemui.qs.panels.ui.compose.infinitegrid.DefaultEditTileGrid
+import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.shared.model.TileCategory
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class EditModeTest : SysuiTestCase() {
+ @get:Rule val composeRule = createComposeRule()
+
+ @Composable
+ private fun EditTileGridUnderTest() {
+ var tiles by remember { mutableStateOf(TestEditTiles) }
+ val (currentTiles, otherTiles) = tiles.partition { it.tile.isCurrent }
+ val listState = EditTileListState(currentTiles, columns = 4, largeTilesSpan = 2)
+ DefaultEditTileGrid(
+ listState = listState,
+ otherTiles = otherTiles,
+ columns = 4,
+ largeTilesSpan = 4,
+ modifier = Modifier.fillMaxSize(),
+ onAddTile = { tiles = tiles.add(it) },
+ onRemoveTile = { tiles = tiles.remove(it) },
+ onSetTiles = {},
+ onResize = { _, _ -> },
+ onStopEditing = {},
+ onReset = null,
+ )
+ }
+
+ @Test
+ fun clickAvailableTile_shouldAdd() {
+ composeRule.setContent { EditTileGridUnderTest() }
+ composeRule.waitForIdle()
+
+ composeRule.onNodeWithContentDescription("tileF").performClick() // Tap to add
+ composeRule.waitForIdle()
+
+ composeRule.assertCurrentTilesGridContainsExactly(
+ listOf("tileA", "tileB", "tileC", "tileD_large", "tileE", "tileF")
+ )
+ composeRule.assertAvailableTilesGridContainsExactly(listOf("tileG_large"))
+ }
+
+ @Test
+ fun clickRemoveTarget_shouldRemoveSelection() {
+ composeRule.setContent { EditTileGridUnderTest() }
+ composeRule.waitForIdle()
+
+ composeRule.onNodeWithContentDescription("tileA").performClick() // Selects
+ composeRule.onNodeWithText("Remove").performClick() // Removes
+
+ composeRule.waitForIdle()
+
+ composeRule.assertCurrentTilesGridContainsExactly(
+ listOf("tileB", "tileC", "tileD_large", "tileE")
+ )
+ composeRule.assertAvailableTilesGridContainsExactly(listOf("tileA", "tileF", "tileG_large"))
+ }
+
+ private fun ComposeContentTestRule.assertCurrentTilesGridContainsExactly(specs: List<String>) =
+ assertGridContainsExactly(CURRENT_TILES_GRID_TEST_TAG, specs)
+
+ private fun ComposeContentTestRule.assertAvailableTilesGridContainsExactly(
+ specs: List<String>
+ ) = assertGridContainsExactly(AVAILABLE_TILES_GRID_TEST_TAG, specs)
+
+ private fun ComposeContentTestRule.assertGridContainsExactly(
+ testTag: String,
+ specs: List<String>,
+ ) {
+ onNodeWithTag(testTag)
+ .onChildren()
+ .filter(SemanticsMatcher.keyIsDefined(SemanticsProperties.ContentDescription))
+ .apply {
+ fetchSemanticsNodes().forEachIndexed { index, _ ->
+ get(index).assert(hasContentDescription(specs[index]))
+ }
+ }
+ }
+
+ companion object {
+ private const val CURRENT_TILES_GRID_TEST_TAG = "CurrentTilesGrid"
+ private const val AVAILABLE_TILES_GRID_TEST_TAG = "AvailableTilesGrid"
+
+ private fun List<SizedTile<EditTileViewModel>>.add(
+ spec: TileSpec
+ ): List<SizedTile<EditTileViewModel>> {
+ return map {
+ if (it.tile.tileSpec == spec) {
+ createEditTile(it.tile.tileSpec.spec)
+ } else {
+ it
+ }
+ }
+ }
+
+ private fun List<SizedTile<EditTileViewModel>>.remove(
+ spec: TileSpec
+ ): List<SizedTile<EditTileViewModel>> {
+ return map {
+ if (it.tile.tileSpec == spec) {
+ createEditTile(it.tile.tileSpec.spec, isCurrent = false)
+ } else {
+ it
+ }
+ }
+ }
+
+ private fun createEditTile(
+ tileSpec: String,
+ isCurrent: Boolean = true,
+ ): SizedTile<EditTileViewModel> {
+ return SizedTileImpl(
+ EditTileViewModel(
+ tileSpec = TileSpec.create(tileSpec),
+ icon =
+ Icon.Resource(
+ android.R.drawable.star_on,
+ ContentDescription.Loaded(tileSpec),
+ ),
+ label = AnnotatedString(tileSpec),
+ appName = null,
+ isCurrent = isCurrent,
+ availableEditActions = emptySet(),
+ category = TileCategory.UNKNOWN,
+ ),
+ getWidth(tileSpec),
+ )
+ }
+
+ private fun getWidth(tileSpec: String): Int {
+ return if (tileSpec.endsWith("large")) {
+ 2
+ } else {
+ 1
+ }
+ }
+
+ private val TestEditTiles =
+ listOf(
+ createEditTile("tileA"),
+ createEditTile("tileB"),
+ createEditTile("tileC"),
+ createEditTile("tileD_large"),
+ createEditTile("tileE"),
+ createEditTile("tileF", isCurrent = false),
+ createEditTile("tileG_large", isCurrent = false),
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
index f23553eda3b2..a0be02f1ef7e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
@@ -65,6 +65,7 @@ class ResizingTest : SysuiTestCase() {
columns = 4,
largeTilesSpan = 4,
modifier = Modifier.fillMaxSize(),
+ onAddTile = {},
onRemoveTile = {},
onSetTiles = {},
onResize = onResize,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
index 330b887b70a3..1305b0c4c499 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt
@@ -238,7 +238,8 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() {
tile.handleClick(null)
- verify(bluetoothTileDialogViewModel).showDialog(null)
+ verify(bluetoothTileDialogViewModel)
+ .showDetailsContent(/* expandable= */ null, /* view= */ null)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
index c8faa81adffa..cf54df8565d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/reardisplay/RearDisplayCoreStartableTest.kt
@@ -45,7 +45,6 @@ import org.mockito.kotlin.whenever
/** atest SystemUITests:com.android.systemui.reardisplay.RearDisplayCoreStartableTest */
@SmallTest
-@kotlinx.coroutines.ExperimentalCoroutinesApi
class RearDisplayCoreStartableTest : SysuiTestCase() {
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
index f695c13a9e62..1474defa1662 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt
@@ -38,7 +38,6 @@ import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.TruthJUnit.assume
import java.util.concurrent.Executor
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
@@ -58,7 +57,6 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(Parameterized::class)
class UserTrackerImplTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index 6724f82dfd99..732561e0979b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -69,7 +69,6 @@ import com.android.systemui.statusbar.lockscreen.lockscreenSmartspaceController
import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
@@ -87,7 +86,6 @@ import org.mockito.kotlin.spy
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
-@ExperimentalCoroutinesApi
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 49d6909c1f93..856ece7e7ff3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -89,7 +89,6 @@ import com.android.systemui.window.ui.viewmodel.WindowRootViewModel
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.TestScope
@@ -116,7 +115,6 @@ import org.mockito.kotlin.clearInvocations
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
@RunWithLooper(setAsMainLooper = true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
index 9abe9aa5e598..3de7db76deae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
@@ -16,6 +16,8 @@
package com.android.systemui.shade
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
@@ -26,6 +28,7 @@ import androidx.annotation.IdRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.fragments.FragmentHostManager
import com.android.systemui.fragments.FragmentService
@@ -122,6 +125,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() {
overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN)
overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN)
+ overrideResource(R.dimen.notification_2025_panel_margin_bottom, NOTIFICATIONS_MARGIN)
overrideResource(R.bool.config_use_split_notification_shade, false)
overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING)
overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET)
@@ -360,6 +364,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() {
}
@Test
+ @DisableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW)
fun testNotificationsMarginBottomIsUpdated() {
Mockito.clearInvocations(view)
enableSplitShade()
@@ -371,6 +376,18 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW)
+ fun testNotificationsMarginBottomIsUpdated_footerRedesign() {
+ Mockito.clearInvocations(view)
+ enableSplitShade()
+ verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN)
+
+ overrideResource(R.dimen.notification_2025_panel_margin_bottom, 100)
+ disableSplitShade()
+ verify(view).setNotificationsMarginBottom(100)
+ }
+
+ @Test
fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() {
enableSplitShade()
underTest.updateResources()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
index 4c12cc886e33..552fe8d5bc55 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt
@@ -16,6 +16,8 @@
package com.android.systemui.shade
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
@@ -26,6 +28,7 @@ import androidx.annotation.IdRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.fragments.FragmentHostManager
import com.android.systemui.fragments.FragmentService
@@ -122,6 +125,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() {
overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN)
overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN)
+ overrideResource(R.dimen.notification_2025_panel_margin_bottom, NOTIFICATIONS_MARGIN)
overrideResource(R.bool.config_use_split_notification_shade, false)
overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING)
overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET)
@@ -358,6 +362,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() {
}
@Test
+ @DisableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW)
fun testNotificationsMarginBottomIsUpdated() {
Mockito.clearInvocations(view)
enableSplitShade()
@@ -369,6 +374,18 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_NOTIFICATIONS_REDESIGN_FOOTER_VIEW)
+ fun testNotificationsMarginBottomIsUpdated_footerRedesign() {
+ Mockito.clearInvocations(view)
+ enableSplitShade()
+ verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN)
+
+ overrideResource(R.dimen.notification_2025_panel_margin_bottom, 100)
+ disableSplitShade()
+ verify(view).setNotificationsMarginBottom(100)
+ }
+
+ @Test
fun testSplitShadeLayout_isAlignedToGuideline() {
enableSplitShade()
underTest.updateResources()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index cfc00a918f61..b7040ee2a11e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -35,7 +35,6 @@ import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.After
@@ -69,7 +68,6 @@ private fun <T> anyObject(): T {
@SmallTest
@RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidJUnit4::class)
-@OptIn(ExperimentalCoroutinesApi::class)
class LockscreenShadeTransitionControllerTest : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerTest.kt
index 98487f7ac059..f8154ddb2a5f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerTest.kt
@@ -3,7 +3,6 @@ package com.android.systemui.statusbar
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.qs.QS
@@ -30,7 +29,6 @@ class SplitShadeLockScreenOverScrollerTest : SysuiTestCase() {
private val configurationController = FakeConfigurationController()
- @OptIn(ExperimentalCoroutinesApi::class)
@Mock private lateinit var scrimController: ScrimController
@Mock private lateinit var statusBarStateController: SysuiStatusBarStateController
private var qS: QS? = null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
index 77ca51c5efcb..3c4aeccec2eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
@@ -41,7 +41,6 @@ import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
import com.android.systemui.util.time.FakeSystemClock
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
@@ -64,7 +63,6 @@ import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
@RunWithLooper(setAsMainLooper = true)
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
index c4ef4f978ff8..a16f2f6161ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/IconManagerTest.kt
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
package com.android.systemui.statusbar.notification.icon
import android.app.ActivityManager
@@ -43,7 +41,6 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
index 8645a40319f4..7cc1ac1d3806 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
@@ -75,7 +75,6 @@ import com.android.systemui.util.kotlin.JavaAdapter
import com.android.systemui.wmshell.BubblesManager
import java.util.Optional
import kotlin.test.assertEquals
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
import org.junit.Assert
@@ -98,7 +97,6 @@ import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
/** Tests for [NotificationGutsManager] with the scene container enabled. */
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelImplTest.kt
index 8beed01ffbe4..f4c0e367871f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/airplane/ui/viewmodel/AirplaneModeViewModelImplTest.kt
@@ -29,7 +29,6 @@ import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
@@ -39,7 +38,6 @@ import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
@SmallTest
-@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@RunWith(AndroidJUnit4::class)
class AirplaneModeViewModelImplTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfigTest.kt
index 4a2f6f281566..df74404aaaf8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfigTest.kt
@@ -25,12 +25,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class SystemUiCarrierConfigTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryImplTest.kt
index 7d101be4e748..d074fc256133 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryImplTest.kt
@@ -29,7 +29,6 @@ import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger
import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest.Companion.createTestConfig
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
@@ -45,7 +44,6 @@ import org.mockito.MockitoAnnotations
import org.mockito.MockitoSession
import org.mockito.quality.Strictness
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class CarrierConfigRepositoryImplTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
index 36f5236c3936..d05590521fab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
@@ -50,7 +50,6 @@ import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
@@ -70,7 +69,6 @@ import org.mockito.MockitoAnnotations
* switches over when the value of `demoMode` changes
*/
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class MobileRepositorySwitcherTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
index fd23655ffc1c..02ad90cb4269 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
@@ -50,7 +50,6 @@ import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -69,7 +68,6 @@ import org.mockito.Mockito.verify
* properly switches over when the value of `isCarrierMerged` changes.
*/
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class FullMobileConnectionRepositoryTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 3a25ecb27404..df5c6e916931 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -98,7 +98,6 @@ import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -115,7 +114,6 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class MobileConnectionRepositoryTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt
index 0d82c79fea79..ec260fcc7a65 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionTelephonySmokeTests.kt
@@ -49,7 +49,6 @@ import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityMod
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -92,7 +91,6 @@ import org.mockito.MockitoAnnotations
* 5. Assert that B has the state sent in step #2
*/
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class MobileConnectionTelephonySmokeTests : SysuiTestCase() {
private lateinit var underTest: MobileConnectionRepositoryImpl
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 6c60f55a3904..d1d6e27332b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -79,7 +79,6 @@ import com.android.wifitrackerlib.WifiEntry
import com.android.wifitrackerlib.WifiPickerTracker
import com.google.common.truth.Truth.assertThat
import java.util.UUID
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -101,7 +100,6 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
// This is required because our [SubscriptionManager.OnSubscriptionsChangedListener] uses a looper
// to run the callback and this makes the looper place nicely with TestScope etc.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt
index 84846a16f39a..ce99e595504d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt
@@ -44,7 +44,6 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnec
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
@@ -57,7 +56,6 @@ import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper(setAsMainLooper = true)
-@OptIn(ExperimentalCoroutinesApi::class)
class ModernStatusBarMobileViewTest : SysuiTestCase() {
private val kosmos = testKosmos()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 599729d953d4..6a8b783acf73 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -53,7 +53,6 @@ import com.google.common.truth.Truth.assertThat
import java.util.Optional
import java.util.function.Consumer
import kotlin.test.Test
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
@@ -71,7 +70,6 @@ import org.mockito.MockitoAnnotations
import org.mockito.kotlin.doThrow
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
index 88f262bec123..8a0159ce174e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
@@ -47,7 +47,6 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.Connectivi
import com.android.systemui.testKosmos
import com.android.systemui.tuner.TunerService
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -60,7 +59,6 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class ConnectivityRepositoryImplTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 5d88f72b805b..097f3929db42 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -26,8 +26,6 @@ import static android.service.notification.NotificationListenerService.REASON_AP
import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED;
-import static androidx.test.ext.truth.content.IntentSubject.assertThat;
-
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING;
import static com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR;
@@ -175,6 +173,7 @@ import com.android.wm.shell.bubbles.BubbleEducationController;
import com.android.wm.shell.bubbles.BubbleEntry;
import com.android.wm.shell.bubbles.BubbleLogger;
import com.android.wm.shell.bubbles.BubbleOverflow;
+import com.android.wm.shell.bubbles.BubbleResizabilityChecker;
import com.android.wm.shell.bubbles.BubbleStackView;
import com.android.wm.shell.bubbles.BubbleTaskView;
import com.android.wm.shell.bubbles.BubbleViewInfoTask;
@@ -182,7 +181,6 @@ import com.android.wm.shell.bubbles.BubbleViewProvider;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.bubbles.StackEducationView;
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
-import com.android.wm.shell.bubbles.properties.BubbleProperties;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
@@ -297,7 +295,7 @@ public class BubblesTest extends SysuiTestCase {
private BubbleEntry mBubbleEntryUser11;
private BubbleEntry mBubbleEntry2User11;
- private Intent mAppBubbleIntent;
+ private Intent mNotesBubbleIntent;
@Mock
private ShellInit mShellInit;
@@ -354,7 +352,7 @@ public class BubblesTest extends SysuiTestCase {
@Mock
private NotifPipelineFlags mNotifPipelineFlags;
@Mock
- private Icon mAppBubbleIcon;
+ private Icon mNotesBubbleIcon;
@Mock
private Display mDefaultDisplay;
@Mock
@@ -380,8 +378,6 @@ public class BubblesTest extends SysuiTestCase {
private UserHandle mUser0;
- private FakeBubbleProperties mBubbleProperties;
-
@Parameters(name = "{0}")
public static List<FlagsParameterization> getParams() {
return SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag();
@@ -457,8 +453,8 @@ public class BubblesTest extends SysuiTestCase {
mNotificationShadeWindowController.fetchWindowRootView();
mNotificationShadeWindowController.attach();
- mAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class);
- mAppBubbleIntent.setPackage(mContext.getPackageName());
+ mNotesBubbleIntent = new Intent(mContext, BubblesTestActivity.class);
+ mNotesBubbleIntent.setPackage(mContext.getPackageName());
mZenModeConfig.suppressedVisualEffects = 0;
when(mZenModeController.getConfig()).thenReturn(mZenModeConfig);
@@ -522,7 +518,6 @@ public class BubblesTest extends SysuiTestCase {
mTaskViewRepository = new TaskViewRepository();
mTaskViewTransitions = new TaskViewTransitions(mTransitions, mTaskViewRepository,
mShellTaskOrganizer, mSyncQueue);
- mBubbleProperties = new FakeBubbleProperties();
mBubbleController = new TestableBubbleController(
mContext,
mShellInit,
@@ -551,7 +546,7 @@ public class BubblesTest extends SysuiTestCase {
mTransitions,
mock(SyncTransactionQueue.class),
mock(IWindowManager.class),
- mBubbleProperties);
+ new BubbleResizabilityChecker());
mBubbleController.setExpandListener(mBubbleExpandListener);
spyOn(mBubbleController);
@@ -1478,8 +1473,8 @@ public class BubblesTest extends SysuiTestCase {
}
@Test
- public void testShowManageMenuChangesSysuiState_appBubble() {
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+ public void testShowManageMenuChangesSysuiState_notesBubble() {
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
assertTrue(mBubbleController.hasBubbles());
// Expand the stack
@@ -1979,79 +1974,80 @@ public class BubblesTest extends SysuiTestCase {
}
@Test
- public void testShowOrHideAppBubble_addsAndExpand() {
+ public void testShowOrHideNotesBubble_addsAndExpand() {
assertThat(mBubbleController.isStackExpanded()).isFalse();
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
verify(mBubbleController).inflateAndAdd(any(Bubble.class), /* suppressFlyout= */ eq(true),
/* showInShade= */ eq(false));
assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(
- Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0));
+ Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0));
assertThat(mBubbleController.isStackExpanded()).isTrue();
}
@Test
- public void testShowOrHideAppBubble_expandIfCollapsed() {
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+ public void testShowOrHideNotesBubble_expandIfCollapsed() {
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
mBubbleController.updateBubble(mBubbleEntry);
mBubbleController.collapseStack();
assertThat(mBubbleController.isStackExpanded()).isFalse();
// Calling this while collapsed will expand the app bubble
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(
- Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0));
+ Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0));
assertThat(mBubbleController.isStackExpanded()).isTrue();
assertThat(mBubbleData.getBubbles().size()).isEqualTo(2);
}
@Test
- public void testShowOrHideAppBubble_collapseIfSelected() {
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+ public void testShowOrHideNotesBubble_collapseIfSelected() {
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(
- Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0));
+ Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0));
assertThat(mBubbleController.isStackExpanded()).isTrue();
// Calling this while the app bubble is expanded should collapse the stack
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(
- Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0));
+ Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0));
assertThat(mBubbleController.isStackExpanded()).isFalse();
assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(mUser0);
}
@Test
- public void testShowOrHideAppBubbleWithNonPrimaryUser_bubbleCollapsedWithExpectedUser() {
+ public void testShowOrHideNotesBubbleWithNonPrimaryUser_bubbleCollapsedWithExpectedUser() {
UserHandle user10 = createUserHandle(/* userId = */ 10);
- String appBubbleKey = Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), user10);
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, user10, mAppBubbleIcon);
- assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey);
+ String notesKey = Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), user10);
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, user10, mNotesBubbleIcon);
+ assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(notesKey);
assertThat(mBubbleController.isStackExpanded()).isTrue();
assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(user10);
// Calling this while the app bubble is expanded should collapse the stack
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, user10, mAppBubbleIcon);
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, user10, mNotesBubbleIcon);
- assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey);
+ assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(notesKey);
assertThat(mBubbleController.isStackExpanded()).isFalse();
assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(user10);
}
@Test
- public void testShowOrHideAppBubbleOnUser10AndThenUser0_user0BubbleExpanded() {
+ public void testShowOrHideNotesBubbleOnUser10AndThenUser0_user0BubbleExpanded() {
UserHandle user10 = createUserHandle(/* userId = */ 10);
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, user10, mAppBubbleIcon);
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, user10, mNotesBubbleIcon);
- String appBubbleUser0Key = Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0);
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+ String notesBubbleUser0Key = Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(),
+ mUser0);
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
- assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleUser0Key);
+ assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(notesBubbleUser0Key);
assertThat(mBubbleController.isStackExpanded()).isTrue();
assertThat(mBubbleData.getBubbles()).hasSize(2);
assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(mUser0);
@@ -2059,63 +2055,64 @@ public class BubblesTest extends SysuiTestCase {
}
@Test
- public void testShowOrHideAppBubble_selectIfNotSelected() {
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+ public void testShowOrHideNotesBubble_selectIfNotSelected() {
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
mBubbleController.updateBubble(mBubbleEntry);
mBubbleController.expandStackAndSelectBubble(mBubbleEntry);
assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(mBubbleEntry.getKey());
assertThat(mBubbleController.isStackExpanded()).isTrue();
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(
- Bubble.getAppBubbleKeyForApp(mContext.getPackageName(), mUser0));
+ Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0));
assertThat(mBubbleController.isStackExpanded()).isTrue();
assertThat(mBubbleData.getBubbles().size()).isEqualTo(2);
}
@Test
- public void testShowOrHideAppBubble_addsFromOverflow() {
- String appBubbleKey = Bubble.getAppBubbleKeyForApp(mAppBubbleIntent.getPackage(), mUser0);
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
-
+ public void testShowOrHideNotesBubble_addsFromOverflow() {
+ String noteBubbleKey = Bubble.getNoteBubbleKeyForApp(mNotesBubbleIntent.getPackage(),
+ mUser0);
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
// Collapse the stack so we don't need to wait for the dismiss animation in the test
mBubbleController.collapseStack();
// Dismiss the app bubble so it's in the overflow
- mBubbleController.dismissBubble(appBubbleKey, Bubbles.DISMISS_USER_GESTURE);
- assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNotNull();
+ mBubbleController.dismissBubble(noteBubbleKey, Bubbles.DISMISS_USER_GESTURE);
+ assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNotNull();
// Calling this while collapsed will re-add and expand the app bubble
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
- assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey);
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
+ assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(noteBubbleKey);
assertThat(mBubbleController.isStackExpanded()).isTrue();
assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
- assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNull();
+ assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNull();
}
@Test
- public void testShowOrHideAppBubble_updateExistedBubbleInOverflow_updateIntentInBubble() {
- String appBubbleKey = Bubble.getAppBubbleKeyForApp(mAppBubbleIntent.getPackage(), mUser0);
- mBubbleController.showOrHideAppBubble(mAppBubbleIntent, mUser0, mAppBubbleIcon);
+ public void testShowOrHideNotesBubble_updateExistedBubbleInOverflow_updateIntentInBubble() {
+ String noteBubbleKey = Bubble.getNoteBubbleKeyForApp(mNotesBubbleIntent.getPackage(),
+ mUser0);
+ mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
// Collapse the stack so we don't need to wait for the dismiss animation in the test
mBubbleController.collapseStack();
// Dismiss the app bubble so it's in the overflow
- mBubbleController.dismissBubble(appBubbleKey, Bubbles.DISMISS_USER_GESTURE);
- assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNotNull();
+ mBubbleController.dismissBubble(noteBubbleKey, Bubbles.DISMISS_USER_GESTURE);
+ assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNotNull();
// Modify the intent to include new extras.
- Intent newAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class)
+ Intent newIntent = new Intent(mContext, BubblesTestActivity.class)
.setPackage(mContext.getPackageName())
.putExtra("hello", "world");
// Calling this while collapsed will re-add and expand the app bubble
- mBubbleController.showOrHideAppBubble(newAppBubbleIntent, mUser0, mAppBubbleIcon);
- assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(appBubbleKey);
+ mBubbleController.showOrHideNotesBubble(newIntent, mUser0, mNotesBubbleIcon);
+ assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(noteBubbleKey);
assertThat(mBubbleController.isStackExpanded()).isTrue();
assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
- assertThat(mBubbleData.getBubbles().get(0).getAppBubbleIntent()).extras().string(
- "hello").isEqualTo("world");
- assertThat(mBubbleData.getOverflowBubbleWithKey(appBubbleKey)).isNull();
+ assertThat(mBubbleData.getBubbles().get(0).getAppBubbleIntent()
+ .getStringExtra("hello")).isEqualTo("world");
+ assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNull();
}
@Test
@@ -2143,9 +2140,9 @@ public class BubblesTest extends SysuiTestCase {
assertFalse("FLAG_NO_DISMISS Notifs should be non-dismissable", bubble.isDismissable());
}
+ @DisableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void registerBubbleBarListener_barDisabled_largeScreen_shouldBeIgnored() {
- mBubbleProperties.mIsBubbleBarEnabled = false;
mPositioner.setIsLargeScreen(true);
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
@@ -2161,9 +2158,9 @@ public class BubblesTest extends SysuiTestCase {
assertThat(mBubbleController.getStackView().getBubbleCount()).isEqualTo(1);
}
+ @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void registerBubbleBarListener_barEnabled_smallScreen_shouldBeIgnored() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(false);
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
@@ -2179,9 +2176,9 @@ public class BubblesTest extends SysuiTestCase {
assertThat(mBubbleController.getStackView().getBubbleCount()).isEqualTo(1);
}
+ @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void registerBubbleBarListener_switchToBarAndBackToStack() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
@@ -2211,9 +2208,9 @@ public class BubblesTest extends SysuiTestCase {
assertBubbleIsInflatedForStack(mBubbleData.getOverflow());
}
+ @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void registerBubbleBarListener_switchToBarWhileExpanded() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
mEntryListener.onEntryAdded(mRow);
@@ -2238,9 +2235,9 @@ public class BubblesTest extends SysuiTestCase {
assertThat(layerView.isExpanded()).isTrue();
}
+ @DisableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void switchBetweenBarAndStack_noBubbles_shouldBeIgnored() {
- mBubbleProperties.mIsBubbleBarEnabled = false;
mPositioner.setIsLargeScreen(true);
assertFalse(mBubbleController.hasBubbles());
@@ -2256,9 +2253,9 @@ public class BubblesTest extends SysuiTestCase {
assertNoBubbleContainerViews();
}
+ @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void bubbleBarBubbleExpandedAndCollapsed() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
mEntryListener.onEntryAdded(mRow);
mBubbleController.updateBubble(mBubbleEntry);
@@ -2277,7 +2274,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void dragBubbleBarBubble_selectedBubble_expandedViewCollapsesDuringDrag() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
@@ -2306,7 +2302,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void dragBubbleBarBubble_unselectedBubble_expandedViewCollapsesDuringDrag() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
@@ -2335,7 +2330,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void dismissBubbleBarBubble_selected_selectsAndExpandsNext() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
@@ -2359,7 +2353,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void dismissBubbleBarBubble_unselected_selectionDoesNotChange() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
@@ -2407,9 +2400,9 @@ public class BubblesTest extends SysuiTestCase {
verify(mBubbleController).onSensitiveNotificationProtectionStateChanged(false);
}
+ @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void setBubbleBarLocation_listenerNotified() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
@@ -2421,9 +2414,9 @@ public class BubblesTest extends SysuiTestCase {
BubbleBarLocation.LEFT);
}
+ @DisableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void setBubbleBarLocation_barDisabled_shouldBeIgnored() {
- mBubbleProperties.mIsBubbleBarEnabled = false;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
@@ -2496,7 +2489,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void testEventLogging_bubbleBar_addBubble() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
@@ -2510,7 +2502,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void testEventLogging_bubbleBar_updateBubble() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
@@ -2527,7 +2518,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void testEventLogging_bubbleBar_dragSelectedBubbleToDismiss() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
@@ -2554,7 +2544,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void testEventLogging_bubbleBar_dragOtherBubbleToDismiss() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
@@ -2579,7 +2568,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void testEventLogging_bubbleBar_dragBarToDismiss() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
// Not a user gesture, should not log an event
@@ -2594,7 +2582,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void testEventLogging_bubbleBar_expandAndCollapse() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
@@ -2614,7 +2601,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void testEventLogging_bubbleBar_autoExpandingBubble() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
@@ -2630,7 +2616,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void testEventLogging_bubbleBar_switchBubble() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
@@ -2656,7 +2641,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void testEventLogging_bubbleBar_openOverflow() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
@@ -2672,7 +2656,6 @@ public class BubblesTest extends SysuiTestCase {
@EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
@Test
public void testEventLogging_bubbleBar_fromOverflowToBar() {
- mBubbleProperties.mIsBubbleBarEnabled = true;
mPositioner.setIsLargeScreen(true);
FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
mBubbleController.registerBubbleStateListener(bubbleStateListener);
@@ -2902,16 +2885,4 @@ public class BubblesTest extends SysuiTestCase {
@Override
public void onItemDraggedOutsideBubbleBarDropZone() {}
}
-
- private static class FakeBubbleProperties implements BubbleProperties {
- boolean mIsBubbleBarEnabled = false;
-
- @Override
- public boolean isBubbleBarEnabled() {
- return mIsBubbleBarEnabled;
- }
-
- @Override
- public void refresh() {}
- }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelKosmos.kt
index 35e85bb1e68d..b6f8ec666001 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModelKosmos.kt
@@ -19,6 +19,7 @@ package com.android.systemui.brightness.ui.viewmodel
import com.android.systemui.brightness.domain.interactor.brightnessPolicyEnforcementInteractor
import com.android.systemui.brightness.domain.interactor.screenBrightnessInteractor
import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.graphics.imageLoader
import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor
@@ -36,6 +37,7 @@ val Kosmos.brightnessSliderViewModelFactory: BrightnessSliderViewModel.Factory b
supportsMirroring = allowsMirroring,
falsingInteractor = falsingInteractor,
brightnessWarningToast = brightnessWarningToast,
+ imageLoader = imageLoader,
)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
index b3c1411243c1..3f35bb9f3520 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
@@ -17,8 +17,7 @@ import kotlinx.coroutines.launch
/** Fake implementation of [CommunalSceneRepository]. */
class FakeCommunalSceneRepository(
private val applicationScope: CoroutineScope,
- override val currentScene: MutableStateFlow<SceneKey> =
- MutableStateFlow(CommunalScenes.Default),
+ override val currentScene: MutableStateFlow<SceneKey> = MutableStateFlow(CommunalScenes.Default),
) : CommunalSceneRepository {
override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) =
@@ -31,6 +30,10 @@ class FakeCommunalSceneRepository(
}
}
+ override suspend fun showHubFromPowerButton() {
+ snapToScene(CommunalScenes.Communal)
+ }
+
private val defaultTransitionState = ObservableTransitionState.Idle(CommunalScenes.Default)
private val _transitionState = MutableStateFlow<Flow<ObservableTransitionState>?>(null)
override val transitionState: StateFlow<ObservableTransitionState> =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
index 2e6d8ed5aa5b..4f024d7509ba 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
@@ -19,7 +19,6 @@ package com.android.systemui.plugins.statusbar
import com.android.internal.logging.uiEventLogger
import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
-import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
@@ -37,7 +36,6 @@ var Kosmos.statusBarStateController: SysuiStatusBarStateController by
Kosmos.Fixture {
StatusBarStateControllerImpl(
uiEventLogger,
- { interactionJankMonitor },
mock(),
{ keyguardInteractor },
{ keyguardTransitionInteractor },
diff --git a/packages/CrashRecovery/framework/java/android/service/watchdog/IExplicitHealthCheckService.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsContent.kt
index 90965092ac2b..84a736439e08 100644
--- a/packages/CrashRecovery/framework/java/android/service/watchdog/IExplicitHealthCheckService.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsContent.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2025 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,19 +14,18 @@
* limitations under the License.
*/
-package android.service.watchdog;
+package com.android.systemui.qs
-import android.os.RemoteCallback;
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
-/**
- * @hide
- */
-@PermissionManuallyEnforced
-oneway interface IExplicitHealthCheckService
-{
- void setCallback(in @nullable RemoteCallback callback);
- void request(String packageName);
- void cancel(String packageName);
- void getSupportedPackages(in RemoteCallback callback);
- void getRequestedPackages(in RemoteCallback callback);
+@Composable
+fun FakeTileDetailsContent() {
+ Text(
+ text = "Fake details content",
+ textAlign = TextAlign.Center,
+ fontWeight = FontWeight.ExtraBold,
+ )
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt
index 555f019822c2..4f8d5a14e390 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt
@@ -16,24 +16,11 @@
package com.android.systemui.qs
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
import com.android.systemui.plugins.qs.TileDetailsViewModel
class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel() {
private var _clickOnSettingsButton = 0
- @Composable
- override fun GetContentView() {
- Text(
- text = "Fake details content",
- textAlign = TextAlign.Center,
- fontWeight = FontWeight.ExtraBold,
- )
- }
-
override fun clickOnSettingsButton() {
_clickOnSettingsButton++
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
index 65e580cafcb5..583a9def8094 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt
@@ -33,6 +33,7 @@ import com.android.systemui.qs.footerActionsViewModelFactory
import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor
import com.android.systemui.qs.panels.ui.viewmodel.inFirstPageViewModel
import com.android.systemui.qs.panels.ui.viewmodel.mediaInRowInLandscapeViewModelFactory
+import com.android.systemui.qs.panels.ui.viewmodel.quickQuickSettingsViewModelFactory
import com.android.systemui.qs.ui.viewmodel.quickSettingsContainerViewModelFactory
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.largeScreenHeaderHelper
@@ -48,6 +49,7 @@ val Kosmos.qsFragmentComposeViewModelFactory by
): QSFragmentComposeViewModel {
return QSFragmentComposeViewModel(
quickSettingsContainerViewModelFactory,
+ quickQuickSettingsViewModelFactory,
mainResources,
footerActionsViewModelFactory,
footerActionsController,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt
index f8fa5db4ddf7..3fc73cbc5552 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt
@@ -20,9 +20,9 @@ import com.android.systemui.brightness.ui.viewmodel.brightnessSliderViewModelFac
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.panels.ui.viewmodel.detailsViewModel
import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel
-import com.android.systemui.qs.panels.ui.viewmodel.quickQuickSettingsViewModelFactory
import com.android.systemui.qs.panels.ui.viewmodel.tileGridViewModel
import com.android.systemui.qs.panels.ui.viewmodel.toolbar.toolbarViewModelFactory
+import com.android.systemui.shade.domain.interactor.shadeModeInteractor
import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory
val Kosmos.quickSettingsContainerViewModelFactory by
@@ -33,13 +33,13 @@ val Kosmos.quickSettingsContainerViewModelFactory by
): QuickSettingsContainerViewModel {
return QuickSettingsContainerViewModel(
brightnessSliderViewModelFactory,
- quickQuickSettingsViewModelFactory,
shadeHeaderViewModelFactory,
supportsBrightnessMirroring,
tileGridViewModel,
editModeViewModel,
detailsViewModel,
toolbarViewModelFactory,
+ shadeModeInteractor,
)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt
index 4f3b8f3541e1..108a20ab73d6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeOverlayContentViewModelKosmos.kt
@@ -19,7 +19,6 @@ package com.android.systemui.qs.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
-import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
val Kosmos.quickSettingsShadeOverlayContentViewModel: QuickSettingsShadeOverlayContentViewModel by
@@ -28,7 +27,5 @@ val Kosmos.quickSettingsShadeOverlayContentViewModel: QuickSettingsShadeOverlayC
shadeInteractor = shadeInteractor,
sceneInteractor = sceneInteractor,
notificationStackAppearanceInteractor = notificationStackAppearanceInteractor,
- shadeHeaderViewModelFactory = shadeHeaderViewModelFactory,
- quickSettingsContainerViewModelFactory = quickSettingsContainerViewModelFactory,
)
}
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 609f97d0b249..ae4e8d275341 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
@@ -4,6 +4,7 @@ import android.view.View
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.lightRevealScrimViewModel
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
@@ -100,6 +101,7 @@ val Kosmos.sceneContainerViewModelFactory by Fixture {
motionEventHandlerReceiver = motionEventHandlerReceiver,
lightRevealScrim = lightRevealScrimViewModel,
wallpaperViewModel = wallpaperViewModel,
+ keyguardInteractor = keyguardInteractor,
)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
index 65bfafbfa9b0..7a9b052481cb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
@@ -33,6 +33,7 @@ import com.android.systemui.haptics.vibratorHelper
import com.android.systemui.keyguard.dismissCallbackRegistry
import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.trustInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testScope
@@ -91,5 +92,6 @@ val Kosmos.sceneContainerStartable by Fixture {
activityTransitionAnimator = activityTransitionAnimator,
shadeModeInteractor = shadeModeInteractor,
tableLogBuffer = logcatTableLogBuffer(this, "sceneFrameworkTableLogBuffer"),
+ trustInteractor = trustInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
index 7eb9f3472482..2be8acb845b9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.shade.ui.viewmodel
import android.content.applicationContext
+import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.plugins.activityStarter
@@ -24,8 +25,12 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.privacyChipInteractor
import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder
+import com.android.systemui.statusbar.phone.ui.StatusBarIconController
+import com.android.systemui.statusbar.phone.ui.TintedIconManager
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.mobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.mobileIconsViewModel
+import org.mockito.kotlin.mock
val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by
Kosmos.Fixture {
@@ -38,6 +43,11 @@ val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by
mobileIconsViewModel = mobileIconsViewModel,
privacyChipInteractor = privacyChipInteractor,
clockInteractor = shadeHeaderClockInteractor,
+ tintedIconManagerFactory = mock<TintedIconManager.Factory>(),
+ batteryMeterViewControllerFactory = mock<BatteryMeterViewController.Factory>(),
+ statusBarIconController = mock<StatusBarIconController>(),
+ notificationIconContainerStatusBarViewBinder =
+ mock<NotificationIconContainerStatusBarViewBinder>(),
broadcastDispatcher = broadcastDispatcher,
)
}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
index 3cb6c5a6bd16..7af03ed2e6c8 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -44,6 +44,7 @@ import android.app.UiAutomation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
+import android.icu.util.ULocale;
import android.os.Binder;
import android.os.Build;
import android.os.Build.VERSION_CODES;
@@ -81,6 +82,7 @@ import java.io.IOException;
import java.io.PrintStream;
import java.util.Collections;
import java.util.HashMap;
+import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
@@ -228,6 +230,9 @@ public class RavenwoodRuntimeEnvironmentController {
RuntimeInit.redirectLogStreams();
dumpCommandLineArgs();
+ dumpEnvironment();
+ dumpJavaProperties();
+ dumpOtherInfo();
// We haven't initialized liblog yet, so directly write to System.out here.
RavenwoodCommonUtils.log(TAG, "globalInitInner()");
@@ -564,4 +569,34 @@ public class RavenwoodRuntimeEnvironmentController {
Log.v(TAG, " " + arg);
}
}
+
+ private static void dumpJavaProperties() {
+ Log.v(TAG, "JVM properties:");
+ dumpMap(System.getProperties());
+ }
+
+ private static void dumpEnvironment() {
+ Log.v(TAG, "Environment:");
+ dumpMap(System.getenv());
+ }
+
+ private static void dumpMap(Map<?, ?> map) {
+ for (var key : map.keySet().stream().sorted().toList()) {
+ Log.v(TAG, " " + key + "=" + map.get(key));
+ }
+ }
+
+ private static void dumpOtherInfo() {
+ Log.v(TAG, "Other key information:");
+ var jloc = Locale.getDefault();
+ Log.v(TAG, " java.util.Locale=" + jloc + " / " + jloc.toLanguageTag());
+ var uloc = ULocale.getDefault();
+ Log.v(TAG, " android.icu.util.ULocale=" + uloc + " / " + uloc.toLanguageTag());
+
+ var jtz = java.util.TimeZone.getDefault();
+ Log.v(TAG, " java.util.TimeZone=" + jtz.getDisplayName() + " / " + jtz);
+
+ var itz = android.icu.util.TimeZone.getDefault();
+ Log.v(TAG, " android.icu.util.TimeZone=" + itz.getDisplayName() + " / " + itz);
+ }
}
diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
index 383e75bb5122..e202d0ecfa23 100644
--- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
@@ -151,6 +151,10 @@ android.os.Looper
android.os.Message
android.os.MessageQueue
android.os.MessageQueue_ravenwood
+android.os.PerfettoTrace
+android.os.PerfettoTrace$Category
+android.os.PerfettoTrackEventExtra
+android.os.PerfettoTrackEventExtra$NoOpBuilder
android.os.PackageTagsList
android.os.Parcel
android.os.ParcelFileDescriptor
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
index 1bc9c783df76..8e448676c214 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -81,6 +81,7 @@ public class AutoclickController extends BaseEventStreamTransformation {
@VisibleForTesting AutoclickSettingsObserver mAutoclickSettingsObserver;
@VisibleForTesting AutoclickIndicatorScheduler mAutoclickIndicatorScheduler;
@VisibleForTesting AutoclickIndicatorView mAutoclickIndicatorView;
+ @VisibleForTesting AutoclickTypePanel mAutoclickTypePanel;
private WindowManager mWindowManager;
public AutoclickController(Context context, int userId, AccessibilityTraceManager trace) {
@@ -123,6 +124,9 @@ public class AutoclickController extends BaseEventStreamTransformation {
mAutoclickIndicatorView = new AutoclickIndicatorView(mContext);
mWindowManager = mContext.getSystemService(WindowManager.class);
+ mAutoclickTypePanel = new AutoclickTypePanel(mContext, mWindowManager);
+
+ mAutoclickTypePanel.show();
mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams());
}
@@ -167,6 +171,7 @@ public class AutoclickController extends BaseEventStreamTransformation {
mAutoclickIndicatorScheduler.cancel();
mAutoclickIndicatorScheduler = null;
mWindowManager.removeView(mAutoclickIndicatorView);
+ mAutoclickTypePanel.hide();
}
}
@@ -310,7 +315,7 @@ public class AutoclickController extends BaseEventStreamTransformation {
if (mAutoclickIndicatorScheduler != null) {
mAutoclickIndicatorScheduler.updateCursorAreaSize(size);
}
- mClickScheduler.updateMovementSlope(size);
+ mClickScheduler.updateMovementSlop(size);
}
if (mAutoclickIgnoreMinorCursorMovementSettingUri.equals(uri)) {
@@ -400,9 +405,9 @@ public class AutoclickController extends BaseEventStreamTransformation {
* to be discarded as noise. Anchor is the position of the last MOVE event that was not
* considered noise.
*/
- private static final double DEFAULT_MOVEMENT_SLOPE = 20f;
+ private static final double DEFAULT_MOVEMENT_SLOP = 20f;
- private double mMovementSlope = DEFAULT_MOVEMENT_SLOPE;
+ private double mMovementSlop = DEFAULT_MOVEMENT_SLOP;
/** Whether the minor cursor movement should be ignored. */
private boolean mIgnoreMinorCursorMovement = AUTOCLICK_IGNORE_MINOR_CURSOR_MOVEMENT_DEFAULT;
@@ -500,6 +505,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
mMetaState = state;
}
+ @VisibleForTesting
+ int getMetaStateForTesting() {
+ return mMetaState;
+ }
+
/**
* Updates delay that should be used when scheduling clicks. The delay will be used only for
* clicks scheduled after this point (pending click tasks are not affected).
@@ -589,19 +599,19 @@ public class AutoclickController extends BaseEventStreamTransformation {
float deltaX = mAnchorCoords.x - event.getX(pointerIndex);
float deltaY = mAnchorCoords.y - event.getY(pointerIndex);
double delta = Math.hypot(deltaX, deltaY);
- double slope =
+ double slop =
((Flags.enableAutoclickIndicator() && mIgnoreMinorCursorMovement)
- ? mMovementSlope
- : DEFAULT_MOVEMENT_SLOPE);
- return delta > slope;
+ ? mMovementSlop
+ : DEFAULT_MOVEMENT_SLOP);
+ return delta > slop;
}
public void setIgnoreMinorCursorMovement(boolean ignoreMinorCursorMovement) {
mIgnoreMinorCursorMovement = ignoreMinorCursorMovement;
}
- private void updateMovementSlope(double slope) {
- mMovementSlope = slope;
+ private void updateMovementSlop(double slop) {
+ mMovementSlop = slop;
}
/**
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
new file mode 100644
index 000000000000..1ba574559918
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.autoclick;
+
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.R;
+
+public class AutoclickTypePanel {
+
+ private final String TAG = AutoclickTypePanel.class.getSimpleName();
+
+ private final Context mContext;
+
+ private final View mContentView;
+
+ private final WindowManager mWindowManager;
+
+ public AutoclickTypePanel(Context context, WindowManager windowManager) {
+ mContext = context;
+ mWindowManager = windowManager;
+
+ mContentView = LayoutInflater.from(context).inflate(
+ R.layout.accessibility_autoclick_type_panel, null);
+ }
+
+ public void show() {
+ mWindowManager.addView(mContentView, getLayoutParams());
+ }
+
+ public void hide() {
+ mWindowManager.removeView(mContentView);
+ }
+
+ /**
+ * Retrieves the layout params for AutoclickIndicatorView, used when it's added to the Window
+ * Manager.
+ */
+ @NonNull
+ private WindowManager.LayoutParams getLayoutParams() {
+ final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
+ layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+ layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+ layoutParams.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+ layoutParams.setFitInsetsTypes(WindowInsets.Type.statusBars());
+ layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ layoutParams.format = PixelFormat.TRANSLUCENT;
+ layoutParams.setTitle(AutoclickTypePanel.class.getSimpleName());
+ layoutParams.accessibilityTitle =
+ mContext.getString(R.string.accessibility_autoclick_type_settings_panel_title);
+ layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ // TODO(b/388847771): Compute position based on user interaction.
+ layoutParams.x = 15;
+ layoutParams.y = 90;
+ layoutParams.gravity = Gravity.END | Gravity.BOTTOM;
+
+ return layoutParams;
+ }
+}
diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
index df47c98d6433..89c9d690a82c 100644
--- a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
+++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java
@@ -27,10 +27,6 @@ import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
-import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
-import static android.view.WindowManager.LayoutParams.TYPE_POINTER;
-import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_CONTENT;
import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUCTURE;
@@ -74,6 +70,7 @@ import android.util.Log;
import android.util.Slog;
import android.view.IWindowManager;
import android.window.ScreenCapture;
+import android.window.ScreenCapture.ScreenshotHardwareBuffer;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
@@ -333,10 +330,10 @@ public class ContextualSearchManagerService extends SystemService {
isManagedProfileVisible = true;
}
}
+ final String csPackage = Objects.requireNonNull(launchIntent.getPackage());
+ final int csUid = mPackageManager.getPackageUid(csPackage, /* flags */ 0L, userId);
if (isAssistDataAllowed) {
try {
- final String csPackage = Objects.requireNonNull(launchIntent.getPackage());
- final int csUid = mPackageManager.getPackageUid(csPackage, 0, 0);
mAssistDataRequester.requestAssistData(
activityTokens,
/* fetchData */ true,
@@ -350,17 +347,8 @@ public class ContextualSearchManagerService extends SystemService {
Log.e(TAG, "Could not request assist data", e);
}
}
- final ScreenCapture.ScreenshotHardwareBuffer shb;
- if (mWmInternal != null) {
- shb = mWmInternal.takeAssistScreenshot(Set.of(
- TYPE_STATUS_BAR,
- TYPE_NAVIGATION_BAR,
- TYPE_NAVIGATION_BAR_PANEL,
- TYPE_POINTER));
- } else {
- if (DEBUG) Log.w(TAG, "Can't capture contextual screenshot: mWmInternal is null");
- shb = null;
- }
+ final ScreenshotHardwareBuffer shb = mWmInternal.takeContextualSearchScreenshot(
+ (Flags.contextualSearchWindowLayer() ? csUid : -1));
final Bitmap bm = shb != null ? shb.asBitmap() : null;
// Now that everything is fetched, putting it in the launchIntent.
if (bm != null) {
@@ -509,15 +497,17 @@ public class ContextualSearchManagerService extends SystemService {
bundle.putParcelable(ContextualSearchManager.EXTRA_TOKEN, mToken);
// We get take the screenshot with the system server's identity because the system
// server has READ_FRAME_BUFFER permission to get the screenshot.
+ final int callingUid = Binder.getCallingUid();
Binder.withCleanCallingIdentity(() -> {
- if (mWmInternal != null) {
+ final ScreenshotHardwareBuffer shb =
+ mWmInternal.takeContextualSearchScreenshot(
+ (Flags.contextualSearchWindowLayer() ? callingUid : -1));
+ final Bitmap bm = shb != null ? shb.asBitmap() : null;
+ if (bm != null) {
bundle.putParcelable(ContextualSearchManager.EXTRA_SCREENSHOT,
- mWmInternal.takeAssistScreenshot(Set.of(
- TYPE_STATUS_BAR,
- TYPE_NAVIGATION_BAR,
- TYPE_NAVIGATION_BAR_PANEL,
- TYPE_POINTER))
- .asBitmap().asShared());
+ bm.asShared());
+ bundle.putBoolean(ContextualSearchManager.EXTRA_FLAG_SECURE_FOUND,
+ shb.containsSecureLayers());
}
try {
callback.onResult(
diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java
index 7a5b8660ef7c..cf0d7e72ba8b 100644
--- a/services/core/java/com/android/server/BootReceiver.java
+++ b/services/core/java/com/android/server/BootReceiver.java
@@ -53,6 +53,7 @@ import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.am.DropboxRateLimiter;
import com.android.server.os.TombstoneProtos.Tombstone;
+import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -154,6 +155,10 @@ public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(final Context context, Intent intent) {
+ if (!Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
+ return;
+ }
+
// Log boot events in the background to avoid blocking the main thread with I/O
new Thread() {
@Override
@@ -219,6 +224,8 @@ public class BootReceiver extends BroadcastReceiver {
} catch (Exception e) {
Slog.wtf(TAG, "Error watching for trace events", e);
return 0; // Unregister the handler.
+ } finally {
+ IoUtils.closeQuietly(fd);
}
return OnFileDescriptorEventListener.EVENT_INPUT;
}
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 19e7e062758a..ce6e1ba0cfda 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -947,7 +947,6 @@ class StorageManagerService extends IStorageManager.Stub
refreshZramSettings();
if (mmdEnabled()) {
- // TODO: b/375432472 - Start zram maintenance only when zram is enabled.
ZramMaintenance.startZramMaintenance(mContext);
} else {
// Schedule zram writeback unless zram is disabled by persist.sys.zram_enabled
diff --git a/services/core/java/com/android/server/TradeInModeService.java b/services/core/java/com/android/server/TradeInModeService.java
index a69667395ebd..1a9e02c86560 100644
--- a/services/core/java/com/android/server/TradeInModeService.java
+++ b/services/core/java/com/android/server/TradeInModeService.java
@@ -41,12 +41,15 @@ import android.util.Slog;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
public final class TradeInModeService extends SystemService {
private static final String TAG = "TradeInModeService";
private static final String TIM_PROP = "persist.adb.tradeinmode";
+ private static final String TIM_TEST_PROP = "persist.adb.test_tradeinmode";
private static final int TIM_STATE_UNSET = 0;
@@ -108,6 +111,10 @@ public final class TradeInModeService extends SystemService {
// setup completion observer.
if (isDeviceSetup()) {
stopTradeInMode();
+ } else if (isDebuggable() && !isForceEnabledForTesting()) {
+ // The device was made debuggable after entering TIM. This
+ // can happen while flashing. For convenience, leave test mode.
+ leaveTestMode();
} else {
watchForSetupCompletion();
watchForNetworkChange();
@@ -171,12 +178,7 @@ public final class TradeInModeService extends SystemService {
Slog.e(TAG, "Cannot enter evaluation mode, FRP lock is present.");
return false;
}
-
- try (FileWriter fw = new FileWriter(WIPE_INDICATOR_FILE,
- StandardCharsets.US_ASCII)) {
- fw.write("0");
- } catch (IOException e) {
- Slog.e(TAG, "Failed to write " + WIPE_INDICATOR_FILE, e);
+ if (!scheduleTradeInModeWipe()) {
return false;
}
@@ -189,7 +191,7 @@ public final class TradeInModeService extends SystemService {
}
SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_EVALUATION_MODE));
- SystemProperties.set("ctl.restart", "adbd");
+ restartAdbd();
return true;
}
@@ -200,6 +202,55 @@ public final class TradeInModeService extends SystemService {
"Cannot test for trade-in evaluation mode allowed");
return !isFrpActive();
}
+
+ @Override
+ @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
+ public void scheduleWipeForTesting() {
+ enforceTestingPermissions();
+
+ scheduleTradeInModeWipe();
+ }
+
+ @Override
+ @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
+ public void startTesting() {
+ enforceTestingPermissions();
+
+ enterTestMode();
+ }
+
+ @Override
+ @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
+ public void stopTesting() {
+ enforceTestingPermissions();
+
+ if (!isForceEnabledForTesting()) {
+ throw new IllegalStateException("testing must have been started");
+ }
+
+ final long callingId = Binder.clearCallingIdentity();
+ try {
+ leaveTestMode();
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ }
+
+ @Override
+ @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
+ public boolean isTesting() {
+ enforceTestingPermissions();
+
+ return isForceEnabledForTesting();
+ }
+
+ private void enforceTestingPermissions() {
+ mContext.enforceCallingOrSelfPermission("android.permission.ENTER_TRADE_IN_MODE",
+ "Caller must have ENTER_TRADE_IN_MODE permission");
+ if (!isDebuggable()) {
+ throw new SecurityException("ro.debuggable must be set to 1");
+ }
+ }
}
private void startTradeInMode() {
@@ -207,8 +258,7 @@ public final class TradeInModeService extends SystemService {
SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_FOYER));
- final ContentResolver cr = mContext.getContentResolver();
- Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 1);
+ setAdbEnabled(true);
watchForSetupCompletion();
watchForNetworkChange();
@@ -223,8 +273,51 @@ public final class TradeInModeService extends SystemService {
removeNetworkWatch();
removeAccountsWatch();
+ if (isForceEnabledForTesting()) {
+ // If testing in a debug build, we need to re-enable ADB.
+ restartAdbd();
+ } else {
+ // Otherwise, ADB must not be enabled.
+ setAdbEnabled(false);
+ }
+ }
+
+ private void enterTestMode() {
+ SystemProperties.set(TIM_TEST_PROP, "1");
+ }
+
+ private void leaveTestMode() {
+ if (getTradeInModeState() == TIM_STATE_FOYER) {
+ stopTradeInMode();
+ }
+
+ SystemProperties.set(TIM_TEST_PROP, "");
+ SystemProperties.set(TIM_PROP, "");
+ try {
+ Files.deleteIfExists(Paths.get(WIPE_INDICATOR_FILE));
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to remove wipe indicator", e);
+ }
+ }
+
+ private boolean scheduleTradeInModeWipe() {
+ try (FileWriter fw = new FileWriter(WIPE_INDICATOR_FILE,
+ StandardCharsets.US_ASCII)) {
+ fw.write("0");
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to write " + WIPE_INDICATOR_FILE, e);
+ return false;
+ }
+ return true;
+ }
+
+ private void restartAdbd() {
+ SystemProperties.set("ctl.restart", "adbd");
+ }
+
+ private void setAdbEnabled(boolean enabled) {
final ContentResolver cr = mContext.getContentResolver();
- Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 0);
+ Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, enabled ? 1 : 0);
}
private int getTradeInModeState() {
@@ -236,7 +329,7 @@ public final class TradeInModeService extends SystemService {
}
private boolean isForceEnabledForTesting() {
- return SystemProperties.getInt("persist.adb.test_tradeinmode", 0) == 1;
+ return isDebuggable() && SystemProperties.getInt(TIM_TEST_PROP, 0) == 1;
}
private boolean isAdbEnabled() {
diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java
index 896c9b8d0932..c65981bf0703 100644
--- a/services/core/java/com/android/server/UiModeManagerService.java
+++ b/services/core/java/com/android/server/UiModeManagerService.java
@@ -16,11 +16,13 @@
package com.android.server;
-import static android.app.Flags.modesApi;
import static android.app.Flags.enableCurrentModeTypeBinderCache;
import static android.app.Flags.enableNightModeBinderCache;
+import static android.app.Flags.modesApi;
import static android.app.UiModeManager.ContrastUtils.CONTRAST_DEFAULT_VALUE;
import static android.app.UiModeManager.DEFAULT_PRIORITY;
+import static android.app.UiModeManager.FORCE_INVERT_TYPE_DARK;
+import static android.app.UiModeManager.FORCE_INVERT_TYPE_OFF;
import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF;
import static android.app.UiModeManager.MODE_NIGHT_AUTO;
import static android.app.UiModeManager.MODE_NIGHT_CUSTOM;
@@ -33,6 +35,7 @@ import static android.app.UiModeManager.PROJECTION_TYPE_AUTOMOTIVE;
import static android.app.UiModeManager.PROJECTION_TYPE_NONE;
import static android.os.UserHandle.USER_SYSTEM;
import static android.os.UserHandle.getCallingUserId;
+import static android.provider.Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED;
import static android.provider.Settings.Secure.CONTRAST_LEVEL;
import static android.util.TimeUtils.isTimeBetween;
@@ -45,6 +48,7 @@ import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
+import android.app.ActivityThread;
import android.app.AlarmManager;
import android.app.IOnProjectionStateChangedListener;
import android.app.IUiModeManager;
@@ -56,6 +60,7 @@ import android.app.PendingIntent;
import android.app.StatusBarManager;
import android.app.UiModeManager;
import android.app.UiModeManager.AttentionModeThemeOverlayType;
+import android.app.UiModeManager.ForceInvertType;
import android.app.UiModeManager.NightModeCustomReturnType;
import android.app.UiModeManager.NightModeCustomType;
import android.content.BroadcastReceiver;
@@ -93,6 +98,7 @@ import android.service.vr.IVrStateCallbacks;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseIntArray;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
@@ -256,6 +262,9 @@ final class UiModeManagerService extends SystemService {
@GuardedBy("mLock")
private final SparseArray<Float> mContrasts = new SparseArray<>();
+ @GuardedBy("mLock")
+ private final SparseIntArray mForceInvertStates = new SparseIntArray();
+
public UiModeManagerService(Context context) {
this(context, /* setupWizardComplete= */ false, /* tm= */ null, new Injector());
}
@@ -407,9 +416,33 @@ final class UiModeManagerService extends SystemService {
@Override
public void onChange(boolean selfChange, Uri uri) {
updateSystemProperties();
+ updateForceInvertStates();
}
};
+ private final ContentObserver mForceInvertStateObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ updateForceInvertStates();
+ }
+ };
+
+ private void updateForceInvertStates() {
+ if (!android.view.accessibility.Flags.forceInvertColor()) {
+ return;
+ }
+
+ synchronized (mLock) {
+ if (updateForceInvertStateLocked()) {
+ int forceInvertState = getForceInvertStateLocked();
+ mUiModeManagerCallbacks.get(mCurrentUser, new RemoteCallbackList<>())
+ .broadcast(ignoreRemoteException(
+ callback ->
+ callback.notifyForceInvertStateChanged(forceInvertState)));
+ }
+ }
+ }
+
private final ContentObserver mContrastObserver = new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange, Uri uri) {
@@ -485,6 +518,12 @@ final class UiModeManagerService extends SystemService {
context.getContentResolver()
.registerContentObserver(Secure.getUriFor(Secure.UI_NIGHT_MODE),
false, mDarkThemeObserver, 0);
+ if (android.view.accessibility.Flags.forceInvertColor()) {
+ context.getContentResolver()
+ .registerContentObserver(
+ Secure.getUriFor(ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED),
+ false, mForceInvertStateObserver, UserHandle.USER_ALL);
+ }
context.getContentResolver().registerContentObserver(
Secure.getUriFor(Secure.CONTRAST_LEVEL), false,
mContrastObserver, UserHandle.USER_ALL);
@@ -919,7 +958,7 @@ final class UiModeManagerService extends SystemService {
@android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)
@Override
- public @NightModeCustomReturnType int getNightModeCustomType() {
+ public @NightModeCustomReturnType int getNightModeCustomType() {
getNightModeCustomType_enforcePermission();
synchronized (mLock) {
return mNightModeCustomType;
@@ -1275,6 +1314,14 @@ final class UiModeManagerService extends SystemService {
return getContrastLocked();
}
}
+
+ @Override
+ @ForceInvertType
+ public int getForceInvertState() {
+ synchronized (mLock) {
+ return getForceInvertStateLocked();
+ }
+ }
};
private void enforceProjectionTypePermissions(@UiModeManager.ProjectionType int p) {
@@ -1358,6 +1405,76 @@ final class UiModeManagerService extends SystemService {
}
/**
+ * Return the force invert for the current user. If not cached, fetch it from the settings.
+ */
+ @GuardedBy("mLock")
+ @ForceInvertType
+ private int getForceInvertStateLocked() {
+ if (mForceInvertStates.indexOfKey(mCurrentUser) < 0) {
+ updateForceInvertStateLocked();
+ }
+ return mForceInvertStates.get(mCurrentUser);
+ }
+
+ /**
+ * Read the force invert setting for the current user and update {@link #mForceInvertStates}
+ * if the contrast changed. Returns true if {@link #mForceInvertStates} was updated.
+ */
+ @GuardedBy("mLock")
+ private boolean updateForceInvertStateLocked() {
+ int forceInvertState = getForceInvertStateInternal();
+ if (mForceInvertStates.get(mCurrentUser) != forceInvertState) {
+ mForceInvertStates.put(mCurrentUser, forceInvertState);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the current state of force invert, which modifies the display colors to
+ * enhance visibility based on the system's dark theme settings and app-specific configurations.
+ *
+ * <p>This method is for informational purposes only. The application does not need to
+ * implement any special handling for force invert; the system applies it automatically.
+ * If you want to prevent force invert from affecting your app, ensure you have defined
+ * both light and dark themes. Force invert is not applied to apps that already adapt
+ * to the user's system theme preference.</p>
+ *
+ * @return The current force invert state, represented by a {@code ForceDarkType} constant.
+ *
+ * @hide
+ */
+ private int getForceInvertStateInternal() {
+ if (!android.view.accessibility.Flags.forceInvertColor()) {
+ return FORCE_INVERT_TYPE_OFF;
+ }
+
+ if (!isSystemInDarkTheme()) {
+ return FORCE_INVERT_TYPE_OFF;
+ }
+
+ if (!isForceInvert()) {
+ return FORCE_INVERT_TYPE_OFF;
+ }
+
+ return FORCE_INVERT_TYPE_DARK;
+ }
+
+ private boolean isSystemInDarkTheme() {
+ Context sysUiContext = ActivityThread.currentActivityThread().getSystemUiContext();
+ int sysUiNightMode = sysUiContext.getResources().getConfiguration().uiMode
+ & Configuration.UI_MODE_NIGHT_MASK;
+ return sysUiNightMode == Configuration.UI_MODE_NIGHT_YES;
+ }
+
+ private boolean isForceInvert() {
+ return Settings.Secure.getIntForUser(
+ getContext().getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED,
+ /* def= */ 0, mCurrentUser) == 1;
+ }
+
+ /**
* Return the contrast for the current user. If not cached, fetch it from the settings.
*/
@GuardedBy("mLock")
@@ -1967,6 +2084,14 @@ final class UiModeManagerService extends SystemService {
sendConfigurationAndStartDreamOrDockAppLocked(category);
}
+ private boolean shouldStartDockApp(Context context, Intent homeIntent) {
+ if (mWatch && !mSetupWizardComplete) {
+ // Do not ever start dock app when setup is not complete on a watch.
+ return false;
+ }
+ return Sandman.shouldStartDockApp(context, homeIntent);
+ }
+
private void sendConfigurationAndStartDreamOrDockAppLocked(String category) {
// Update the configuration but don't send it yet.
mHoldingConfiguration = false;
@@ -1983,7 +2108,7 @@ final class UiModeManagerService extends SystemService {
// activity manager take care of both the start and config
// change.
Intent homeIntent = buildHomeIntent(category);
- if (Sandman.shouldStartDockApp(getContext(), homeIntent)) {
+ if (shouldStartDockApp(getContext(), homeIntent)) {
try {
int result = ActivityTaskManager.getService().startActivityWithConfig(
null, getContext().getBasePackageName(),
diff --git a/services/core/java/com/android/server/ZramMaintenance.java b/services/core/java/com/android/server/ZramMaintenance.java
index cdb48122e321..099e5b3fe440 100644
--- a/services/core/java/com/android/server/ZramMaintenance.java
+++ b/services/core/java/com/android/server/ZramMaintenance.java
@@ -24,11 +24,15 @@ import android.content.ComponentName;
import android.content.Context;
import android.os.IBinder;
import android.os.IMmd;
+import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
+import android.provider.DeviceConfig;
import android.util.Slog;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.time.Duration;
/**
@@ -46,43 +50,45 @@ public class ZramMaintenance extends JobService {
private static final String TAG = ZramMaintenance.class.getName();
// Job id must be unique across all clients of the same uid. ZramMaintenance uses the bug number
// as the job id.
- private static final int JOB_ID = 375432472;
+ @VisibleForTesting
+ public static final int JOB_ID = 375432472;
private static final ComponentName sZramMaintenance =
new ComponentName("android", ZramMaintenance.class.getName());
+ @VisibleForTesting
+ public static final String KEY_CHECK_STATUS = "check_status";
+ private static final String SYSTEM_PROPERTY_PREFIX = "mm.";
private static final String FIRST_DELAY_SECONDS_PROP =
- "mm.zram.maintenance.first_delay_seconds";
+ "zram.maintenance.first_delay_seconds";
// The default is 1 hour.
private static final long DEFAULT_FIRST_DELAY_SECONDS = 3600;
private static final String PERIODIC_DELAY_SECONDS_PROP =
- "mm.zram.maintenance.periodic_delay_seconds";
+ "zram.maintenance.periodic_delay_seconds";
// The default is 1 hour.
private static final long DEFAULT_PERIODIC_DELAY_SECONDS = 3600;
private static final String REQUIRE_DEVICE_IDLE_PROP =
- "mm.zram.maintenance.require_device_idle";
+ "zram.maintenance.require_device_idle";
private static final boolean DEFAULT_REQUIRE_DEVICE_IDLE =
true;
private static final String REQUIRE_BATTERY_NOT_LOW_PROP =
- "mm.zram.maintenance.require_battry_not_low";
+ "zram.maintenance.require_battry_not_low";
private static final boolean DEFAULT_REQUIRE_BATTERY_NOT_LOW =
true;
@Override
public boolean onStartJob(JobParameters params) {
- IBinder binder = ServiceManager.getService("mmd");
- if (binder != null) {
- IMmd mmd = IMmd.Stub.asInterface(binder);
- try {
- mmd.doZramMaintenance();
- } catch (RemoteException e) {
- Slog.e(TAG, "Failed to doZramMaintenance", e);
+ new Thread("ZramMaintenance") {
+ @Override
+ public void run() {
+ try {
+ IBinder binder = ServiceManager.getService("mmd");
+ IMmd mmd = IMmd.Stub.asInterface(binder);
+ startJob(ZramMaintenance.this, params, mmd);
+ } finally {
+ jobFinished(params, false);
+ }
}
- } else {
- Slog.w(TAG, "binder not found");
- }
- Duration delay = Duration.ofSeconds(SystemProperties.getLong(PERIODIC_DELAY_SECONDS_PROP,
- DEFAULT_PERIODIC_DELAY_SECONDS));
- scheduleZramMaintenance(this, delay);
+ }.start();
return true;
}
@@ -92,27 +98,75 @@ public class ZramMaintenance extends JobService {
}
/**
+ * This is public to test ZramMaintenance logic.
+ *
+ * <p>
+ * We need to pass mmd as parameter because we can't mock "IMmd.Stub.asInterface".
+ *
+ * <p>
+ * Since IMmd.isZramMaintenanceSupported() is blocking call, this method should be executed on
+ * a worker thread.
+ */
+ @VisibleForTesting
+ public static void startJob(Context context, JobParameters params, IMmd mmd) {
+ boolean checkStatus = params.getExtras().getBoolean(KEY_CHECK_STATUS);
+ if (mmd != null) {
+ try {
+ if (checkStatus && !mmd.isZramMaintenanceSupported()) {
+ Slog.i(TAG, "zram maintenance is not supported");
+ return;
+ }
+ // Status check is required before the first doZramMaintenanceAsync() call once.
+ checkStatus = false;
+
+ mmd.doZramMaintenanceAsync();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to binder call to mmd", e);
+ }
+ } else {
+ Slog.w(TAG, "binder not found");
+ }
+ Duration delay = Duration.ofSeconds(getLongProperty(PERIODIC_DELAY_SECONDS_PROP,
+ DEFAULT_PERIODIC_DELAY_SECONDS));
+ scheduleZramMaintenance(context, delay, checkStatus);
+ }
+
+ /**
* Starts periodical zram maintenance.
*/
public static void startZramMaintenance(Context context) {
Duration delay = Duration.ofSeconds(
- SystemProperties.getLong(FIRST_DELAY_SECONDS_PROP, DEFAULT_FIRST_DELAY_SECONDS));
- scheduleZramMaintenance(context, delay);
+ getLongProperty(FIRST_DELAY_SECONDS_PROP, DEFAULT_FIRST_DELAY_SECONDS));
+ scheduleZramMaintenance(context, delay, true);
}
- private static void scheduleZramMaintenance(Context context, Duration delay) {
+ private static void scheduleZramMaintenance(Context context, Duration delay,
+ boolean checkStatus) {
JobScheduler js = context.getSystemService(JobScheduler.class);
if (js != null) {
+ final PersistableBundle bundle = new PersistableBundle();
+ bundle.putBoolean(KEY_CHECK_STATUS, checkStatus);
js.schedule(new JobInfo.Builder(JOB_ID, sZramMaintenance)
.setMinimumLatency(delay.toMillis())
.setRequiresDeviceIdle(
- SystemProperties.getBoolean(REQUIRE_DEVICE_IDLE_PROP,
+ getBooleanProperty(REQUIRE_DEVICE_IDLE_PROP,
DEFAULT_REQUIRE_DEVICE_IDLE))
.setRequiresBatteryNotLow(
- SystemProperties.getBoolean(REQUIRE_BATTERY_NOT_LOW_PROP,
+ getBooleanProperty(REQUIRE_BATTERY_NOT_LOW_PROP,
DEFAULT_REQUIRE_BATTERY_NOT_LOW))
+ .setExtras(bundle)
.build());
}
}
+
+ private static long getLongProperty(String name, long defaultValue) {
+ return DeviceConfig.getLong(DeviceConfig.NAMESPACE_MM, name,
+ SystemProperties.getLong(SYSTEM_PROPERTY_PREFIX + name, defaultValue));
+ }
+
+ private static boolean getBooleanProperty(String name, boolean defaultValue) {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_MM, name,
+ SystemProperties.getBoolean(SYSTEM_PROPERTY_PREFIX + name, defaultValue));
+ }
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index cbebc905796c..c237897f1229 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -4279,7 +4279,6 @@ final class ActivityManagerShellCommand extends ShellCommand {
}
int runClearBadProcess(PrintWriter pw) throws RemoteException {
- final String processName = getNextArgRequired();
int userId = UserHandle.USER_CURRENT;
String opt;
while ((opt = getNextOption()) != null) {
@@ -4290,6 +4289,7 @@ final class ActivityManagerShellCommand extends ShellCommand {
return -1;
}
}
+ final String processName = getNextArgRequired();
if (userId == UserHandle.USER_CURRENT) {
userId = mInternal.getCurrentUserId();
}
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 5184a2c4ec30..c6338307b192 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -186,8 +186,10 @@ public class SettingsToPropertiesMapper {
"core_libraries",
"crumpet",
"dck_framework",
+ "desktop_connectivity",
"desktop_hwsec",
"desktop_stats",
+ "desktop_wifi",
"devoptions_settings",
"game",
"gpu",
@@ -218,6 +220,7 @@ public class SettingsToPropertiesMapper {
"pixel_continuity",
"pixel_display",
"pixel_perf",
+ "pixel_sensai",
"pixel_sensors",
"pixel_state_server",
"pixel_system_sw_video",
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 27e9e44f1090..e0fbaf43ea43 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -31,6 +31,7 @@ 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;
@@ -3904,10 +3905,6 @@ class UserController implements Handler.Callback {
return mService.mWindowManager;
}
- ActivityTaskManagerInternal getActivityTaskManagerInternal() {
- return mService.mAtmInternal;
- }
-
void activityManagerOnUserStopped(@UserIdInt int userId) {
LocalServices.getService(ActivityTaskManagerInternal.class).onUserStopped(userId);
}
@@ -4122,40 +4119,25 @@ 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);
- 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();
+ Bundle bundle = new Bundle();
+ bundle.putBinder(LOCK_ON_USER_SWITCH_CALLBACK, new IRemoteCallback.Stub() {
+ public void sendResult(Bundle data) {
+ latch.countDown();
+ }
+ });
+ getWindowManager().lockNow(bundle);
try {
if (!latch.await(20, TimeUnit.SECONDS)) {
- throw new RuntimeException("Keyguard is not shown in 20 seconds");
+ throw new RuntimeException("User controller expected a callback while waiting "
+ + "to show the keyguard. Timed out after 20 seconds.");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
- getActivityTaskManagerInternal().unregisterScreenObserver(screenObserver);
t.traceEnd();
}
}
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index e8a222625b1b..32c4e9b1727e 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -551,6 +551,11 @@ public class AppOpsService extends IAppOpsService.Stub {
@VisibleForTesting
final Constants mConstants;
+ /**
+ * Some processes in the user may still be running when trying to drop the user's state
+ */
+ private static final long REMOVE_USER_DELAY = 5000L;
+
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
final class UidState {
public final int uid;
@@ -6820,14 +6825,17 @@ public class AppOpsService extends IAppOpsService.Stub {
@Override
public void removeUser(int userHandle) throws RemoteException {
checkSystemUid("removeUser");
- synchronized (AppOpsService.this) {
- final int tokenCount = mOpUserRestrictions.size();
- for (int i = tokenCount - 1; i >= 0; i--) {
- ClientUserRestrictionState opRestrictions = mOpUserRestrictions.valueAt(i);
- opRestrictions.removeUser(userHandle);
+ mHandler.postDelayed(() -> {
+ Slog.i(TAG, "Removing user " + userHandle + " from AppOpsService");
+ synchronized (AppOpsService.this) {
+ final int tokenCount = mOpUserRestrictions.size();
+ for (int i = tokenCount - 1; i >= 0; i--) {
+ ClientUserRestrictionState opRestrictions = mOpUserRestrictions.valueAt(i);
+ opRestrictions.removeUser(userHandle);
+ }
+ removeUidsForUserLocked(userHandle);
}
- removeUidsForUserLocked(userHandle);
- }
+ }, REMOVE_USER_DELAY);
}
@Override
diff --git a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java
index fece7a899f0a..ae961b53f547 100644
--- a/services/core/java/com/android/server/audio/AudioManagerShellCommand.java
+++ b/services/core/java/com/android/server/audio/AudioManagerShellCommand.java
@@ -83,6 +83,8 @@ class AudioManagerShellCommand extends ShellCommand {
return setGroupVolume();
case "adj-group-volume":
return adjGroupVolume();
+ case "set-hardening":
+ return setEnableHardening();
}
return 0;
}
@@ -130,6 +132,8 @@ class AudioManagerShellCommand extends ShellCommand {
pw.println(" Sets the volume for GROUP_ID to VOLUME_INDEX");
pw.println(" adj-group-volume GROUP_ID <RAISE|LOWER|MUTE|UNMUTE>");
pw.println(" Adjusts the group volume for GROUP_ID given the specified direction");
+ pw.println(" set-enable-hardening <1|0>");
+ pw.println(" Enables full audio hardening enforcement, disabling any exemptions");
}
private int setSurroundFormatEnabled() {
@@ -405,6 +409,20 @@ class AudioManagerShellCommand extends ShellCommand {
return 0;
}
+ private int setEnableHardening() {
+ final Context context = mService.mContext;
+ final AudioManager am = context.getSystemService(AudioManager.class);
+ final boolean shouldEnable = !(readIntArg() == 0);
+ getOutPrintWriter().println(
+ "calling AudioManager.setEnableHardening(" + shouldEnable + ")");
+ try {
+ am.setEnableHardening(shouldEnable);
+ } catch (Exception e) {
+ getOutPrintWriter().println("Exception: " + e);
+ }
+ return 0;
+ }
+
private int readIntArg() throws IllegalArgumentException {
final String argText = getNextArg();
diff --git a/services/core/java/com/android/server/audio/AudioPolicyFacade.java b/services/core/java/com/android/server/audio/AudioPolicyFacade.java
index f652b33b3fd3..6c0b81f513be 100644
--- a/services/core/java/com/android/server/audio/AudioPolicyFacade.java
+++ b/services/core/java/com/android/server/audio/AudioPolicyFacade.java
@@ -26,4 +26,5 @@ public interface AudioPolicyFacade {
public boolean isHotwordStreamSupported(boolean lookbackAudio);
public INativePermissionController getPermissionController();
public void registerOnStartTask(Runnable r);
+ public void setEnableHardening(boolean shouldEnable);
}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index f2830090e7db..02e0d9ffb1d4 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -781,7 +781,8 @@ public class AudioService extends IAudioService.Stub
private int mRingerModeExternal = -1; // reported ringer mode to outside clients (AudioManager)
/** @see System#MODE_RINGER_STREAMS_AFFECTED */
- private int mRingerModeAffectedStreams = 0;
+ @VisibleForTesting
+ protected int mRingerModeAffectedStreams = 0;
private int mZenModeAffectedStreams = 0;
@@ -1191,6 +1192,11 @@ public class AudioService extends IAudioService.Stub
private @AttributeSystemUsage int[] mSupportedSystemUsages =
new int[]{AudioAttributes.USAGE_CALL_ASSISTANT};
+ // Tracks the API/shell override of hardening enforcement used for debugging
+ // When this is set to true, enforcement is on regardless of flag state and any specific
+ // exemptions in place for compat purposes.
+ private final AtomicBoolean mShouldEnableAllHardening = new AtomicBoolean(false);
+
// Defines the format for the connection "address" for ALSA devices
public static String makeAlsaAddressString(int card, int device) {
return "card=" + card + ";device=" + device;
@@ -1334,6 +1340,10 @@ public class AudioService extends IAudioService.Stub
mAudioVolumeGroupHelper = audioVolumeGroupHelper;
mSettings = settings;
mAudioPolicy = audioPolicy;
+ mAudioPolicy.registerOnStartTask(() -> {
+ mAudioPolicy.setEnableHardening(mShouldEnableAllHardening.get());
+ });
+
mPlatformType = AudioSystem.getPlatformType(context);
mBroadcastHandlerThread = new HandlerThread("AudioService Broadcast");
@@ -6315,17 +6325,15 @@ public class AudioService extends IAudioService.Stub
}
}
sRingerAndZenModeMutedStreams &= ~(1 << streamType);
- sMuteLogger.enqueue(new AudioServiceEvents.RingerZenMutedStreamsEvent(
- sRingerAndZenModeMutedStreams, "muteRingerModeStreams"));
vss.mute(false, "muteRingerModeStreams");
} else {
// mute
sRingerAndZenModeMutedStreams |= (1 << streamType);
- sMuteLogger.enqueue(new AudioServiceEvents.RingerZenMutedStreamsEvent(
- sRingerAndZenModeMutedStreams, "muteRingerModeStreams"));
vss.mute(true, "muteRingerModeStreams");
}
}
+ sMuteLogger.enqueue(new AudioServiceEvents.RingerZenMutedStreamsEvent(
+ sRingerAndZenModeMutedStreams, "muteRingerModeStreams"));
}
private boolean isAlarm(int streamType) {
@@ -10045,12 +10053,14 @@ public class AudioService extends IAudioService.Stub
new AudioServiceEvents.StreamMuteEvent(mStreamType, state, src));
// check to see if unmuting should not have happened due to ringer muted streams
if (!state && isStreamMutedByRingerOrZenMode(mStreamType)) {
- Log.e(TAG, "Unmuting stream " + mStreamType
+ Slog.e(TAG, "Attempt to unmute stream " + mStreamType
+ " despite ringer-zen muted stream 0x"
+ Integer.toHexString(AudioService.sRingerAndZenModeMutedStreams),
new Exception()); // this will put a stack trace in the logs
sMuteLogger.enqueue(new AudioServiceEvents.StreamUnmuteErrorEvent(
mStreamType, AudioService.sRingerAndZenModeMutedStreams));
+ // do not change mute state
+ return false;
}
mIsMuted = state;
if (apply) {
@@ -15019,6 +15029,16 @@ public class AudioService extends IAudioService.Stub
return true;
}
+ /**
+ * @see AudioManager#setEnableHardening(boolean)
+ */
+ @android.annotation.EnforcePermission(MODIFY_AUDIO_SETTINGS_PRIVILEGED)
+ public void setEnableHardening(boolean shouldEnable) {
+ super.setEnableHardening_enforcePermission();
+ mShouldEnableAllHardening.set(shouldEnable);
+ mAudioPolicy.setEnableHardening(shouldEnable);
+ }
+
//======================
// Audioserver state dispatch
//======================
diff --git a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java
index 09701e49a8ac..c41f41e0f31b 100644
--- a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java
+++ b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java
@@ -80,4 +80,14 @@ public class DefaultAudioPolicyFacade implements AudioPolicyFacade {
public void registerOnStartTask(Runnable task) {
mServiceHolder.registerOnStartTask(unused -> task.run());
}
+
+ @Override
+ public void setEnableHardening(boolean shouldEnable) {
+ IAudioPolicyService ap = mServiceHolder.waitForService();
+ try {
+ ap.setEnableHardening(shouldEnable);
+ } catch (RemoteException e) {
+ mServiceHolder.attemptClear(ap.asBinder());
+ }
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
index c739118194e5..6fab7e620277 100644
--- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java
+++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java
@@ -164,6 +164,7 @@ class PreAuthInfo {
Slog.d(TAG, "Package: " + opPackageName
+ " Sensor ID: " + sensor.id
+ " Modality: " + sensor.modality
+ + " User id: " + effectiveUserId
+ " Status: " + status);
// A sensor with privacy enabled will still be eligible to
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index fb5ce5b4e5fa..41f58ae76a4d 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -809,7 +809,7 @@ final class KeyGestureController {
if (firstDown) {
handleKeyGesture(deviceId, new int[]{KeyEvent.KEYCODE_FULLSCREEN},
/* modifierState = */0,
- KeyGestureEvent.KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW,
+ KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION,
KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, focusedToken,
/* flags = */0, /* appLaunchData = */null);
}
diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
index b13dee530ee2..b529853c63a4 100644
--- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
+++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java
@@ -58,6 +58,8 @@ import java.util.stream.Stream;
private static final String UNIQUE_SYSTEM_ID_PREFIX = "SYSTEM";
private static final String UNIQUE_SYSTEM_ID_SEPARATOR = "-";
+ private static final boolean FORCE_GLOBAL_ROUTING_SESSION = true;
+ private static final String PACKAGE_NAME_FOR_GLOBAL_SESSION = "";
private final PackageManager mPackageManager;
@@ -118,6 +120,9 @@ import java.util.stream.Stream;
String routeOriginalId,
int transferReason) {
synchronized (mLock) {
+ if (FORCE_GLOBAL_ROUTING_SESSION) {
+ clientPackageName = PACKAGE_NAME_FOR_GLOBAL_SESSION;
+ }
var targetProviderProxyId = mOriginalRouteIdToProviderId.get(routeOriginalId);
var targetProviderProxyRecord = mProxyRecords.get(targetProviderProxyId);
// Holds the target route, if it's managed by a provider service. Holds null otherwise.
@@ -125,7 +130,7 @@ import java.util.stream.Stream;
targetProviderProxyRecord != null
? targetProviderProxyRecord.getRouteByOriginalId(routeOriginalId)
: null;
- var existingSessionRecord = mPackageNameToSessionRecord.get(clientPackageName);
+ var existingSessionRecord = getSessionRecordByPackageName(clientPackageName);
if (existingSessionRecord != null) {
var existingSession = existingSessionRecord.mSourceSessionInfo;
if (targetProviderProxyId != null
@@ -206,7 +211,7 @@ import java.util.stream.Stream;
if (systemSession == null) {
return null;
}
- var overridingSession = mPackageNameToSessionRecord.get(packageName);
+ var overridingSession = getSessionRecordByPackageName(packageName);
if (overridingSession != null) {
var builder =
new RoutingSessionInfo.Builder(overridingSession.mTranslatedSessionInfo)
@@ -251,7 +256,7 @@ import java.util.stream.Stream;
return;
}
synchronized (mLock) {
- var sessionRecord = mSessionOriginalIdToSessionRecord.get(sessionOriginalId);
+ var sessionRecord = getSessionRecordByOriginalId(sessionOriginalId);
var proxyRecord = sessionRecord != null ? sessionRecord.getProxyRecord() : null;
if (proxyRecord != null) {
proxyRecord.mProxy.setSessionVolume(
@@ -262,6 +267,23 @@ import java.util.stream.Stream;
notifyRequestFailed(requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE);
}
+ @GuardedBy("mLock")
+ private SystemMediaSessionRecord getSessionRecordByOriginalId(String sessionOriginalId) {
+ if (FORCE_GLOBAL_ROUTING_SESSION) {
+ return getSessionRecordByPackageName(PACKAGE_NAME_FOR_GLOBAL_SESSION);
+ } else {
+ return mSessionOriginalIdToSessionRecord.get(sessionOriginalId);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private SystemMediaSessionRecord getSessionRecordByPackageName(String clientPackageName) {
+ if (FORCE_GLOBAL_ROUTING_SESSION) {
+ clientPackageName = PACKAGE_NAME_FOR_GLOBAL_SESSION;
+ }
+ return mPackageNameToSessionRecord.get(clientPackageName);
+ }
+
/**
* Returns the uid that corresponds to the given name and user handle, or {@link
* Process#INVALID_UID} if a uid couldn't be found.
@@ -319,16 +341,34 @@ import java.util.stream.Stream;
*/
private void updateSessionInfo() {
synchronized (mLock) {
- var systemSessionInfo = mSystemSessionInfo;
- if (systemSessionInfo == null) {
+ var globalSessionInfoRecord =
+ getSessionRecordByPackageName(PACKAGE_NAME_FOR_GLOBAL_SESSION);
+ var globalSessionInfo =
+ globalSessionInfoRecord != null
+ ? globalSessionInfoRecord.mTranslatedSessionInfo
+ : null;
+ if (globalSessionInfo == null) {
+ globalSessionInfo = mSystemSessionInfo;
+ }
+ if (globalSessionInfo == null) {
// The system session info hasn't been initialized yet. Do nothing.
return;
}
- var builder = new RoutingSessionInfo.Builder(systemSessionInfo);
- mProxyRecords.values().stream()
- .flatMap(ProviderProxyRecord::getRoutesStream)
- .map(MediaRoute2Info::getOriginalId)
- .forEach(builder::addTransferableRoute);
+ var builder = new RoutingSessionInfo.Builder(globalSessionInfo);
+ if (globalSessionInfo == mSystemSessionInfo) {
+ // The session is the system one. So we make all the service-provided routes
+ // available for transfer. The system transferable routes are already there.
+ mProxyRecords.values().stream()
+ .flatMap(ProviderProxyRecord::getRoutesStream)
+ .map(MediaRoute2Info::getOriginalId)
+ .forEach(builder::addTransferableRoute);
+ } else {
+ // The session is service-provided. So we add the system-provided routes as
+ // transferable.
+ mLastSystemProviderInfo.getRoutes().stream()
+ .map(MediaRoute2Info::getOriginalId)
+ .forEach(builder::addTransferableRoute);
+ }
mSessionInfos.clear();
mSessionInfos.add(builder.build());
for (var sessionRecords : mPackageNameToSessionRecord.values()) {
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index e47f8ae9d3a5..02f817e9cd44 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -146,18 +146,27 @@ public class GroupHelper {
private static List<NotificationSectioner> NOTIFICATION_SHADE_SECTIONS =
getNotificationShadeSections();
+ private static List<NotificationSectioner> NOTIFICATION_BUNDLE_SECTIONS;
+
private static List<NotificationSectioner> getNotificationShadeSections() {
ArrayList<NotificationSectioner> sectionsList = new ArrayList<>();
if (android.service.notification.Flags.notificationClassification()) {
sectionsList.addAll(List.of(
new NotificationSectioner("PromotionsSection", 0, (record) ->
- NotificationChannel.PROMOTIONS_ID.equals(record.getChannel().getId())),
+ NotificationChannel.PROMOTIONS_ID.equals(record.getChannel().getId())
+ && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT),
new NotificationSectioner("SocialSection", 0, (record) ->
- NotificationChannel.SOCIAL_MEDIA_ID.equals(record.getChannel().getId())),
+ NotificationChannel.SOCIAL_MEDIA_ID.equals(record.getChannel().getId())
+ && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT),
new NotificationSectioner("NewsSection", 0, (record) ->
- NotificationChannel.NEWS_ID.equals(record.getChannel().getId())),
+ NotificationChannel.NEWS_ID.equals(record.getChannel().getId())
+ && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT),
new NotificationSectioner("RecsSection", 0, (record) ->
- NotificationChannel.RECS_ID.equals(record.getChannel().getId()))));
+ NotificationChannel.RECS_ID.equals(record.getChannel().getId())
+ && record.getImportance() < NotificationManager.IMPORTANCE_DEFAULT)
+ ));
+
+ NOTIFICATION_BUNDLE_SECTIONS = new ArrayList<>(sectionsList);
}
if (Flags.notificationForceGroupConversations()) {
@@ -828,7 +837,7 @@ public class GroupHelper {
}
moveNotificationsToNewSection(record.getUserId(), pkgName,
List.of(new NotificationMoveOp(record, null, fullAggregateGroupKey)),
- REGROUP_REASON_BUNDLE);
+ Map.of(record.getKey(), REGROUP_REASON_BUNDLE));
return;
}
}
@@ -895,7 +904,12 @@ public class GroupHelper {
return false;
}
- return NotificationChannel.SYSTEM_RESERVED_IDS.contains(record.getChannel().getId());
+ return isInBundleSection(record);
+ }
+
+ private static boolean isInBundleSection(final NotificationRecord record) {
+ final NotificationSectioner sectioner = getSection(record);
+ return (sectioner != null && NOTIFICATION_BUNDLE_SECTIONS.contains(sectioner));
}
/**
@@ -1084,10 +1098,10 @@ public class GroupHelper {
FullyQualifiedGroupKey newGroup) { }
/**
- * Called when a notification channel is updated (channel attributes have changed),
- * so that this helper can adjust the aggregate groups by moving children
- * if their section has changed.
- * see {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)}
+ * Called when a notification channel is updated (channel attributes have changed), so that this
+ * helper can adjust the aggregate groups by moving children if their section has changed. see
+ * {@link #onNotificationPostedWithDelay(NotificationRecord, List, Map)}
+ *
* @param userId the userId of the channel
* @param pkgName the channel's package
* @param channel the channel that was updated
@@ -1095,19 +1109,30 @@ public class GroupHelper {
*/
@FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING)
public void onChannelUpdated(final int userId, final String pkgName,
- final NotificationChannel channel, final List<NotificationRecord> notificationList) {
+ final NotificationChannel channel, final List<NotificationRecord> notificationList,
+ ArrayMap<String, NotificationRecord> summaryByGroupKey) {
synchronized (mAggregatedNotifications) {
+ final ArrayMap<String, Integer> regroupingReasonMap = new ArrayMap<>();
ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>();
for (NotificationRecord r : notificationList) {
if (r.getChannel().getId().equals(channel.getId())
&& r.getSbn().getPackageName().equals(pkgName)
&& r.getUserId() == userId) {
notificationsToCheck.put(r.getKey(), r);
+ regroupingReasonMap.put(r.getKey(), REGROUP_REASON_CHANNEL_UPDATE);
+ if (notificationRegroupOnClassification()) {
+ // Notification is unbundled and original summary found
+ // => regroup in original group
+ if (!isInBundleSection(r)
+ && isOriginalGroupSummaryPresent(r, summaryByGroupKey)) {
+ regroupingReasonMap.put(r.getKey(),
+ REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP);
+ }
+ }
}
}
- regroupNotifications(userId, pkgName, notificationsToCheck,
- REGROUP_REASON_CHANNEL_UPDATE);
+ regroupNotifications(userId, pkgName, notificationsToCheck, regroupingReasonMap);
}
}
@@ -1124,8 +1149,10 @@ public class GroupHelper {
synchronized (mAggregatedNotifications) {
ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>();
notificationsToCheck.put(record.getKey(), record);
+ ArrayMap<String, Integer> regroupReasons = new ArrayMap<>();
+ regroupReasons.put(record.getKey(), REGROUP_REASON_BUNDLE);
regroupNotifications(record.getUserId(), record.getSbn().getPackageName(),
- notificationsToCheck, REGROUP_REASON_BUNDLE);
+ notificationsToCheck, regroupReasons);
}
}
@@ -1144,16 +1171,16 @@ public class GroupHelper {
ArrayMap<String, NotificationRecord> notificationsToCheck = new ArrayMap<>();
notificationsToCheck.put(record.getKey(), record);
regroupNotifications(record.getUserId(), record.getSbn().getPackageName(),
- notificationsToCheck,
- originalSummaryExists ? REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP
- : REGROUP_REASON_UNBUNDLE);
+ notificationsToCheck, Map.of(record.getKey(),
+ originalSummaryExists ? REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP
+ : REGROUP_REASON_UNBUNDLE));
}
}
@GuardedBy("mAggregatedNotifications")
private void regroupNotifications(int userId, String pkgName,
ArrayMap<String, NotificationRecord> notificationsToCheck,
- @RegroupingReason int regroupingReason) {
+ Map<String, Integer> regroupReasons) {
// The list of notification operations required after the channel update
final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>();
@@ -1170,15 +1197,13 @@ public class GroupHelper {
// Handle "grouped correctly" notifications that were re-classified (bundled)
if (notificationRegroupOnClassification()) {
- if (regroupingReason == REGROUP_REASON_BUNDLE) {
- notificationsToMove.addAll(
- getReclassifiedNotificationsMoveOps(userId, pkgName, notificationsToCheck));
- }
+ notificationsToMove.addAll(
+ getReclassifiedNotificationsMoveOps(userId, pkgName, notificationsToCheck));
}
// Batch move to new section
if (!notificationsToMove.isEmpty()) {
- moveNotificationsToNewSection(userId, pkgName, notificationsToMove, regroupingReason);
+ moveNotificationsToNewSection(userId, pkgName, notificationsToMove, regroupReasons);
}
}
@@ -1187,9 +1212,9 @@ public class GroupHelper {
final ArrayList<NotificationMoveOp> notificationsToMove = new ArrayList<>();
for (NotificationRecord record : notificationsToCheck.values()) {
if (isChildOfValidAppGroup(record)) {
- // Check if section changes
+ // Check if section changes to a bundle section
NotificationSectioner sectioner = getSection(record);
- if (sectioner != null) {
+ if (sectioner != null && NOTIFICATION_BUNDLE_SECTIONS.contains(sectioner)) {
FullyQualifiedGroupKey newFullAggregateGroupKey =
new FullyQualifiedGroupKey(userId, pkgName, sectioner);
if (DEBUG) {
@@ -1204,6 +1229,24 @@ public class GroupHelper {
return notificationsToMove;
}
+ /**
+ * Checks if the original group's summary exists for a notification that was regrouped
+ * @param r notification to check
+ * @param summaryByGroupKey map of the current group summaries
+ * @return true if the original group summary exists
+ */
+ public static boolean isOriginalGroupSummaryPresent(final NotificationRecord r,
+ final ArrayMap<String, NotificationRecord> summaryByGroupKey) {
+ if (r.getSbn().isAppGroup() && r.getNotification().isGroupChild()) {
+ final String oldGroupKey = GroupHelper.getFullAggregateGroupKey(
+ r.getSbn().getPackageName(), r.getOriginalGroupKey(), r.getUserId());
+ NotificationRecord groupSummary = summaryByGroupKey.get(oldGroupKey);
+ // We only care about app-provided valid groups
+ return (groupSummary != null && !GroupHelper.isAggregatedGroup(groupSummary));
+ }
+ return false;
+ }
+
@GuardedBy("mAggregatedNotifications")
private List<NotificationMoveOp> getAutogroupedNotificationsMoveOps(int userId, String pkgName,
ArrayMap<String, NotificationRecord> notificationsToCheck) {
@@ -1298,7 +1341,8 @@ public class GroupHelper {
@GuardedBy("mAggregatedNotifications")
private void moveNotificationsToNewSection(final int userId, final String pkgName,
- final List<NotificationMoveOp> notificationsToMove, int regroupingReason) {
+ final List<NotificationMoveOp> notificationsToMove,
+ final Map<String, Integer> regroupReasons) {
record GroupUpdateOp(FullyQualifiedGroupKey groupKey, NotificationRecord record,
boolean hasSummary) { }
// Bundled operations to apply to groups affected by the channel update
@@ -1317,7 +1361,7 @@ public class GroupHelper {
Log.i(TAG,
"moveNotificationToNewSection: " + record + " " + newFullAggregateGroupKey
+ " from: " + oldFullAggregateGroupKey + " regroupingReason: "
- + regroupingReason);
+ + regroupReasons);
}
// Update/remove aggregate summary for old group
@@ -1347,7 +1391,8 @@ public class GroupHelper {
// after all notifications have been handled
if (newFullAggregateGroupKey != null) {
if (notificationRegroupOnClassification()
- && regroupingReason == REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP) {
+ && regroupReasons.getOrDefault(record.getKey(), REGROUP_REASON_CHANNEL_UPDATE)
+ == REGROUP_REASON_UNBUNDLE_ORIGINAL_GROUP) {
// Just reset override group key, original summary exists
// => will be grouped back to its original group
record.setOverrideGroupKey(null);
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 6d565d2157e1..638b2dd4a7fe 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -111,7 +111,9 @@ import static android.os.UserHandle.USER_SYSTEM;
import static android.service.notification.Adjustment.KEY_SUMMARIZATION;
import static android.service.notification.Adjustment.KEY_TYPE;
import static android.service.notification.Adjustment.TYPE_CONTENT_RECOMMENDATION;
+import static android.service.notification.Adjustment.TYPE_NEWS;
import static android.service.notification.Adjustment.TYPE_PROMOTION;
+import static android.service.notification.Adjustment.TYPE_SOCIAL_MEDIA;
import static android.service.notification.Flags.FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT;
import static android.service.notification.Flags.callstyleCallbackApi;
import static android.service.notification.Flags.notificationClassification;
@@ -346,6 +348,7 @@ import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.messages.nano.SystemMessageProto;
+import com.android.internal.notification.NotificationChannelGroupsHelper;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.SomeArgs;
@@ -491,7 +494,10 @@ public class NotificationManagerService extends SystemService {
};
static final Integer[] DEFAULT_ALLOWED_ADJUSTMENT_KEY_TYPES = new Integer[] {
- TYPE_PROMOTION
+ TYPE_PROMOTION,
+ TYPE_NEWS,
+ TYPE_CONTENT_RECOMMENDATION,
+ TYPE_SOCIAL_MEDIA
};
static final String[] NON_BLOCKABLE_DEFAULT_ROLES = new String[] {
@@ -1929,27 +1935,17 @@ public class NotificationManagerService extends SystemService {
if (DBG) {
Slog.v(TAG, "unclassifyNotification: " + r);
}
-
- boolean hasOriginalSummary = false;
- if (r.getSbn().isAppGroup() && r.getNotification().isGroupChild()) {
- final String oldGroupKey = GroupHelper.getFullAggregateGroupKey(
- r.getSbn().getPackageName(), r.getOriginalGroupKey(), r.getUserId());
- NotificationRecord groupSummary = mSummaryByGroupKey.get(oldGroupKey);
- // We only care about app-provided valid groups
- hasOriginalSummary = (groupSummary != null
- && !GroupHelper.isAggregatedGroup(groupSummary));
- }
-
// Only NotificationRecord's mChannel is updated when bundled, the Notification
// mChannelId will always be the original channel.
String origChannelId = r.getNotification().getChannelId();
NotificationChannel originalChannel = mPreferencesHelper.getNotificationChannel(
r.getSbn().getPackageName(), r.getUid(), origChannelId, false);
String currChannelId = r.getChannel().getId();
- boolean isBundled = NotificationChannel.SYSTEM_RESERVED_IDS.contains(currChannelId);
- if (originalChannel != null && !origChannelId.equals(currChannelId) && isBundled) {
+ boolean isClassified = NotificationChannel.SYSTEM_RESERVED_IDS.contains(currChannelId);
+ if (originalChannel != null && !origChannelId.equals(currChannelId) && isClassified) {
r.updateNotificationChannel(originalChannel);
- mGroupHelper.onNotificationUnbundled(r, hasOriginalSummary);
+ mGroupHelper.onNotificationUnbundled(r,
+ GroupHelper.isOriginalGroupSummaryPresent(r, mSummaryByGroupKey));
}
}
@@ -2034,9 +2030,9 @@ public class NotificationManagerService extends SystemService {
Slog.v(TAG, "reclassifyNotification: " + r);
}
- boolean isBundled = NotificationChannel.SYSTEM_RESERVED_IDS.contains(
+ boolean isClassified = NotificationChannel.SYSTEM_RESERVED_IDS.contains(
r.getChannel().getId());
- if (r.getBundleType() != Adjustment.TYPE_OTHER && !isBundled) {
+ if (r.getBundleType() != Adjustment.TYPE_OTHER && !isClassified) {
final Bundle classifBundle = new Bundle();
classifBundle.putInt(KEY_TYPE, r.getBundleType());
Adjustment adj = new Adjustment(r.getSbn().getPackageName(), r.getKey(),
@@ -3583,7 +3579,7 @@ public class NotificationManagerService extends SystemService {
synchronized (mNotificationLock) {
mGroupHelper.onChannelUpdated(
UserHandle.getUserHandleForUid(uid).getIdentifier(), pkg,
- updatedChannel, mNotificationList);
+ updatedChannel, mNotificationList, mSummaryByGroupKey);
}
}, DELAY_FORCE_REGROUP_TIME);
}
@@ -4531,7 +4527,7 @@ public class NotificationManagerService extends SystemService {
if (key == null) {
return;
}
- mAssistants.setAdjustmentTypeSupportedState(info, key, supported);
+ mAssistants.setAdjustmentTypeSupportedState(info.userid, key, supported);
}
} finally {
Binder.restoreCallingIdentity(identity);
@@ -4574,6 +4570,12 @@ public class NotificationManagerService extends SystemService {
}
@Override
+ public String[] getAdjustmentDeniedPackages(String key) {
+ checkCallerIsSystemOrSystemUiOrShell();
+ return mAssistants.getAdjustmentDeniedPackages(key);
+ }
+
+ @Override
public boolean isAdjustmentSupportedForPackage(String key, String pkg) {
checkCallerIsSystemOrSystemUiOrShell();
return mAssistants.isAdjustmentAllowedForPackage(key, pkg);
@@ -5020,8 +5022,8 @@ public class NotificationManagerService extends SystemService {
public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(
String pkg) {
checkCallerIsSystemOrSameApp(pkg);
- return mPreferencesHelper.getNotificationChannelGroups(
- pkg, Binder.getCallingUid(), false, false, true, true, null);
+ return mPreferencesHelper.getNotificationChannelGroups(pkg, Binder.getCallingUid(),
+ NotificationChannelGroupsHelper.Params.forAllGroups());
}
@Override
@@ -5141,8 +5143,9 @@ public class NotificationManagerService extends SystemService {
public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroupsForPackage(
String pkg, int uid, boolean includeDeleted) {
enforceSystemOrSystemUI("getNotificationChannelGroupsForPackage");
- return mPreferencesHelper.getNotificationChannelGroups(
- pkg, uid, includeDeleted, true, false, true, null);
+ return mPreferencesHelper.getNotificationChannelGroups(pkg, uid,
+ new NotificationChannelGroupsHelper.Params(includeDeleted, true, false, true,
+ null));
}
@Override
@@ -5170,8 +5173,9 @@ public class NotificationManagerService extends SystemService {
}
}
- return mPreferencesHelper.getNotificationChannelGroups(
- pkg, uid, false, true, false, true, recentlySentChannels);
+ return mPreferencesHelper.getNotificationChannelGroups(pkg, uid,
+ NotificationChannelGroupsHelper.Params.onlySpecifiedOrBlockedChannels(
+ recentlySentChannels));
}
@Override
@@ -7024,7 +7028,7 @@ public class NotificationManagerService extends SystemService {
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mNotificationLock) {
- mAssistants.checkServiceTokenLocked(token);
+ ManagedServiceInfo info = mAssistants.checkServiceTokenLocked(token);
int N = mEnqueuedNotifications.size();
for (int i = 0; i < N; i++) {
final NotificationRecord r = mEnqueuedNotifications.get(i);
@@ -7370,6 +7374,10 @@ public class NotificationManagerService extends SystemService {
mAssistants.setPackageOrComponentEnabled(assistant.flattenToString(),
userId, true, granted, userSet);
+ if (android.service.notification.Flags.notificationClassification()) {
+ mAssistants.setNasUnsupportedDefaults(userId);
+ }
+
getContext().sendBroadcastAsUser(
new Intent(ACTION_NOTIFICATION_POLICY_ACCESS_GRANTED_CHANGED)
.setPackage(assistant.getPackageName())
@@ -7401,7 +7409,8 @@ public class NotificationManagerService extends SystemService {
toRemove.add(potentialKey);
}
if (notificationClassification() && potentialKey.equals(KEY_TYPE)) {
- mAssistants.setNasUnsupportedDefaults(r.getSbn().getNormalizedUserId());
+ mAssistants.setAdjustmentTypeSupportedState(
+ r.getSbn().getNormalizedUserId(), potentialKey, true);
if (!mAssistants.isAdjustmentKeyTypeAllowed(adjustments.getInt(KEY_TYPE))) {
toRemove.add(potentialKey);
} else if (notificationClassificationUi()
@@ -7412,6 +7421,8 @@ public class NotificationManagerService extends SystemService {
}
if ((nmSummarization() || nmSummarizationUi())
&& potentialKey.equals(KEY_SUMMARIZATION)) {
+ mAssistants.setAdjustmentTypeSupportedState(
+ r.getSbn().getNormalizedUserId(), potentialKey, true);
if (!mAssistants.isAdjustmentAllowedForPackage(KEY_SUMMARIZATION,
r.getSbn().getPackageName())) {
toRemove.add(potentialKey);
@@ -12069,8 +12080,8 @@ public class NotificationManagerService extends SystemService {
private static final String TAG_DENIED_KEY = "adjustment";
private static final String ATT_DENIED_KEY = "key";
private static final String ATT_DENIED_KEY_APPS = "denied_apps";
- private static final String TAG_ENABLED_TYPES = "enabled_key_types";
- private static final String ATT_NAS_UNSUPPORTED = "nas_unsupported_adjustments";
+ private static final String TAG_ENABLED_TYPES = "enabled_classification_types";
+ private static final String ATT_NAS_UNSUPPORTED = "unsupported_adjustments";
private final Object mLock = new Object();
@@ -12290,6 +12301,16 @@ public class NotificationManagerService extends SystemService {
}
}
+ protected @NonNull String[] getAdjustmentDeniedPackages(@Adjustment.Keys String key) {
+ synchronized (mLock) {
+ if (notificationClassificationUi() || nmSummarization() | nmSummarizationUi()) {
+ return mAdjustmentKeyDeniedPackages.getOrDefault(
+ key, new ArraySet<>()).toArray(new String[0]);
+ }
+ }
+ return new String[]{};
+ }
+
protected @NonNull boolean isAdjustmentAllowedForPackage(@Adjustment.Keys String key,
String pkg) {
synchronized (mLock) {
@@ -12672,10 +12693,6 @@ public class NotificationManagerService extends SystemService {
setNotificationAssistantAccessGrantedForUserInternal(
currentComponent, userId, false, userSet);
}
- } else {
- if (android.service.notification.Flags.notificationClassification()) {
- setNasUnsupportedDefaults(userId);
- }
}
super.setPackageOrComponentEnabled(pkgOrComponent, userId, isPrimary, enabled, userSet);
}
@@ -12708,36 +12725,37 @@ public class NotificationManagerService extends SystemService {
}
}
- @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
@GuardedBy("mNotificationLock")
- public void setAdjustmentTypeSupportedState(ManagedServiceInfo info,
+ public void setAdjustmentTypeSupportedState(@UserIdInt int userId,
@Adjustment.Keys String key, boolean supported) {
- if (!android.service.notification.Flags.notificationClassification()) {
+ if (!(android.service.notification.Flags.notificationClassification()
+ || android.app.Flags.nmSummarizationUi()
+ || android.app.Flags.nmSummarization())) {
return;
}
- setNasUnsupportedDefaults(info.userid);
- HashSet<String> disabledAdjustments = mNasUnsupported.get(info.userid);
+ HashSet<String> disabledAdjustments =
+ mNasUnsupported.getOrDefault(userId, new HashSet<>());
if (supported) {
disabledAdjustments.remove(key);
} else {
disabledAdjustments.add(key);
}
- mNasUnsupported.put(info.userid, disabledAdjustments);
+ mNasUnsupported.put(userId, disabledAdjustments);
handleSavePolicyFile();
}
- @FlaggedApi(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
@GuardedBy("mNotificationLock")
public @NonNull Set<String> getUnsupportedAdjustments(@UserIdInt int userId) {
- if (!android.service.notification.Flags.notificationClassification()) {
+ if (!(android.service.notification.Flags.notificationClassification()
+ || android.app.Flags.nmSummarizationUi()
+ || android.app.Flags.nmSummarization())) {
return new HashSet<>();
}
- setNasUnsupportedDefaults(userId);
- return mNasUnsupported.get(userId);
+ return mNasUnsupported.getOrDefault(userId, new HashSet<>());
}
private void setNasUnsupportedDefaults(@UserIdInt int userId) {
- if (mNasUnsupported != null && !mNasUnsupported.containsKey(userId)) {
+ if (mNasUnsupported != null) {
mNasUnsupported.put(userId, new HashSet(List.of(mDefaultUnsupportedAdjustments)));
handleSavePolicyFile();
}
@@ -12750,10 +12768,8 @@ public class NotificationManagerService extends SystemService {
return;
}
synchronized (mLock) {
- if (mNasUnsupported.containsKey(approvedUserId)) {
- out.attribute(null, ATT_NAS_UNSUPPORTED,
- TextUtils.join(",", mNasUnsupported.get(approvedUserId)));
- }
+ out.attribute(null, ATT_NAS_UNSUPPORTED, TextUtils.join(",",
+ mNasUnsupported.getOrDefault(approvedUserId, new HashSet<>())));
}
}
@@ -12766,8 +12782,15 @@ public class NotificationManagerService extends SystemService {
if (ManagedServices.TAG_MANAGED_SERVICES.equals(tag)) {
final String types = XmlUtils.readStringAttribute(parser, ATT_NAS_UNSUPPORTED);
synchronized (mLock) {
- if (!TextUtils.isEmpty(types)) {
- mNasUnsupported.put(approvedUserId, new HashSet(List.of(types.split(","))));
+ if (types == null) {
+ setNasUnsupportedDefaults(approvedUserId);
+ } else {
+ if (!TextUtils.isEmpty(types)) {
+ mNasUnsupported.put(approvedUserId,
+ new HashSet(List.of(types.split(","))));
+ } else {
+ mNasUnsupported.put(approvedUserId, new HashSet());
+ }
}
}
}
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 7d45cd9752b8..33c94a7e63da 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -92,6 +92,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;
import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags;
import com.android.internal.logging.MetricsLogger;
+import com.android.internal.notification.NotificationChannelGroupsHelper;
import com.android.internal.util.FrameworkStatsLog;
import com.android.internal.util.Preconditions;
import com.android.internal.util.XmlUtils;
@@ -1678,18 +1679,8 @@ public class PreferencesHelper implements RankingConfig {
if (r == null || groupId == null || !r.groups.containsKey(groupId)) {
return null;
}
- NotificationChannelGroup group = r.groups.get(groupId).clone();
- group.setChannels(new ArrayList<>());
- int N = r.channels.size();
- for (int i = 0; i < N; i++) {
- final NotificationChannel nc = r.channels.valueAt(i);
- if (includeDeleted || !nc.isDeleted()) {
- if (groupId.equals(nc.getGroup())) {
- group.addChannel(nc);
- }
- }
- }
- return group;
+ return NotificationChannelGroupsHelper.getGroupWithChannels(groupId,
+ r.channels.values(), r.groups, includeDeleted);
}
}
@@ -1706,51 +1697,16 @@ public class PreferencesHelper implements RankingConfig {
}
public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
- int uid, boolean includeDeleted, boolean includeNonGrouped, boolean includeEmpty,
- boolean includeBlocked, Set<String> activeChannelFilter) {
+ int uid, NotificationChannelGroupsHelper.Params params) {
Objects.requireNonNull(pkg);
- Map<String, NotificationChannelGroup> groups = new ArrayMap<>();
synchronized (mLock) {
PackagePreferences r = getPackagePreferencesLocked(pkg, uid);
if (r == null) {
return ParceledListSlice.emptyList();
}
- NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
- int N = r.channels.size();
- for (int i = 0; i < N; i++) {
- final NotificationChannel nc = r.channels.valueAt(i);
- boolean includeChannel = (includeDeleted || !nc.isDeleted())
- && (activeChannelFilter == null
- || (includeBlocked && nc.getImportance() == IMPORTANCE_NONE)
- || activeChannelFilter.contains(nc.getId()))
- && !SYSTEM_RESERVED_IDS.contains(nc.getId());
- if (includeChannel) {
- if (nc.getGroup() != null) {
- if (r.groups.get(nc.getGroup()) != null) {
- NotificationChannelGroup ncg = groups.get(nc.getGroup());
- if (ncg == null) {
- ncg = r.groups.get(nc.getGroup()).clone();
- ncg.setChannels(new ArrayList<>());
- groups.put(nc.getGroup(), ncg);
- }
- ncg.addChannel(nc);
- }
- } else {
- nonGrouped.addChannel(nc);
- }
- }
- }
- if (includeNonGrouped && nonGrouped.getChannels().size() > 0) {
- groups.put(null, nonGrouped);
- }
- if (includeEmpty) {
- for (NotificationChannelGroup group : r.groups.values()) {
- if (!groups.containsKey(group.getId())) {
- groups.put(group.getId(), group);
- }
- }
- }
- return new ParceledListSlice<>(new ArrayList<>(groups.values()));
+ return new ParceledListSlice<>(
+ NotificationChannelGroupsHelper.getGroupsWithChannels(r.channels.values(),
+ r.groups, params));
}
}
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 8710438d76b3..d6b587b5f16d 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -251,14 +251,17 @@ public final class OverlayManagerService extends SystemService {
private final Object mLock = new Object();
+ @GuardedBy("mLock")
private final AtomicFile mSettingsFile;
private final PackageManagerHelperImpl mPackageManager;
private final UserManagerService mUserManager;
+ @GuardedBy("mLock")
private final OverlayManagerSettings mSettings;
+ @GuardedBy("mLock")
private final OverlayManagerServiceImpl mImpl;
private final OverlayActorEnforcer mActorEnforcer;
@@ -296,7 +299,9 @@ public final class OverlayManagerService extends SystemService {
UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class);
umi.addUserLifecycleListener(new UserLifecycleListener());
- restoreSettings();
+ // No async stuff happening in the constructor yet, so it's OK to call a ...Locked()
+ // method without a lock here.
+ restoreSettingsLocked();
// Wipe all shell overlays on boot, to recover from a potentially broken device
String shellPkgName = TextUtils.emptyIfNull(
@@ -403,6 +408,20 @@ public final class OverlayManagerService extends SystemService {
return userIds;
}
+ /**
+ * Ensure that the caller has permission to interact with the given userId.
+ * If the calling user is not the same as the provided user, the caller needs
+ * to hold the INTERACT_ACROSS_USERS_FULL permission (or be system uid or
+ * root).
+ *
+ * @param userId the user to interact with
+ * @param message message for any SecurityException
+ */
+ static int handleIncomingUser(final int userId, @NonNull final String message) {
+ return ActivityManager.handleIncomingUser(Binder.getCallingPid(),
+ Binder.getCallingUid(), userId, false, true, message, null);
+ }
+
private void handlePackageAdd(String packageName, Bundle extras, int userId) {
final boolean replacing = extras.getBoolean(Intent.EXTRA_REPLACING, false);
if (replacing) {
@@ -902,25 +921,28 @@ public final class OverlayManagerService extends SystemService {
throws RemoteException {
try {
traceBegin(TRACE_TAG_RRO, "OMS#commit " + transaction);
- try {
- executeAllRequests(transaction);
- } catch (Exception e) {
- final long ident = Binder.clearCallingIdentity();
+ synchronized (mLock) {
try {
- restoreSettings();
- } finally {
- Binder.restoreCallingIdentity(ident);
+ executeAllRequestsLocked(transaction);
+ } catch (Exception e) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ restoreSettingsLocked();
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ Slog.d(TAG, "commit failed: " + e.getMessage(), e);
+ throw new SecurityException("commit failed"
+ + (DEBUG || Build.IS_DEBUGGABLE ? ": " + e.getMessage() : ""));
}
- Slog.d(TAG, "commit failed: " + e.getMessage(), e);
- throw new SecurityException("commit failed"
- + (DEBUG || Build.IS_DEBUGGABLE ? ": " + e.getMessage() : ""));
}
} finally {
traceEnd(TRACE_TAG_RRO);
}
}
- private Set<UserPackage> executeRequest(
+ @GuardedBy("mLock")
+ private Set<UserPackage> executeRequestLocked(
@NonNull final OverlayManagerTransaction.Request request)
throws OperationFailedException {
Objects.requireNonNull(request, "Transaction contains a null request");
@@ -995,33 +1017,29 @@ public final class OverlayManagerService extends SystemService {
}
}
- private void executeAllRequests(@NonNull final OverlayManagerTransaction transaction)
+ @GuardedBy("mLock")
+ private void executeAllRequestsLocked(@NonNull final OverlayManagerTransaction transaction)
throws OperationFailedException {
if (DEBUG) {
Slog.d(TAG, "commit " + transaction);
}
- if (transaction == null) {
- throw new IllegalArgumentException("null transaction");
- }
- synchronized (mLock) {
- // execute the requests (as calling user)
- Set<UserPackage> affectedPackagesToUpdate = null;
- for (Iterator<Request> it = transaction.getRequests(); it.hasNext(); ) {
- Request request = it.next();
- affectedPackagesToUpdate = CollectionUtils.addAll(affectedPackagesToUpdate,
- executeRequest(request));
- }
+ // execute the requests (as calling user)
+ Set<UserPackage> affectedPackagesToUpdate = null;
+ for (Iterator<Request> it = transaction.getRequests(); it.hasNext(); ) {
+ Request request = it.next();
+ affectedPackagesToUpdate = CollectionUtils.addAll(affectedPackagesToUpdate,
+ executeRequestLocked(request));
+ }
- // past the point of no return: the entire transaction has been
- // processed successfully, we can no longer fail: continue as
- // system_server
- final long ident = Binder.clearCallingIdentity();
- try {
- updateTargetPackagesLocked(affectedPackagesToUpdate);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
+ // past the point of no return: the entire transaction has been
+ // processed successfully, we can no longer fail: continue as
+ // system_server
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ updateTargetPackagesLocked(affectedPackagesToUpdate);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
}
}
@@ -1037,7 +1055,7 @@ public final class OverlayManagerService extends SystemService {
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
final DumpState dumpState = new DumpState();
- dumpState.setUserId(UserHandle.USER_ALL);
+ int userId = UserHandle.USER_ALL;
int opti = 0;
while (opti < args.length) {
@@ -1064,9 +1082,7 @@ public final class OverlayManagerService extends SystemService {
return;
}
try {
- final int userId = UserHandle.parseUserArg(args[opti]);
- final int realUserId = handleIncomingUser(userId, "dump");
- dumpState.setUserId(realUserId);
+ userId = UserHandle.parseUserArg(args[opti]);
opti++;
} catch (Exception e) {
pw.println("Error: " + e.getMessage());
@@ -1105,6 +1121,9 @@ public final class OverlayManagerService extends SystemService {
}
enforceDumpPermission("dump");
+ final int realUserId = userId != UserHandle.USER_ALL
+ ? handleIncomingUser(userId, "dump") : userId;
+ dumpState.setUserId(realUserId);
synchronized (mLock) {
mImpl.dump(pw, dumpState);
if (dumpState.getPackageName() == null) {
@@ -1114,20 +1133,6 @@ public final class OverlayManagerService extends SystemService {
}
/**
- * Ensure that the caller has permission to interact with the given userId.
- * If the calling user is not the same as the provided user, the caller needs
- * to hold the INTERACT_ACROSS_USERS_FULL permission (or be system uid or
- * root).
- *
- * @param userId the user to interact with
- * @param message message for any SecurityException
- */
- private int handleIncomingUser(final int userId, @NonNull final String message) {
- return ActivityManager.handleIncomingUser(Binder.getCallingPid(),
- Binder.getCallingUid(), userId, false, true, message, null);
- }
-
- /**
* Enforce that the caller holds the DUMP permission (or is system or root).
*
* @param message used as message if SecurityException is thrown
@@ -1447,12 +1452,14 @@ public final class OverlayManagerService extends SystemService {
}
}
+ @GuardedBy("mLock")
private void updateTargetPackagesLocked(@Nullable UserPackage updatedTarget) {
if (updatedTarget != null) {
updateTargetPackagesLocked(Set.of(updatedTarget));
}
}
+ @GuardedBy("mLock")
private void updateTargetPackagesLocked(@Nullable Set<UserPackage> updatedTargets) {
if (CollectionUtils.isEmpty(updatedTargets)) {
return;
@@ -1548,6 +1555,7 @@ public final class OverlayManagerService extends SystemService {
}
@NonNull
+ @GuardedBy("mLock")
private SparseArray<List<String>> updatePackageManagerLocked(
@Nullable Set<UserPackage> targets) {
if (CollectionUtils.isEmpty(targets)) {
@@ -1568,6 +1576,7 @@ public final class OverlayManagerService extends SystemService {
* targetPackageNames: the target themselves and shared libraries)
*/
@NonNull
+ @GuardedBy("mLock")
private List<String> updatePackageManagerLocked(@NonNull Collection<String> targetPackageNames,
final int userId) {
try {
@@ -1623,6 +1632,7 @@ public final class OverlayManagerService extends SystemService {
}
}
+ @GuardedBy("mLock")
private void persistSettingsLocked() {
if (DEBUG) {
Slog.d(TAG, "Writing overlay settings");
@@ -1638,35 +1648,35 @@ public final class OverlayManagerService extends SystemService {
}
}
- private void restoreSettings() {
+ @GuardedBy("mLock")
+ private void restoreSettingsLocked() {
try {
traceBegin(TRACE_TAG_RRO, "OMS#restoreSettings");
- synchronized (mLock) {
- if (!mSettingsFile.getBaseFile().exists()) {
- return;
- }
- try (FileInputStream stream = mSettingsFile.openRead()) {
- mSettings.restore(stream);
- // We might have data for dying users if the device was
- // restarted before we received USER_REMOVED. Remove data for
- // users that will not exist after the system is ready.
+ if (!mSettingsFile.getBaseFile().exists()) {
+ return;
+ }
+ try (FileInputStream stream = mSettingsFile.openRead()) {
+ mSettings.restore(stream);
- final List<UserInfo> liveUsers = mUserManager.getUsers(true /*excludeDying*/);
- final int[] liveUserIds = new int[liveUsers.size()];
- for (int i = 0; i < liveUsers.size(); i++) {
- liveUserIds[i] = liveUsers.get(i).getUserHandle().getIdentifier();
- }
- Arrays.sort(liveUserIds);
+ // We might have data for dying users if the device was
+ // restarted before we received USER_REMOVED. Remove data for
+ // users that will not exist after the system is ready.
- for (int userId : mSettings.getUsers()) {
- if (Arrays.binarySearch(liveUserIds, userId) < 0) {
- mSettings.removeUser(userId);
- }
+ final List<UserInfo> liveUsers = mUserManager.getUsers(true /*excludeDying*/);
+ final int[] liveUserIds = new int[liveUsers.size()];
+ for (int i = 0; i < liveUsers.size(); i++) {
+ liveUserIds[i] = liveUsers.get(i).getUserHandle().getIdentifier();
+ }
+ Arrays.sort(liveUserIds);
+
+ for (int userId : mSettings.getUsers()) {
+ if (Arrays.binarySearch(liveUserIds, userId) < 0) {
+ mSettings.removeUser(userId);
}
- } catch (IOException | XmlPullParserException e) {
- Slog.e(TAG, "failed to restore overlay state", e);
}
+ } catch (IOException | XmlPullParserException e) {
+ Slog.e(TAG, "failed to restore overlay state", e);
}
} finally {
traceEnd(TRACE_TAG_RRO);
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index 0e9ec4d71421..02c0190224d0 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -715,20 +715,11 @@ final class OverlayManagerServiceImpl {
}
void dump(@NonNull final PrintWriter pw, @NonNull DumpState dumpState) {
- Pair<OverlayIdentifier, String> overlayIdmap = null;
- if (dumpState.getPackageName() != null) {
- OverlayIdentifier id = new OverlayIdentifier(dumpState.getPackageName(),
- dumpState.getOverlayName());
- OverlayInfo oi = mSettings.getNullableOverlayInfo(id, USER_SYSTEM);
- if (oi != null) {
- overlayIdmap = new Pair<>(id, oi.baseCodePath);
- }
- }
-
// settings
mSettings.dump(pw, dumpState);
// idmap data
+ final var overlayIdmap = mSettings.getIdentifierAndBaseCodePath(dumpState);
if (dumpState.getField() == null) {
Set<Pair<OverlayIdentifier, String>> allIdmaps = (overlayIdmap != null)
? Set.of(overlayIdmap) : mSettings.getAllIdentifiersAndBaseCodePaths();
diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java
index f9758fcd5d01..e6b1c5f640f2 100644
--- a/services/core/java/com/android/server/om/OverlayManagerSettings.java
+++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java
@@ -212,17 +212,41 @@ final class OverlayManagerSettings {
}
Set<String> getAllBaseCodePaths() {
+ // Overlays installed for multiple users have the same code path, avoid duplicates with Set.
final Set<String> paths = new ArraySet<>();
mItems.forEach(item -> paths.add(item.mBaseCodePath));
return paths;
}
Set<Pair<OverlayIdentifier, String>> getAllIdentifiersAndBaseCodePaths() {
+ // Overlays installed for multiple users have the same code path, avoid duplicates with Set.
final Set<Pair<OverlayIdentifier, String>> set = new ArraySet<>();
- mItems.forEach(item -> set.add(new Pair(item.mOverlay, item.mBaseCodePath)));
+ mItems.forEach(item -> set.add(new Pair<>(item.mOverlay, item.mBaseCodePath)));
return set;
}
+ @Nullable
+ Pair<OverlayIdentifier, String> getIdentifierAndBaseCodePath(@NonNull DumpState dumpState) {
+ if (dumpState.getPackageName() == null) {
+ return null;
+ }
+ OverlayIdentifier id = new OverlayIdentifier(dumpState.getPackageName(),
+ dumpState.getOverlayName());
+ final int userId = dumpState.getUserId();
+ for (int i = 0; i < mItems.size(); i++) {
+ final var item = mItems.get(i);
+ if (userId != UserHandle.USER_ALL && userId != item.mUserId) {
+ continue;
+ }
+ if (!id.equals(item.mOverlay)) {
+ continue;
+ }
+ // Overlays installed for multiple users have the same code path, return first found.
+ return new Pair<>(id, item.mBaseCodePath);
+ }
+ return null;
+ }
+
@NonNull
List<OverlayInfo> removeIf(@NonNull final Predicate<OverlayInfo> predicate, final int userId) {
return removeIf(info -> (predicate.test(info) && info.userId == userId));
diff --git a/services/core/java/com/android/server/om/OverlayManagerShellCommand.java b/services/core/java/com/android/server/om/OverlayManagerShellCommand.java
index b7f21125148c..39a6b1895c3a 100644
--- a/services/core/java/com/android/server/om/OverlayManagerShellCommand.java
+++ b/services/core/java/com/android/server/om/OverlayManagerShellCommand.java
@@ -17,6 +17,7 @@
package com.android.server.om;
import static com.android.internal.content.om.OverlayConfig.PARTITION_ORDER_FILE_PATH;
+import static com.android.server.om.OverlayManagerService.handleIncomingUser;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -145,7 +146,7 @@ final class OverlayManagerShellCommand extends ShellCommand {
out.println(" Load a package and print the value of a given resource");
out.println(" applying the current configuration and enabled overlays.");
out.println(" For a more fine-grained alternative, use 'idmap2 lookup'.");
- out.println(" fabricate [--user USER_ID] [--target-name OVERLAYABLE] --target PACKAGE");
+ out.println(" fabricate [--target-name OVERLAYABLE] --target PACKAGE");
out.println(" --name NAME [--file FILE] ");
out.println(" PACKAGE:TYPE/NAME ENCODED-TYPE-ID|TYPE-NAME ENCODED-VALUE");
out.println(" Create an overlay from a single resource. Caller must be root. Example:");
@@ -160,7 +161,7 @@ final class OverlayManagerShellCommand extends ShellCommand {
final PrintWriter out = getOutPrintWriter();
final PrintWriter err = getErrPrintWriter();
- int userId = UserHandle.USER_SYSTEM;
+ int userId = UserHandle.USER_CURRENT;
String opt;
while ((opt = getNextOption()) != null) {
switch (opt) {
@@ -234,7 +235,7 @@ final class OverlayManagerShellCommand extends ShellCommand {
private int runEnableDisable(final boolean enable) throws RemoteException {
final PrintWriter err = getErrPrintWriter();
- int userId = UserHandle.USER_SYSTEM;
+ int userId = UserHandle.USER_CURRENT;
String opt;
while ((opt = getNextOption()) != null) {
switch (opt) {
@@ -269,7 +270,6 @@ final class OverlayManagerShellCommand extends ShellCommand {
return 1;
}
- int userId = UserHandle.USER_SYSTEM;
String targetPackage = "";
String targetOverlayable = "";
String name = "";
@@ -278,9 +278,6 @@ final class OverlayManagerShellCommand extends ShellCommand {
String config = null;
while ((opt = getNextOption()) != null) {
switch (opt) {
- case "--user":
- userId = UserHandle.parseUserArg(getNextArgRequired());
- break;
case "--target":
targetPackage = getNextArgRequired();
break;
@@ -442,7 +439,7 @@ final class OverlayManagerShellCommand extends ShellCommand {
private int runEnableExclusive() throws RemoteException {
final PrintWriter err = getErrPrintWriter();
- int userId = UserHandle.USER_SYSTEM;
+ int userId = UserHandle.USER_CURRENT;
boolean inCategory = false;
String opt;
while ((opt = getNextOption()) != null) {
@@ -469,7 +466,7 @@ final class OverlayManagerShellCommand extends ShellCommand {
private int runSetPriority() throws RemoteException {
final PrintWriter err = getErrPrintWriter();
- int userId = UserHandle.USER_SYSTEM;
+ int userId = UserHandle.USER_CURRENT;
String opt;
while ((opt = getNextOption()) != null) {
switch (opt) {
@@ -498,7 +495,7 @@ final class OverlayManagerShellCommand extends ShellCommand {
final PrintWriter out = getOutPrintWriter();
final PrintWriter err = getErrPrintWriter();
- int userId = UserHandle.USER_SYSTEM;
+ int userId = UserHandle.USER_CURRENT;
boolean verbose = false;
String opt;
while ((opt = getNextOption()) != null) {
@@ -525,15 +522,16 @@ final class OverlayManagerShellCommand extends ShellCommand {
return 1;
}
+ final int realUserId = handleIncomingUser(userId, "runLookup");
final Resources res;
try {
res = mContext
- .createContextAsUser(UserHandle.of(userId), /* flags */ 0)
+ .createContextAsUser(UserHandle.of(realUserId), /* flags */ 0)
.getPackageManager()
.getResourcesForApplication(packageToLoad);
} catch (PackageManager.NameNotFoundException e) {
err.println(String.format("Error: failed to get resources for package %s for user %d",
- packageToLoad, userId));
+ packageToLoad, realUserId));
return 1;
}
final AssetManager assets = res.getAssets();
diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java
index c62aaebf673b..f88681dbcaeb 100644
--- a/services/core/java/com/android/server/pm/UserManagerInternal.java
+++ b/services/core/java/com/android/server/pm/UserManagerInternal.java
@@ -23,7 +23,9 @@ import android.content.Context;
import android.content.pm.LauncherUserInfo;
import android.content.pm.UserInfo;
import android.content.pm.UserProperties;
+import android.content.res.Resources;
import android.graphics.Bitmap;
+import android.multiuser.Flags;
import android.os.Bundle;
import android.os.UserManager;
import android.util.DebugUtils;
@@ -351,8 +353,10 @@ public abstract class UserManagerInternal {
boolean excludePreCreated);
/**
- * Returns an array of ids for profiles associated with the specified user including the user
- * itself.
+ * Returns a list of the users that are associated with the specified user, including the user
+ * itself. This includes the user, its profiles, its parent, and its parent's other profiles,
+ * as applicable.
+ *
* <p>Note that this includes all profile types (not including Restricted profiles).
*
* @param userId id of the user to return profiles for
@@ -617,4 +621,14 @@ public abstract class UserManagerInternal {
* if there is no such user.
*/
public abstract @UserIdInt int getCommunalProfileId();
+
+ /**
+ * Checks whether to show a notification for sounds (e.g., alarms, timers, etc.) from
+ * background users.
+ */
+ public static boolean shouldShowNotificationForBackgroundUserSounds() {
+ return Flags.addUiForSoundsFromBackgroundUsers() && Resources.getSystem().getBoolean(
+ com.android.internal.R.bool.config_showNotificationForBackgroundUserAlarms)
+ && UserManager.supportsMultipleUsers();
+ }
}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 8cbccf5feead..7de7dde8c260 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -33,7 +33,6 @@ import static android.os.UserManager.SYSTEM_USER_MODE_EMULATION_PROPERTY;
import static android.os.UserManager.USER_OPERATION_ERROR_UNKNOWN;
import static android.os.UserManager.USER_OPERATION_ERROR_USER_RESTRICTED;
import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE;
-import static android.os.UserManager.supportsMultipleUsers;
import static android.provider.Settings.Secure.HIDE_PRIVATESPACE_ENTRY_POINT;
import static com.android.internal.app.SetScreenLockDialogActivity.EXTRA_ORIGIN_USER_ID;
@@ -60,7 +59,6 @@ import android.annotation.RequiresPermission;
import android.annotation.SpecialUsers.CanBeALL;
import android.annotation.SpecialUsers.CanBeCURRENT;
import android.annotation.SpecialUsers.CanBeNULL;
-import android.annotation.SpecialUsers.CannotBeSpecialUser;
import android.annotation.StringRes;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
@@ -1161,7 +1159,7 @@ public class UserManagerService extends IUserManager.Stub {
showHsumNotificationIfNeeded();
- if (shouldShowNotificationForBackgroundUserSounds()) {
+ if (UserManagerInternal.shouldShowNotificationForBackgroundUserSounds()) {
new BackgroundUserSoundNotifier(mContext);
}
}
@@ -1638,7 +1636,7 @@ public class UserManagerService extends IUserManager.Stub {
final int userSize = mUsers.size();
for (int i = 0; i < userSize; i++) {
UserInfo profile = mUsers.valueAt(i).info;
- if (!isProfileOf(user, profile)) {
+ if (!isSameProfileGroup(user, profile)) {
continue;
}
if (enabledOnly && !profile.isEnabled()) {
@@ -1706,22 +1704,18 @@ public class UserManagerService extends IUserManager.Stub {
return isSameProfileGroupNoChecks(userId, otherUserId);
}
- /**
- * Returns whether users are in the same non-empty profile group.
- * Currently, false if empty profile group, even if they are the same user, for whatever reason.
- */
+ /** Returns whether users are in the same profile group. */
private boolean isSameProfileGroupNoChecks(@UserIdInt int userId, int otherUserId) {
synchronized (mUsersLock) {
UserInfo userInfo = getUserInfoLU(userId);
- if (userInfo == null || userInfo.profileGroupId == UserInfo.NO_PROFILE_GROUP_ID) {
+ if (userInfo == null) {
return false;
}
UserInfo otherUserInfo = getUserInfoLU(otherUserId);
- if (otherUserInfo == null
- || otherUserInfo.profileGroupId == UserInfo.NO_PROFILE_GROUP_ID) {
+ if (otherUserInfo == null) {
return false;
}
- return userInfo.profileGroupId == otherUserInfo.profileGroupId;
+ return isSameProfileGroup(userInfo, otherUserInfo);
}
}
@@ -1780,10 +1774,10 @@ public class UserManagerService extends IUserManager.Stub {
}
}
- private static boolean isProfileOf(UserInfo user, UserInfo profile) {
- return user.id == profile.id ||
- (user.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID
- && user.profileGroupId == profile.profileGroupId);
+ private static boolean isSameProfileGroup(@NonNull UserInfo user1, @NonNull UserInfo user2) {
+ return user1.id == user2.id ||
+ (user1.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID
+ && user1.profileGroupId == user2.profileGroupId);
}
private String getAvailabilityIntentAction(boolean enableQuietMode, boolean useManagedActions) {
@@ -7551,6 +7545,10 @@ public class UserManagerService extends IUserManager.Stub {
pw.println(" (and being updated after boot)");
}
}
+ if (isHeadlessSystemUserMode) {
+ pw.println(" Can switch to headless system user: " + Resources.getSystem()
+ .getBoolean(com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser));
+ }
pw.println(" User version: " + mUserVersion);
pw.println(" Owner name: " + getOwnerName());
if (DBG_ALLOCATION) {
@@ -8413,21 +8411,27 @@ public class UserManagerService extends IUserManager.Stub {
}
/**
- * Checks if the given user has a profile associated with it.
- * @param userId The parent user
- * @return
+ * Formerly: Checks if the given user has a profile associated with it.
+ * Now: Just throws. Do not use it.
+ * @param userId The parent user (passing in a profile user is not supported)
+ * @deprecated
*/
boolean hasProfile(@UserIdInt int userId) {
- synchronized (mUsersLock) {
- UserInfo userInfo = getUserInfoLU(userId);
- final int userSize = mUsers.size();
- for (int i = 0; i < userSize; i++) {
- UserInfo profile = mUsers.valueAt(i).info;
- if (userId != profile.id && isProfileOf(userInfo, profile)) {
- return true;
+ if (!android.content.pm.Flags.removeCrossUserPermissionHack()) {
+ synchronized (mUsersLock) {
+ UserInfo userInfo = getUserInfoLU(userId);
+ final int userSize = mUsers.size();
+ for (int i = 0; i < userSize; i++) {
+ UserInfo profile = mUsers.valueAt(i).info;
+ if (userId != profile.id && isSameProfileGroup(userInfo, profile)) {
+ return true;
+ }
}
+ return false;
}
- return false;
+ } else {
+ // TODO(b/332664521): Remove this method entirely. It is no longer used.
+ throw new UnsupportedOperationException();
}
}
@@ -8499,16 +8503,6 @@ public class UserManagerService extends IUserManager.Stub {
}
/**
- * Checks whether to show a notification for sounds (e.g., alarms, timers, etc.) from
- * background users.
- */
- public static boolean shouldShowNotificationForBackgroundUserSounds() {
- return Flags.addUiForSoundsFromBackgroundUsers() && Resources.getSystem().getBoolean(
- com.android.internal.R.bool.config_showNotificationForBackgroundUserAlarms)
- && supportsMultipleUsers();
- }
-
- /**
* Returns instance of {@link com.android.server.pm.UserJourneyLogger}.
*/
public UserJourneyLogger getUserJourneyLogger() {
diff --git a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
index 2bc6d53147fb..a1082481abb8 100644
--- a/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
+++ b/services/core/java/com/android/server/pm/UserRestrictionsUtils.java
@@ -309,7 +309,8 @@ public class UserRestrictionsUtils {
* in settings. So it is handled separately.
*/
private static final Set<String> DEFAULT_ENABLED_FOR_MANAGED_PROFILES = Sets.newArraySet(
- UserManager.DISALLOW_BLUETOOTH_SHARING
+ UserManager.DISALLOW_BLUETOOTH_SHARING,
+ UserManager.DISALLOW_DEBUGGING_FEATURES
);
/**
diff --git a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
index 1b7c7ad94dc9..c0441e4e4d46 100644
--- a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java
@@ -155,11 +155,14 @@ public class AndroidPackageUtils {
public static NativeLibraryHelper.Handle createNativeLibraryHandle(AndroidPackage pkg)
throws IOException {
+ boolean pageSizeCompatDisabled = pkg.getPageSizeAppCompatFlags()
+ == ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_DISABLED;
return NativeLibraryHelper.Handle.create(
AndroidPackageUtils.getAllCodePaths(pkg),
pkg.isMultiArch(),
pkg.isExtractNativeLibrariesRequested(),
- pkg.isDebuggable()
+ pkg.isDebuggable(),
+ pageSizeCompatDisabled
);
}
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
index b905041b59e5..5c5a9c1b6c05 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java
@@ -271,8 +271,9 @@ public class PermissionManagerService extends IPermissionManager.Stub {
@NonNull String permissionName, int deviceId) {
Objects.requireNonNull(permissionName, "permission can't be null.");
Objects.requireNonNull(packageName, "package name can't be null.");
+
return mPermissionManagerServiceImpl.getPermissionRequestState(packageName, permissionName,
- getPersistentDeviceId(deviceId));
+ deviceId, getPersistentDeviceId(deviceId));
}
private String getPersistentDeviceId(int deviceId) {
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index ca70bddc5ac1..e51ec04e60fe 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -1014,7 +1014,8 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt
}
@Override
- public int getPermissionRequestState(String packageName, String permName, String deviceId) {
+ public int getPermissionRequestState(String packageName, String permName, int deviceId,
+ String persistentDeviceId) {
throw new IllegalStateException("getPermissionRequestState is not supported.");
}
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java
index b607832767a1..3d295f773805 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceInterface.java
@@ -415,7 +415,7 @@ public interface PermissionManagerServiceInterface extends PermissionManagerInte
* for permission request permission flow.
*/
int getPermissionRequestState(@NonNull String packageName, @NonNull String permName,
- @NonNull String deviceId);
+ int deviceId, @NonNull String persistentDeviceId);
/**
* Gets the permission states for requested package, persistent device and user.
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java
index ba5e97e7b113..f5764006e766 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java
@@ -247,10 +247,12 @@ public class PermissionManagerServiceLoggingDecorator implements PermissionManag
}
@Override
- public int getPermissionRequestState(String packageName, String permName, String deviceId) {
+ public int getPermissionRequestState(String packageName, String permName, int deviceId,
+ String persistentDeviceId) {
Log.i(LOG_TAG, "checkUidPermissionState(permName = " + permName + ", deviceId = "
- + deviceId + ", packageName = " + packageName + ")");
- return mService.getPermissionRequestState(packageName, permName, deviceId);
+ + persistentDeviceId + ", packageName = " + packageName + ")");
+ return mService.getPermissionRequestState(
+ packageName, permName, deviceId, persistentDeviceId);
}
@Override
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java
index 008c14db8b65..21a357025cfb 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTestingShim.java
@@ -319,8 +319,10 @@ public class PermissionManagerServiceTestingShim implements PermissionManagerSer
}
@Override
- public int getPermissionRequestState(String packageName, String permName, String deviceId) {
- return mNewImplementation.getPermissionRequestState(packageName, permName, deviceId);
+ public int getPermissionRequestState(String packageName, String permName, int deviceId,
+ String persistentDeviceId) {
+ return mNewImplementation.getPermissionRequestState(
+ packageName, permName, deviceId, persistentDeviceId);
}
@Override
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java
index 2a47f51da951..e51afb0f66c5 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceTracingDecorator.java
@@ -347,11 +347,13 @@ public class PermissionManagerServiceTracingDecorator implements PermissionManag
@Override
- public int getPermissionRequestState(String packageName, String permName, String deviceId) {
+ public int getPermissionRequestState(String packageName, String permName, int deviceId,
+ String persistentDeviceId) {
Trace.traceBegin(TRACE_TAG,
"TaggedTracingPermissionManagerServiceImpl#checkUidPermissionState");
try {
- return mService.getPermissionRequestState(packageName, permName, deviceId);
+ return mService.getPermissionRequestState(
+ packageName, permName, deviceId, persistentDeviceId);
} finally {
Trace.traceEnd(TRACE_TAG);
}
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index 090707db50a5..8fae875eb29b 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -5979,10 +5979,19 @@ public final class PowerManagerService extends SystemService
if (uids != null) {
ws = new WorkSource();
- // XXX should WorkSource have a way to set uids as an int[] instead of adding them
- // one at a time?
- for (int uid : uids) {
- ws.add(uid);
+ if (mFeatureFlags.isWakelockAttributionViaWorkchainEnabled()) {
+ int callingUid = Binder.getCallingUid();
+ for (int uid : uids) {
+ WorkChain workChain = ws.createWorkChain();
+ workChain.addNode(uid, null);
+ workChain.addNode(callingUid, null);
+ }
+ } else {
+ // XXX should WorkSource have a way to set uids as an int[] instead of
+ // adding them one at a time?
+ for (int uid : uids) {
+ ws.add(uid);
+ }
}
}
updateWakeLockWorkSource(lock, ws, null);
diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
index 42b44013bea2..ebc50fd85f24 100644
--- a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
+++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
@@ -63,6 +63,10 @@ public class PowerManagerFlags {
private final FlagState mMoveWscLoggingToNotifier =
new FlagState(Flags.FLAG_MOVE_WSC_LOGGING_TO_NOTIFIER, Flags::moveWscLoggingToNotifier);
+ private final FlagState mWakelockAttributionViaWorkchain =
+ new FlagState(Flags.FLAG_WAKELOCK_ATTRIBUTION_VIA_WORKCHAIN,
+ Flags::wakelockAttributionViaWorkchain);
+
/** Returns whether early-screen-timeout-detector is enabled on not. */
public boolean isEarlyScreenTimeoutDetectorEnabled() {
return mEarlyScreenTimeoutDetectorFlagState.isEnabled();
@@ -110,6 +114,13 @@ public class PowerManagerFlags {
}
/**
+ * @return Whether the wakelock attribution via workchain is enabled
+ */
+ public boolean isWakelockAttributionViaWorkchainEnabled() {
+ return mWakelockAttributionViaWorkchain.isEnabled();
+ }
+
+ /**
* dumps all flagstates
* @param pw printWriter
*/
@@ -120,6 +131,7 @@ public class PowerManagerFlags {
pw.println(" " + mPerDisplayWakeByTouch);
pw.println(" " + mFrameworkWakelockInfo);
pw.println(" " + mMoveWscLoggingToNotifier);
+ pw.println(" " + mWakelockAttributionViaWorkchain);
}
private static class FlagState {
diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig
index 613daf820e34..fefe195dc337 100644
--- a/services/core/java/com/android/server/power/feature/power_flags.aconfig
+++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig
@@ -23,6 +23,17 @@ flag {
}
flag {
+ name: "wakelock_attribution_via_workchain"
+ namespace: "power"
+ description: "Enables the attribution of wakelocks via WorkChain for updateWakelockUids"
+ bug: "331304805"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "improve_wakelock_latency"
namespace: "power"
description: "Feature flag for tracking the optimizations to improve the latency of acquiring and releasing a wakelock."
diff --git a/services/core/java/com/android/server/power/stats/WakelockStatsFrameworkEvents.java b/services/core/java/com/android/server/power/stats/WakelockStatsFrameworkEvents.java
index f387feca05f2..a2971f302327 100644
--- a/services/core/java/com/android/server/power/stats/WakelockStatsFrameworkEvents.java
+++ b/services/core/java/com/android/server/power/stats/WakelockStatsFrameworkEvents.java
@@ -272,14 +272,10 @@ public class WakelockStatsFrameworkEvents {
WakeLockStats extraTime =
openOverflowStats.computeIfAbsent(key, k -> new WakeLockStats());
- stats.uptimeMillis += openWakeLockUptime + extraTime.uptimeMillis;
-
- logger.logResult(
- key.getUid(),
- key.getTag(),
- key.getPowerManagerWakeLockLevel(),
- stats.uptimeMillis,
- stats.completedCount);
+ long totalUpdate = openWakeLockUptime + stats.uptimeMillis + extraTime.uptimeMillis;
+ long totalCount = stats.completedCount + extraTime.completedCount;
+ logger.logResult(key.getUid(), key.getTag(), key.getPowerManagerWakeLockLevel(),
+ totalUpdate, totalCount);
}
}
}
diff --git a/services/core/java/com/android/server/slice/SlicePermissionManager.java b/services/core/java/com/android/server/slice/SlicePermissionManager.java
index 343d2e353abb..d118eaea37d9 100644
--- a/services/core/java/com/android/server/slice/SlicePermissionManager.java
+++ b/services/core/java/com/android/server/slice/SlicePermissionManager.java
@@ -29,6 +29,7 @@ import android.util.Log;
import android.util.Slog;
import android.util.Xml.Encoding;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.XmlUtils;
import com.android.server.slice.SliceProviderPermissions.SliceAuthority;
@@ -76,8 +77,11 @@ public class SlicePermissionManager implements DirtyTracker {
private final File mSliceDir;
private final Context mContext;
private final Handler mHandler;
+ @GuardedBy("itself")
private final ArrayMap<PkgUser, SliceProviderPermissions> mCachedProviders = new ArrayMap<>();
+ @GuardedBy("itself")
private final ArrayMap<PkgUser, SliceClientPermissions> mCachedClients = new ArrayMap<>();
+ @GuardedBy("this")
private final ArraySet<Persistable> mDirty = new ArraySet<>();
@VisibleForTesting
@@ -354,14 +358,22 @@ public class SlicePermissionManager implements DirtyTracker {
// use addPersistableDirty(); this is just for tests
@VisibleForTesting
void addDirtyImmediate(Persistable obj) {
- mDirty.add(obj);
+ synchronized (this) {
+ mDirty.add(obj);
+ }
}
private void handleRemove(PkgUser pkgUser) {
getFile(SliceClientPermissions.getFileName(pkgUser)).delete();
getFile(SliceProviderPermissions.getFileName(pkgUser)).delete();
- mDirty.remove(mCachedClients.remove(pkgUser));
- mDirty.remove(mCachedProviders.remove(pkgUser));
+ synchronized (this) {
+ synchronized (mCachedClients) {
+ mDirty.remove(mCachedClients.remove(pkgUser));
+ }
+ synchronized (mCachedProviders) {
+ mDirty.remove(mCachedProviders.remove(pkgUser));
+ }
+ }
}
private final class H extends Handler {
@@ -379,7 +391,9 @@ public class SlicePermissionManager implements DirtyTracker {
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_ADD_DIRTY:
- mDirty.add((Persistable) msg.obj);
+ synchronized (SlicePermissionManager.this) {
+ mDirty.add((Persistable) msg.obj);
+ }
break;
case MSG_PERSIST:
handlePersist();
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index a5805043ac42..804cf4663bfd 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -79,7 +79,7 @@ import com.android.internal.app.IBatteryStats;
import com.android.internal.util.DumpUtils;
import com.android.server.SystemService;
import com.android.server.pm.BackgroundUserSoundNotifier;
-import com.android.server.pm.UserManagerService;
+import com.android.server.pm.UserManagerInternal;
import com.android.server.vibrator.VibrationSession.CallerInfo;
import com.android.server.vibrator.VibrationSession.DebugInfo;
import com.android.server.vibrator.VibrationSession.Status;
@@ -201,7 +201,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
VibratorManagerService.this::shouldCancelOnScreenOffLocked,
Status.CANCELLED_BY_SCREEN_OFF);
}
- } else if (UserManagerService.shouldShowNotificationForBackgroundUserSounds()
+ } else if (UserManagerInternal.shouldShowNotificationForBackgroundUserSounds()
&& intent.getAction().equals(BackgroundUserSoundNotifier.ACTION_MUTE_SOUND)) {
synchronized (mLock) {
maybeClearCurrentAndNextSessionsLocked(
@@ -325,7 +325,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_SCREEN_OFF);
- if (UserManagerService.shouldShowNotificationForBackgroundUserSounds()) {
+ if (UserManagerInternal.shouldShowNotificationForBackgroundUserSounds()) {
filter.addAction(BackgroundUserSoundNotifier.ACTION_MUTE_SOUND);
}
context.registerReceiver(mIntentReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index 09b10739d469..d9c79b5c40bb 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -781,13 +781,71 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
if (mLastWallpaper == null || mFallbackWallpaper == null) return;
final WallpaperConnection systemConnection = mLastWallpaper.connection;
final WallpaperConnection fallbackConnection = mFallbackWallpaper.connection;
+ final WallpaperConnection lockConnection;
+ if (mLastLockWallpaper != null) {
+ lockConnection = mLastLockWallpaper.connection;
+ } else {
+ lockConnection = null;
+ }
if (fallbackConnection == null) {
Slog.w(TAG, "Fallback wallpaper connection has not been created yet!!");
return;
}
- // TODO(b/384520326) Passing DEFAULT_DISPLAY temporarily before we revamp the
- // multi-display supports.
- if (isWallpaperCompatibleForDisplay(DEFAULT_DISPLAY, systemConnection)) {
+
+ if (enableConnectedDisplaysWallpaper()) {
+ mWallpaperDisplayHelper.forEachDisplayData(displayData -> {
+ int displayId = displayData.mDisplayId;
+ // 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.
+ if (systemConnection.containsDisplay(displayId)
+ && (lockConnection == null || lockConnection.containsDisplay(displayId))) {
+ DisplayConnector fallbackConnector =
+ mFallbackWallpaper.connection.mDisplayConnector.get(displayId);
+ if (fallbackConnector != null && fallbackConnector.mEngine != null) {
+ fallbackConnector.disconnectLocked(mFallbackWallpaper.connection);
+ mFallbackWallpaper.connection.mDisplayConnector.remove(displayId);
+ }
+ return;
+ }
+
+ // Identify if the fallback wallpaper should be use for lock or system or both.
+ int which = 0;
+ if (!systemConnection.containsDisplay(displayId)) {
+ which |= FLAG_SYSTEM;
+ }
+ if (lockConnection == null || !lockConnection.containsDisplay(displayId)) {
+ which |= FLAG_LOCK;
+ }
+ if (mFallbackWallpaper.connection.containsDisplay(displayId)) {
+ // For existing fallback wallpaper connection, update the `which` flags.
+ DisplayConnector fallbackConnector =
+ mFallbackWallpaper.connection.mDisplayConnector.get(displayId);
+ try {
+ if (fallbackConnector != null && fallbackConnector.mEngine != null
+ && fallbackConnector.mWhich != which) {
+ fallbackConnector.mEngine.setWallpaperFlags(which);
+ mWindowManagerInternal.setWallpaperShowWhenLocked(
+ fallbackConnector.mToken,
+ /* showWhenLocked= */ (which & FLAG_LOCK) != 0);
+ fallbackConnector.mWhich = which;
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to update fallback wallpaper engine flags", e);
+ }
+ } else {
+ // For new fallback connection, establish the connection with the desired
+ // `which` flag.
+ DisplayConnector fallbackConnector =
+ mFallbackWallpaper.connection.getDisplayConnectorOrCreate(displayId);
+ if (fallbackConnector != null && fallbackConnector.mEngine != null) {
+ fallbackConnector.mWhich = which;
+ fallbackConnector.connectLocked(mFallbackWallpaper.connection,
+ mFallbackWallpaper);
+ }
+ }
+ });
+ } else if (isWallpaperCompatibleForDisplay(DEFAULT_DISPLAY, systemConnection)) {
if (fallbackConnection.mDisplayConnector.size() != 0) {
fallbackConnection.forEachDisplayConnector(connector -> {
if (connector.mEngine != null) {
@@ -820,8 +878,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
boolean mDimensionsChanged;
boolean mPaddingChanged;
- DisplayConnector(int displayId) {
+ // This field is added for the fallback wallpaper, which may have a different which flag for
+ // a different display.
+ int mWhich;
+
+ DisplayConnector(int displayId, int which) {
mDisplayId = displayId;
+ mWhich = which;
}
void ensureStatusHandled() {
@@ -850,13 +913,17 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
Slog.w(TAG, "WallpaperService is not connected yet");
return;
}
+ int which = wallpaper.mWhich;
+ if (enableConnectedDisplaysWallpaper()) {
+ which = mWhich;
+ }
TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG);
t.traceBegin("WPMS.connectLocked-" + wallpaper.getComponent());
if (DEBUG) Slog.v(TAG, "Adding window token: " + mToken);
mWindowManagerInternal.addWindowToken(mToken, TYPE_WALLPAPER, mDisplayId,
null /* options */);
mWindowManagerInternal.setWallpaperShowWhenLocked(
- mToken, (wallpaper.mWhich & FLAG_LOCK) != 0);
+ mToken, (which & FLAG_LOCK) != 0);
if (multiCrop() && mImageWallpaper.equals(wallpaper.getComponent())) {
mWindowManagerInternal.setWallpaperCropHints(mToken,
mWallpaperCropper.getRelativeCropHints(wallpaper));
@@ -868,16 +935,15 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
try {
if (liveWallpaperContentHandling()) {
connection.mService.attach(connection, mToken, TYPE_WALLPAPER, false,
- wpdData.mWidth, wpdData.mHeight,
- wpdData.mPadding, mDisplayId, wallpaper.mWhich, connection.mInfo,
- wallpaper.getDescription());
+ wpdData.mWidth, wpdData.mHeight, wpdData.mPadding, mDisplayId, which,
+ connection.mInfo, wallpaper.getDescription());
} else {
WallpaperDescription desc = new WallpaperDescription.Builder().setComponent(
(connection.mInfo != null) ? connection.mInfo.getComponent()
: null).build();
connection.mService.attach(connection, mToken, TYPE_WALLPAPER, false,
- wpdData.mWidth, wpdData.mHeight,
- wpdData.mPadding, mDisplayId, wallpaper.mWhich, connection.mInfo, desc);
+ wpdData.mWidth, wpdData.mHeight, wpdData.mPadding, mDisplayId, which,
+ connection.mInfo, desc);
}
} catch (RemoteException e) {
Slog.w(TAG, "Failed attaching wallpaper on display", e);
@@ -980,7 +1046,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
final int displayId = display.getDisplayId();
final DisplayConnector connector = mDisplayConnector.get(displayId);
if (connector == null) {
- mDisplayConnector.append(displayId, new DisplayConnector(displayId));
+ mDisplayConnector.append(displayId,
+ new DisplayConnector(displayId, mWallpaper.mWhich));
}
}
}
@@ -1006,7 +1073,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
DisplayConnector connector = mDisplayConnector.get(displayId);
if (connector == null) {
if (mWallpaperDisplayHelper.isUsableDisplay(displayId, mClientUid)) {
- connector = new DisplayConnector(displayId);
+ connector = new DisplayConnector(displayId, mWallpaper.mWhich);
mDisplayConnector.append(displayId, connector);
}
}
@@ -1364,6 +1431,17 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
Slog.v(TAG, "static system+lock to system failure");
}
WallpaperData currentSystem = mWallpaperMap.get(mNewWallpaper.userId);
+ // In the constructor, we copied the system+lock wallpaper to
+ // mOriginalSystem. However, the copied WallpaperData#connection is a
+ // reference, not a deep copy. This means
+ // currentSystem.connection.mWallpaper points to mOriginalSystem, so
+ // changes to currentSystem.mWhich alone won't update the corresponding
+ // flag in currentSystem.connection.mWallpaper.mWhich. Let's point
+ // currentSystem.connection.mWallpaper back to currentSystem.
+ if (enableConnectedDisplaysWallpaper()
+ && currentSystem.connection != null) {
+ currentSystem.connection.mWallpaper = currentSystem;
+ }
currentSystem.mWhich = FLAG_SYSTEM | FLAG_LOCK;
updateEngineFlags(currentSystem);
mLockWallpaperMap.remove(mNewWallpaper.userId);
@@ -1385,6 +1463,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
WallpaperData currentSystem = mWallpaperMap.get(mNewWallpaper.userId);
if (currentSystem.wallpaperId == mOriginalSystem.wallpaperId) {
+ // Fixing the reference, see above for more details.
+ if (enableConnectedDisplaysWallpaper()
+ && currentSystem.connection != null) {
+ currentSystem.connection.mWallpaper = currentSystem;
+ }
currentSystem.mWhich = FLAG_SYSTEM;
updateEngineFlags(currentSystem);
}
@@ -3814,6 +3897,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
wallpaper.connection.forEachDisplayConnector(
connector -> {
try {
+ connector.mWhich = wallpaper.mWhich;
if (connector.mEngine != null) {
connector.mEngine.setWallpaperFlags(wallpaper.mWhich);
mWindowManagerInternal.setWallpaperShowWhenLocked(
@@ -3949,8 +4033,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
if (mLastWallpaper == null) {
return;
}
+ int useFallbackWallpaperWhich = 0;
if (enableConnectedDisplaysWallpaper()) {
- int useFallbackWallpaperWhich = 0;
List<WallpaperData> wallpapers = new ArrayList<>();
wallpapers.add(mLastWallpaper);
// If the system and the lock wallpapers are not the same, we should also
@@ -3981,7 +4065,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
|| mFallbackWallpaper.connection == null) {
return;
}
- mFallbackWallpaper.mWhich = useFallbackWallpaperWhich;
} else {
if (isWallpaperCompatibleForDisplay(displayId, mLastWallpaper.connection)) {
final DisplayConnector connector =
@@ -3999,6 +4082,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
final DisplayConnector connector = mFallbackWallpaper
.connection.getDisplayConnectorOrCreate(displayId);
if (connector == null) return;
+ connector.mWhich = useFallbackWallpaperWhich;
connector.connectLocked(mFallbackWallpaper.connection, mFallbackWallpaper);
} else {
Slog.w(TAG, "No wallpaper can be added to the new display");
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 89b46bc4eba4..0024a4166e71 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -178,6 +178,7 @@ import static com.android.server.wm.ActivityRecordProto.PROC_ID;
import static com.android.server.wm.ActivityRecordProto.PROVIDES_MAX_BOUNDS;
import static com.android.server.wm.ActivityRecordProto.REPORTED_DRAWN;
import static com.android.server.wm.ActivityRecordProto.REPORTED_VISIBLE;
+import static com.android.server.wm.ActivityRecordProto.REQUEST_OPEN_IN_BROWSER_EDUCATION_TIMESTAMP;
import static com.android.server.wm.ActivityRecordProto.SHOULD_ENABLE_USER_ASPECT_RATIO_SETTINGS;
import static com.android.server.wm.ActivityRecordProto.SHOULD_FORCE_ROTATE_FOR_CAMERA_COMPAT;
import static com.android.server.wm.ActivityRecordProto.SHOULD_IGNORE_ORIENTATION_REQUEST_LOOP;
@@ -5604,7 +5605,6 @@ final class ActivityRecord extends WindowToken {
}
mAtmService.mBackNavigationController.onAppVisibilityChanged(this, visible);
- onChildVisibilityRequested(visible);
final DisplayContent displayContent = getDisplayContent();
displayContent.mOpeningApps.remove(this);
@@ -9961,6 +9961,8 @@ final class ActivityRecord extends WindowToken {
aspectRatioOverrides.shouldEnableUserAspectRatioSettings());
proto.write(IS_USER_FULLSCREEN_OVERRIDE_ENABLED,
aspectRatioOverrides.isUserFullscreenOverrideEnabled());
+ proto.write(REQUEST_OPEN_IN_BROWSER_EDUCATION_TIMESTAMP,
+ mRequestOpenInBrowserEducationTimestamp);
}
@Override
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 906befc1edcc..6a5adca91e39 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -145,6 +145,7 @@ import android.util.SparseIntArray;
import android.view.Display;
import android.webkit.URLUtil;
import android.window.ActivityWindowInfo;
+import android.window.DesktopModeFlags;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
@@ -278,7 +279,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
/** Helper for {@link Task#fillTaskInfo}. */
final TaskInfoHelper mTaskInfoHelper = new TaskInfoHelper();
- final OpaqueActivityHelper mOpaqueActivityHelper = new OpaqueActivityHelper();
+ final OpaqueContainerHelper mOpaqueContainerHelper = new OpaqueContainerHelper();
private final ActivityTaskSupervisorHandler mHandler;
final Looper mLooper;
@@ -2913,41 +2914,90 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
}
}
- /** The helper to get the top opaque activity of a container. */
- static class OpaqueActivityHelper implements Predicate<ActivityRecord> {
+ /** The helper to calculate whether a container is opaque. */
+ static class OpaqueContainerHelper implements Predicate<ActivityRecord> {
private ActivityRecord mStarting;
- private boolean mIncludeInvisibleAndFinishing;
+ private boolean mIgnoringInvisibleActivity;
private boolean mIgnoringKeyguard;
- ActivityRecord getOpaqueActivity(@NonNull WindowContainer<?> container) {
- mIncludeInvisibleAndFinishing = true;
- mIgnoringKeyguard = true;
- return container.getActivity(this,
- true /* traverseTopToBottom */, null /* boundary */);
+ /** Whether the container is opaque. */
+ boolean isOpaque(@NonNull WindowContainer<?> container) {
+ return isOpaque(container, null /* starting */, true /* ignoringKeyguard */,
+ false /* ignoringInvisibleActivity */);
}
- ActivityRecord getVisibleOpaqueActivity(
+ /**
+ * Whether the container is opaque, but only including visible activities in its
+ * calculation.
+ */
+ boolean isOpaque(
@NonNull WindowContainer<?> container, @Nullable ActivityRecord starting,
- boolean ignoringKeyguard) {
+ boolean ignoringKeyguard, boolean ignoringInvisibleActivity) {
mStarting = starting;
- mIncludeInvisibleAndFinishing = false;
+ mIgnoringInvisibleActivity = ignoringInvisibleActivity;
mIgnoringKeyguard = ignoringKeyguard;
- final ActivityRecord opaque = container.getActivity(this,
- true /* traverseTopToBottom */, null /* boundary */);
+
+ final boolean isOpaque;
+ if (!Flags.enableMultipleDesktopsBackend()) {
+ isOpaque = container.getActivity(this,
+ true /* traverseTopToBottom */, null /* boundary */) != null;
+ } else {
+ isOpaque = isOpaqueInner(container);
+ }
mStarting = null;
- return opaque;
+ return isOpaque;
+ }
+
+ private boolean isOpaqueInner(@NonNull WindowContainer<?> container) {
+ // If it's a leaf task fragment, then opacity is calculated based on its activities.
+ if (container.asTaskFragment() != null
+ && ((TaskFragment) container).isLeafTaskFragment()) {
+ return container.getActivity(this,
+ true /* traverseTopToBottom */, null /* boundary */) != null;
+ }
+ // When not a leaf, it's considered opaque if any of its opaque children fill this
+ // container, unless the children are adjacent fragments, in which case as long as they
+ // are all opaque then |container| is also considered opaque, even if the adjacent
+ // task fragment aren't filling.
+ for (int i = 0; i < container.getChildCount(); i++) {
+ final WindowContainer<?> child = container.getChildAt(i);
+ if (child.fillsParent() && isOpaque(child)) {
+ return true;
+ }
+
+ if (child.asTaskFragment() != null
+ && child.asTaskFragment().hasAdjacentTaskFragment()) {
+ final boolean isAnyTranslucent;
+ if (Flags.allowMultipleAdjacentTaskFragments()) {
+ final TaskFragment.AdjacentSet set =
+ child.asTaskFragment().getAdjacentTaskFragments();
+ isAnyTranslucent = set.forAllTaskFragments(
+ tf -> !isOpaque(tf), null);
+ } else {
+ final TaskFragment adjacent = child.asTaskFragment()
+ .getAdjacentTaskFragment();
+ isAnyTranslucent = !isOpaque(child) || !isOpaque(adjacent);
+ }
+ if (!isAnyTranslucent) {
+ // This task fragment and all its adjacent task fragments are opaque,
+ // consider it opaque even if it doesn't fill its parent.
+ return true;
+ }
+ }
+ }
+ return false;
}
@Override
public boolean test(ActivityRecord r) {
- if (!mIncludeInvisibleAndFinishing && r != mStarting
+ if (mIgnoringInvisibleActivity && r != mStarting
&& ((mIgnoringKeyguard && !r.visibleIgnoringKeyguard)
|| (!mIgnoringKeyguard && !r.isVisible()))) {
// Ignore invisible activities that are not the currently starting activity
// (about to be visible).
return false;
}
- return r.occludesParent(mIncludeInvisibleAndFinishing /* includingFinishing */);
+ return r.occludesParent(!mIgnoringInvisibleActivity /* includingFinishing */);
}
}
@@ -2974,7 +3024,8 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
@Override
public void accept(ActivityRecord r) {
- if (Flags.enableDesktopWindowingAppToWeb() && mInfo.capturedLink == null) {
+ if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_APP_TO_WEB.isTrue()
+ && mInfo.capturedLink == null) {
setCapturedLink(r);
}
if (r.mLaunchCookie != null) {
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index 0a2f6852f6e6..d5fe056a2ba4 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -80,7 +80,6 @@ import android.os.Trace;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Pair;
-import android.view.Display;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationDefinition;
import android.view.WindowManager;
@@ -252,7 +251,6 @@ public class AppTransitionController {
// Check if there is any override
if (!overrideWithTaskFragmentRemoteAnimation(transit, activityTypes)) {
// Unfreeze the windows that were previously frozen for TaskFragment animation.
- unfreezeEmbeddedChangingWindows();
overrideWithRemoteAnimationIfSet(animLpActivity, transit, activityTypes);
}
@@ -545,16 +543,6 @@ public class AppTransitionController {
: null;
}
- private void unfreezeEmbeddedChangingWindows() {
- final ArraySet<WindowContainer> changingContainers = mDisplayContent.mChangingContainers;
- for (int i = changingContainers.size() - 1; i >= 0; i--) {
- final WindowContainer wc = changingContainers.valueAt(i);
- if (wc.isEmbedded()) {
- wc.mSurfaceFreezer.unfreeze(wc.getSyncTransaction());
- }
- }
- }
-
private boolean transitionMayContainNonAppWindows(@TransitionOldType int transit) {
// We don't want to have the client to animate any non-app windows.
// Having {@code transit} of those types doesn't mean it will contain non-app windows, but
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index e3906f9119c2..0eea30a29580 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -52,8 +52,7 @@ public final class DesktopModeHelper {
* Return {@code true} if the current device supports desktop mode.
*/
// TODO(b/337819319): use a companion object instead.
- @VisibleForTesting
- static boolean isDesktopModeSupported(@NonNull Context context) {
+ private static boolean isDesktopModeSupported(@NonNull Context context) {
return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported);
}
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index a0d2d260b39e..ecf2787a2080 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -5165,7 +5165,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
/**
* Creates a LayerCaptureArgs object to represent the entire DisplayContent
*/
- LayerCaptureArgs getLayerCaptureArgs(Set<Integer> windowTypesToExclude) {
+ LayerCaptureArgs getLayerCaptureArgs(@Nullable ToBooleanFunction<WindowState> predicate) {
if (!mWmService.mPolicy.isScreenOn()) {
if (DEBUG_SCREENSHOT) {
Slog.i(TAG_WM, "Attempted to take screenshot while display was off.");
@@ -5178,17 +5178,16 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp
LayerCaptureArgs.Builder builder = new LayerCaptureArgs.Builder(getSurfaceControl())
.setSourceCrop(mTmpRect);
- if (!windowTypesToExclude.isEmpty()) {
- ArrayList<SurfaceControl> surfaceControls = new ArrayList<>();
+ if (predicate != null) {
+ ArrayList<SurfaceControl> excludeLayers = new ArrayList<>();
forAllWindows(
window -> {
- if (windowTypesToExclude.contains(window.getWindowType())) {
- surfaceControls.add(window.mSurfaceControl);
+ if (!predicate.apply(window)) {
+ excludeLayers.add(window.mSurfaceControl);
}
- }, true
- );
- if (!surfaceControls.isEmpty()) {
- builder.setExcludeLayers(surfaceControls.toArray(new SurfaceControl[0]));
+ }, true);
+ if (!excludeLayers.isEmpty()) {
+ builder.setExcludeLayers(excludeLayers.toArray(new SurfaceControl[0]));
}
}
return builder.build();
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 59ca79c7ffc3..cf16204f93a1 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -182,12 +182,18 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider {
if (control != null && control.getLeash() != null) {
ImeTracker.Token statsToken = getAndClearStatsToken();
if (statsToken == null) {
- ProtoLog.d(WM_DEBUG_IME, "IME getControl without statsToken");
- } else {
- ImeTracker.forLogging().onProgress(statsToken,
- ImeTracker.PHASE_WM_GET_CONTROL_WITH_LEASH);
- control.setImeStatsToken(statsToken);
+ ProtoLog.w(WM_DEBUG_IME,
+ "IME getControl without statsToken (check previous request!). "
+ + "Start new request");
+ // TODO(b/353463205) remove this later after fixing the race of two requests
+ // that cancel each other (cf. b/383466954#comment19).
+ statsToken = ImeTracker.forLogging().onStart(ImeTracker.TYPE_SHOW,
+ ImeTracker.ORIGIN_SERVER, SoftInputShowHideReason.CONTROLS_CHANGED,
+ false /* fromUser */);
}
+ ImeTracker.forLogging().onProgress(statsToken,
+ ImeTracker.PHASE_WM_GET_CONTROL_WITH_LEASH);
+ control.setImeStatsToken(statsToken);
}
}
return control;
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index cf464c707ff4..1fe6ad68a313 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1948,7 +1948,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
final IntArray rootTaskIdsToRestore = mUserVisibleRootTasks.get(userId);
boolean homeInFront = false;
if (Flags.enableTopVisibleRootTaskPerUserTracking()) {
- if (rootTaskIdsToRestore == null) {
+ if (rootTaskIdsToRestore == null || rootTaskIdsToRestore.size() == 0) {
// If there are no root tasks saved, try restore id 0 which should create and launch
// the home task.
handleRootTaskLaunchOnUserSwitch(/* restoreRootTaskId */INVALID_TASK_ID);
@@ -1958,11 +1958,8 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
handleRootTaskLaunchOnUserSwitch(rootTaskIdsToRestore.get(i));
}
// Check if the top task is type home
- if (rootTaskIdsToRestore.size() > 0) {
- final int topRootTaskId = rootTaskIdsToRestore.get(
- rootTaskIdsToRestore.size() - 1);
- homeInFront = isHomeTask(topRootTaskId);
- }
+ final int topRootTaskId = rootTaskIdsToRestore.get(rootTaskIdsToRestore.size() - 1);
+ homeInFront = isHomeTask(topRootTaskId);
}
} else {
handleRootTaskLaunchOnUserSwitch(restoreRootTaskId);
diff --git a/services/core/java/com/android/server/wm/SurfaceAnimator.java b/services/core/java/com/android/server/wm/SurfaceAnimator.java
index d7b6d96c781d..3dfff39e9b68 100644
--- a/services/core/java/com/android/server/wm/SurfaceAnimator.java
+++ b/services/core/java/com/android/server/wm/SurfaceAnimator.java
@@ -60,8 +60,6 @@ public class SurfaceAnimator {
@VisibleForTesting
SurfaceControl mLeash;
@VisibleForTesting
- SurfaceFreezer.Snapshot mSnapshot;
- @VisibleForTesting
final Animatable mAnimatable;
@VisibleForTesting
final OnAnimationFinishedCallback mInnerAnimationFinishedCallback;
@@ -165,7 +163,7 @@ public class SurfaceAnimator {
@AnimationType int type,
@Nullable OnAnimationFinishedCallback animationFinishedCallback,
@Nullable Runnable animationCancelledCallback,
- @Nullable AnimationAdapter snapshotAnim, @Nullable SurfaceFreezer freezer) {
+ @Nullable AnimationAdapter snapshotAnim) {
cancelAnimation(t, true /* restarting */, true /* forwardCancel */);
mAnimation = anim;
mAnimationType = type;
@@ -177,7 +175,6 @@ public class SurfaceAnimator {
cancelAnimation();
return;
}
- mLeash = freezer != null ? freezer.takeLeashForAnimation() : null;
if (mLeash == null) {
mLeash = createAnimationLeash(mAnimatable, surface, t, type,
mAnimatable.getSurfaceWidth(), mAnimatable.getSurfaceHeight(), 0 /* x */,
@@ -192,21 +189,13 @@ public class SurfaceAnimator {
mAnimation.dump(pw, "");
ProtoLog.d(WM_DEBUG_ANIM, "Animation start for %s, anim=%s", mAnimatable, sw);
}
- if (snapshotAnim != null) {
- mSnapshot = freezer.takeSnapshotForAnimation();
- if (mSnapshot == null) {
- Slog.e(TAG, "No snapshot target to start animation on for " + mAnimatable);
- return;
- }
- mSnapshot.startAnimation(t, snapshotAnim, type);
- }
setAnimatorPendingState(t);
}
void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,
@AnimationType int type) {
startAnimation(t, anim, hidden, type, null /* animationFinishedCallback */,
- null /* animationCancelledCallback */, null /* snapshotAnim */, null /* freezer */);
+ null /* animationCancelledCallback */, null /* snapshotAnim */);
}
/** Indicates that there are surface operations in the pending transaction. */
@@ -317,7 +306,6 @@ public class SurfaceAnimator {
final OnAnimationFinishedCallback animationFinishedCallback =
mSurfaceAnimationFinishedCallback;
final Runnable animationCancelledCallback = mAnimationCancelledCallback;
- final SurfaceFreezer.Snapshot snapshot = mSnapshot;
reset(t, false);
if (animation != null) {
if (forwardCancel) {
@@ -337,9 +325,6 @@ public class SurfaceAnimator {
}
if (forwardCancel) {
- if (snapshot != null) {
- snapshot.cancelAnimation(t, false /* restarting */);
- }
if (leash != null) {
t.remove(leash);
mService.scheduleAnimationLocked();
@@ -352,12 +337,6 @@ public class SurfaceAnimator {
mAnimation = null;
mSurfaceAnimationFinishedCallback = null;
mAnimationType = ANIMATION_TYPE_NONE;
- final SurfaceFreezer.Snapshot snapshot = mSnapshot;
- mSnapshot = null;
- if (snapshot != null) {
- // Reset the mSnapshot reference before calling the callback to prevent circular reset.
- snapshot.cancelAnimation(t, !destroyLeash);
- }
if (mLeash == null) {
return;
}
@@ -597,8 +576,7 @@ public class SurfaceAnimator {
void commitPendingTransaction();
/**
- * Called when the animation leash is created. Note that this is also called by
- * {@link SurfaceFreezer}, so this doesn't mean we're about to start animating.
+ * Called when the animation leash is created.
*
* @param t The transaction to use to apply any necessary changes.
* @param leash The leash that was created.
diff --git a/services/core/java/com/android/server/wm/SurfaceFreezer.java b/services/core/java/com/android/server/wm/SurfaceFreezer.java
deleted file mode 100644
index e126ed65d508..000000000000
--- a/services/core/java/com/android/server/wm/SurfaceFreezer.java
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import static com.android.internal.protolog.WmProtoLogGroups.WM_SHOW_TRANSACTIONS;
-import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_SCREEN_ROTATION;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.graphics.GraphicBuffer;
-import android.graphics.PixelFormat;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.hardware.HardwareBuffer;
-import android.util.Slog;
-import android.view.SurfaceControl;
-import android.window.ScreenCapture;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.protolog.ProtoLog;
-
-/**
- * This class handles "freezing" of an Animatable. The Animatable in question should implement
- * Freezable.
- *
- * The point of this is to enable WindowContainers to each be capable of freezing themselves.
- * Freezing means taking a snapshot and placing it above everything in the sub-hierarchy.
- * The "placing above" requires that a parent surface be inserted above the target surface so that
- * the target surface and the snapshot are siblings.
- *
- * The overall flow for a transition using this would be:
- * 1. Set transition and record animatable in mChangingApps
- * 2. Call {@link #freeze} to set-up the leashes and cover with a snapshot.
- * 3. When transition participants are ready, start SurfaceAnimator with this as a parameter
- * 4. SurfaceAnimator will then {@link #takeLeashForAnimation} instead of creating another leash.
- * 5. The animation system should eventually clean this up via {@link #unfreeze}.
- */
-class SurfaceFreezer {
-
- private static final String TAG = "SurfaceFreezer";
-
- private final @NonNull Freezable mAnimatable;
- private final @NonNull WindowManagerService mWmService;
- @VisibleForTesting
- SurfaceControl mLeash;
- Snapshot mSnapshot = null;
- final Rect mFreezeBounds = new Rect();
-
- /**
- * @param animatable The object to animate.
- */
- SurfaceFreezer(@NonNull Freezable animatable, @NonNull WindowManagerService service) {
- mAnimatable = animatable;
- mWmService = service;
- }
-
- /**
- * Freeze the target surface. This is done by creating a leash (inserting a parent surface
- * above the target surface) and then taking a snapshot and placing it over the target surface.
- *
- * @param startBounds The original bounds (on screen) of the surface we are snapshotting.
- * @param relativePosition The related position of the snapshot surface to its parent.
- * @param freezeTarget The surface to take snapshot from. If {@code null}, we will take a
- * snapshot from the {@link #mAnimatable} surface.
- */
- void freeze(SurfaceControl.Transaction t, Rect startBounds, Point relativePosition,
- @Nullable SurfaceControl freezeTarget) {
- reset(t);
- mFreezeBounds.set(startBounds);
-
- mLeash = SurfaceAnimator.createAnimationLeash(mAnimatable, mAnimatable.getSurfaceControl(),
- t, ANIMATION_TYPE_SCREEN_ROTATION, startBounds.width(), startBounds.height(),
- relativePosition.x, relativePosition.y, false /* hidden */,
- mWmService.mTransactionFactory);
- mAnimatable.onAnimationLeashCreated(t, mLeash);
-
- freezeTarget = freezeTarget != null ? freezeTarget : mAnimatable.getFreezeSnapshotTarget();
- if (freezeTarget != null) {
- ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = createSnapshotBufferInner(
- freezeTarget, startBounds);
- final HardwareBuffer buffer = screenshotBuffer == null ? null
- : screenshotBuffer.getHardwareBuffer();
- if (buffer == null || buffer.getWidth() <= 1 || buffer.getHeight() <= 1) {
- // This can happen when display is not ready.
- Slog.w(TAG, "Failed to capture screenshot for " + mAnimatable);
- unfreeze(t);
- return;
- }
- mSnapshot = new Snapshot(t, screenshotBuffer, mLeash);
- }
- }
-
- /**
- * Used by {@link SurfaceAnimator}. This "transfers" the leash to be used for animation.
- * By transferring the leash, this will no longer try to clean-up the leash when finished.
- */
- SurfaceControl takeLeashForAnimation() {
- SurfaceControl out = mLeash;
- mLeash = null;
- return out;
- }
-
- /**
- * Used by {@link SurfaceAnimator}. This "transfers" the snapshot leash to be used for
- * animation. By transferring the leash, this will no longer try to clean-up the leash when
- * finished.
- */
- @Nullable
- Snapshot takeSnapshotForAnimation() {
- final Snapshot out = mSnapshot;
- mSnapshot = null;
- return out;
- }
-
- /**
- * Clean-up the snapshot and remove leash. If the leash was taken, this just cleans-up the
- * snapshot.
- */
- void unfreeze(SurfaceControl.Transaction t) {
- unfreezeInner(t);
- mAnimatable.onUnfrozen();
- }
-
- private void unfreezeInner(SurfaceControl.Transaction t) {
- if (mSnapshot != null) {
- mSnapshot.cancelAnimation(t, false /* restarting */);
- mSnapshot = null;
- }
- if (mLeash == null) {
- return;
- }
- SurfaceControl leash = mLeash;
- mLeash = null;
- final boolean scheduleAnim = SurfaceAnimator.removeLeash(t, mAnimatable, leash,
- true /* destroy */);
- if (scheduleAnim) {
- mWmService.scheduleAnimationLocked();
- }
- }
-
- /** Resets the snapshot before taking another one if the animation hasn't been started yet. */
- private void reset(SurfaceControl.Transaction t) {
- // Those would have been taken by the SurfaceAnimator if the animation has been started, so
- // we can remove the leash directly.
- // No need to reset the mAnimatable leash, as this is called before a new animation leash is
- // created, so another #onAnimationLeashCreated will be called.
- if (mSnapshot != null) {
- mSnapshot.destroy(t);
- mSnapshot = null;
- }
- if (mLeash != null) {
- t.remove(mLeash);
- mLeash = null;
- }
- }
-
- void setLayer(SurfaceControl.Transaction t, int layer) {
- if (mLeash != null) {
- t.setLayer(mLeash, layer);
- }
- }
-
- void setRelativeLayer(SurfaceControl.Transaction t, SurfaceControl relativeTo, int layer) {
- if (mLeash != null) {
- t.setRelativeLayer(mLeash, relativeTo, layer);
- }
- }
-
- boolean hasLeash() {
- return mLeash != null;
- }
-
- private static ScreenCapture.ScreenshotHardwareBuffer createSnapshotBuffer(
- @NonNull SurfaceControl target, @Nullable Rect bounds) {
- Rect cropBounds = null;
- if (bounds != null) {
- cropBounds = new Rect(bounds);
- cropBounds.offsetTo(0, 0);
- }
- ScreenCapture.LayerCaptureArgs captureArgs =
- new ScreenCapture.LayerCaptureArgs.Builder(target)
- .setSourceCrop(cropBounds)
- .setCaptureSecureLayers(true)
- .setAllowProtected(true)
- .build();
- return ScreenCapture.captureLayers(captureArgs);
- }
-
- @VisibleForTesting
- ScreenCapture.ScreenshotHardwareBuffer createSnapshotBufferInner(
- SurfaceControl target, Rect bounds) {
- return createSnapshotBuffer(target, bounds);
- }
-
- @VisibleForTesting
- GraphicBuffer createFromHardwareBufferInner(
- ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer) {
- return GraphicBuffer.createFromHardwareBuffer(screenshotBuffer.getHardwareBuffer());
- }
-
- class Snapshot {
- private SurfaceControl mSurfaceControl;
- private AnimationAdapter mAnimation;
-
- /**
- * @param t Transaction to create the thumbnail in.
- * @param screenshotBuffer A thumbnail or placeholder for thumbnail to initialize with.
- */
- Snapshot(SurfaceControl.Transaction t,
- ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer, SurfaceControl parent) {
- GraphicBuffer graphicBuffer = createFromHardwareBufferInner(screenshotBuffer);
-
- mSurfaceControl = mAnimatable.makeAnimationLeash()
- .setName("snapshot anim: " + mAnimatable.toString())
- .setFormat(PixelFormat.TRANSLUCENT)
- .setParent(parent)
- .setSecure(screenshotBuffer.containsSecureLayers())
- .setCallsite("SurfaceFreezer.Snapshot")
- .setBLASTLayer()
- .build();
-
- ProtoLog.i(WM_SHOW_TRANSACTIONS, " THUMBNAIL %s: CREATE", mSurfaceControl);
-
- t.setBuffer(mSurfaceControl, graphicBuffer);
- t.setColorSpace(mSurfaceControl, screenshotBuffer.getColorSpace());
- t.show(mSurfaceControl);
-
- // We parent the thumbnail to the container, and just place it on top of anything else
- // in the container.
- t.setLayer(mSurfaceControl, Integer.MAX_VALUE);
- }
-
- void destroy(SurfaceControl.Transaction t) {
- if (mSurfaceControl == null) {
- return;
- }
- t.remove(mSurfaceControl);
- mSurfaceControl = null;
- }
-
- /**
- * Starts an animation.
- *
- * @param anim The object that bridges the controller, {@link SurfaceAnimator}, with the
- * component responsible for running the animation. It runs the animation with
- * {@link AnimationAdapter#startAnimation} once the hierarchy with
- * the Leash has been set up.
- */
- void startAnimation(SurfaceControl.Transaction t, AnimationAdapter anim, int type) {
- cancelAnimation(t, true /* restarting */);
- mAnimation = anim;
- if (mSurfaceControl == null) {
- cancelAnimation(t, false /* restarting */);
- return;
- }
- mAnimation.startAnimation(mSurfaceControl, t, type, (typ, ani) -> { });
- }
-
- /**
- * Cancels the animation, and resets the leash.
- *
- * @param t The transaction to use for all cancelling surface operations.
- * @param restarting Whether we are restarting the animation.
- */
- void cancelAnimation(SurfaceControl.Transaction t, boolean restarting) {
- final SurfaceControl leash = mSurfaceControl;
- final AnimationAdapter animation = mAnimation;
- mAnimation = null;
- if (animation != null) {
- animation.onAnimationCancelled(leash);
- }
- if (!restarting) {
- destroy(t);
- }
- }
- }
-
- /** freezable */
- public interface Freezable extends SurfaceAnimator.Animatable {
- /**
- * @return The surface to take a snapshot of. If this returns {@code null}, no snapshot
- * will be generated (but the rest of the freezing logic will still happen).
- */
- @Nullable SurfaceControl getFreezeSnapshotTarget();
-
- /** Called when the {@link #unfreeze(SurfaceControl.Transaction)} is called. */
- void onUnfrozen();
- }
-}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index c6136f316c3e..f2f926ac952c 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -52,11 +52,9 @@ import static android.view.SurfaceControl.METADATA_TASK_ID;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_RESIZEABLE_ACTIVITY_OVERRIDES;
-import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED;
import static android.view.WindowManager.TRANSIT_NONE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
@@ -75,7 +73,6 @@ import static com.android.server.wm.ActivityRecord.TRANSFER_SPLASH_SCREEN_COPYIN
import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_SWITCH;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_TRANSITION;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_USER_LEAVING;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_CLEANUP;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RECENTS;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_SWITCH;
@@ -161,13 +158,11 @@ import android.os.Trace;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.voice.IVoiceInteractionSession;
-import android.util.ArraySet;
import android.util.DisplayMetrics;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import android.view.DisplayInfo;
import android.view.InsetsState;
-import android.view.RemoteAnimationAdapter;
import android.view.SurfaceControl;
import android.view.WindowInsets;
import android.view.WindowManager;
@@ -2345,31 +2340,8 @@ class Task extends TaskFragment {
}
@VisibleForTesting
- Point getLastSurfaceSize() {
- return mLastSurfaceSize;
- }
-
- @VisibleForTesting
boolean isInChangeTransition() {
- return mSurfaceFreezer.hasLeash() || AppTransition.isChangeTransitOld(mTransit);
- }
-
- @Override
- public SurfaceControl getFreezeSnapshotTarget() {
- if (!mDisplayContent.mAppTransition.containsTransitRequest(TRANSIT_CHANGE)) {
- return null;
- }
- // Skip creating snapshot if this transition is controlled by a remote animator which
- // doesn't need it.
- final ArraySet<Integer> activityTypes = new ArraySet<>();
- activityTypes.add(getActivityType());
- final RemoteAnimationAdapter adapter =
- mDisplayContent.mAppTransitionController.getRemoteAnimationOverride(
- this, TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE, activityTypes);
- if (adapter != null && !adapter.getChangeNeedsSnapshot()) {
- return null;
- }
- return getSurfaceControl();
+ return AppTransition.isChangeTransitOld(mTransit);
}
@Override
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index fc7437b95e03..ba48fdc963de 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1195,10 +1195,8 @@ class TaskFragment extends WindowContainer<WindowContainer> {
if (!isAttached() || isForceHidden() || isForceTranslucent()) {
return true;
}
- // A TaskFragment isn't translucent if it has at least one visible activity that occludes
- // this TaskFragment.
- return mTaskSupervisor.mOpaqueActivityHelper.getVisibleOpaqueActivity(this,
- starting, true /* ignoringKeyguard */) == null;
+ return !mTaskSupervisor.mOpaqueContainerHelper.isOpaque(
+ this, starting, true /* ignoringKeyguard */, true /* ignoringInvisibleActivity */);
}
/**
@@ -1211,7 +1209,7 @@ class TaskFragment extends WindowContainer<WindowContainer> {
return true;
}
// Including finishing Activity if the TaskFragment is becoming invisible in the transition.
- return mTaskSupervisor.mOpaqueActivityHelper.getOpaqueActivity(this) == null;
+ return !mTaskSupervisor.mOpaqueContainerHelper.isOpaque(this);
}
/**
@@ -1222,8 +1220,8 @@ class TaskFragment extends WindowContainer<WindowContainer> {
if (!isAttached() || isForceHidden() || isForceTranslucent()) {
return true;
}
- return mTaskSupervisor.mOpaqueActivityHelper.getVisibleOpaqueActivity(this, null,
- false /* ignoringKeyguard */) == null;
+ return !mTaskSupervisor.mOpaqueContainerHelper.isOpaque(this, /* starting */ null,
+ false /* ignoringKeyguard */, true /* ignoringInvisibleActivity */);
}
ActivityRecord getTopNonFinishingActivity() {
@@ -2758,7 +2756,7 @@ class TaskFragment extends WindowContainer<WindowContainer> {
// We only want to update for organized TaskFragment. Task will handle itself.
return;
}
- if (mSurfaceControl == null || mSurfaceAnimator.hasLeash() || mSurfaceFreezer.hasLeash()) {
+ if (mSurfaceControl == null || mSurfaceAnimator.hasLeash()) {
return;
}
@@ -2900,20 +2898,6 @@ class TaskFragment extends WindowContainer<WindowContainer> {
return task != null && !task.isDragResizing() && super.canStartChangeTransition();
}
- /**
- * Returns {@code true} if the starting bounds of the closing organized TaskFragment is
- * recorded. Otherwise, return {@code false}.
- */
- boolean setClosingChangingStartBoundsIfNeeded() {
- if (isOrganizedTaskFragment() && mDisplayContent != null
- && mDisplayContent.mChangingContainers.remove(this)) {
- mDisplayContent.mClosingChangingContainers.put(
- this, new Rect(mSurfaceFreezer.mFreezeBounds));
- return true;
- }
- return false;
- }
-
@Override
boolean isSyncFinished(BLASTSyncEngine.SyncGroup group) {
return super.isSyncFinished(group) && isReadyToTransit();
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 27683b2fcff2..5217a759c6ae 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -505,7 +505,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
final WindowContainer<?> sibling = rootParent.getChildAt(j);
if (sibling == transientRoot) break;
if (!sibling.getWindowConfiguration().isAlwaysOnTop() && mController.mAtm
- .mTaskSupervisor.mOpaqueActivityHelper.getOpaqueActivity(sibling) != null) {
+ .mTaskSupervisor.mOpaqueContainerHelper.isOpaque(sibling)) {
occludedCount++;
break;
}
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 883d8f95b612..225951dbd345 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -138,7 +138,7 @@ import java.util.function.Predicate;
* changes are made to this class.
*/
class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<E>
- implements Comparable<WindowContainer>, Animatable, SurfaceFreezer.Freezable,
+ implements Comparable<WindowContainer>, Animatable,
InsetsControlTarget {
private static final String TAG = TAG_WITH_CLASS_NAME ? "WindowContainer" : TAG_WM;
@@ -226,7 +226,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
@Nullable
private SurfaceControl mAnimationLeash;
- final SurfaceFreezer mSurfaceFreezer;
protected final WindowManagerService mWmService;
final TransitionController mTransitionController;
@@ -361,7 +360,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
mTransitionController = mWmService.mAtmService.getTransitionController();
mSyncTransaction = wms.mTransactionFactory.get();
mSurfaceAnimator = new SurfaceAnimator(this, this::onAnimationFinished, wms);
- mSurfaceFreezer = new SurfaceFreezer(this, wms);
}
/**
@@ -908,7 +906,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
final DisplayContent dc = getDisplayContent();
if (dc != null) {
dc.mClosingChangingContainers.remove(this);
- mSurfaceFreezer.unfreeze(getSyncTransaction());
}
while (!mChildren.isEmpty()) {
final E child = mChildren.getLast();
@@ -1124,9 +1121,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
// Cancel any change transition queued-up for this container on the old display when
// this container is moved from the old display.
mDisplayContent.mClosingChangingContainers.remove(this);
- if (mDisplayContent.mChangingContainers.remove(this)) {
- mSurfaceFreezer.unfreeze(getSyncTransaction());
- }
+ mDisplayContent.mChangingContainers.remove(this);
}
mDisplayContent = dc;
if (dc != null && dc != this && mPendingTransaction != null) {
@@ -1434,33 +1429,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
return setVisibleRequested(newVisReq);
}
- /**
- * Called when the visibility of a child is asked to change. This is before visibility actually
- * changes (eg. a transition animation might play out first).
- */
- void onChildVisibilityRequested(boolean visible) {
- // If we are losing visibility, then a snapshot isn't necessary and we are no-longer
- // part of a change transition.
- if (!visible) {
- boolean skipUnfreeze = false;
- if (asTaskFragment() != null) {
- // If the organized TaskFragment is closing while resizing, we want to keep track of
- // its starting bounds to make sure the animation starts at the correct position.
- // This should be called before unfreeze() because we record the starting bounds
- // in SurfaceFreezer.
- skipUnfreeze = asTaskFragment().setClosingChangingStartBoundsIfNeeded();
- }
-
- if (!skipUnfreeze) {
- mSurfaceFreezer.unfreeze(getSyncTransaction());
- }
- }
- WindowContainer parent = getParent();
- if (parent != null) {
- parent.onChildVisibilityRequested(visible);
- }
- }
-
/** Whether this window is closing while resizing. */
boolean isClosingWhenResizing() {
return mDisplayContent != null
@@ -1545,9 +1513,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
void onAppTransitionDone() {
- if (mSurfaceFreezer.hasLeash()) {
- mSurfaceFreezer.unfreeze(getSyncTransaction());
- }
for (int i = mChildren.size() - 1; i >= 0; --i) {
final WindowContainer wc = mChildren.get(i);
wc.onAppTransitionDone();
@@ -2773,15 +2738,9 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
protected void setLayer(Transaction t, int layer) {
- if (mSurfaceFreezer.hasLeash()) {
- // When the freezer has created animation leash parent for the window, set the layer
- // there instead.
- mSurfaceFreezer.setLayer(t, layer);
- } else {
- // Route through surface animator to accommodate that our surface control might be
- // attached to the leash, and leash is attached to parent container.
- mSurfaceAnimator.setLayer(t, layer);
- }
+ // Route through surface animator to accommodate that our surface control might be
+ // attached to the leash, and leash is attached to parent container.
+ mSurfaceAnimator.setLayer(t, layer);
}
int getLastLayer() {
@@ -2793,20 +2752,14 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
protected void setRelativeLayer(Transaction t, SurfaceControl relativeTo, int layer) {
- if (mSurfaceFreezer.hasLeash()) {
- // When the freezer has created animation leash parent for the window, set the layer
- // there instead.
- mSurfaceFreezer.setRelativeLayer(t, relativeTo, layer);
- } else {
- // Route through surface animator to accommodate that our surface control might be
- // attached to the leash, and leash is attached to parent container.
- mSurfaceAnimator.setRelativeLayer(t, relativeTo, layer);
- }
+ // Route through surface animator to accommodate that our surface control might be
+ // attached to the leash, and leash is attached to parent container.
+ mSurfaceAnimator.setRelativeLayer(t, relativeTo, layer);
}
protected void reparentSurfaceControl(Transaction t, SurfaceControl newParent) {
// Don't reparent active leashes since the animator won't know about the change.
- if (mSurfaceFreezer.hasLeash() || mSurfaceAnimator.hasLeash()) return;
+ if (mSurfaceAnimator.hasLeash()) return;
t.reparent(getSurfaceControl(), newParent);
}
@@ -3044,7 +2997,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
// TODO: This should use isVisible() but because isVisible has a really weird meaning at
// the moment this doesn't work for all animatable window containers.
mSurfaceAnimator.startAnimation(t, anim, hidden, type, animationFinishedCallback,
- animationCancelledCallback, snapshotAnim, mSurfaceFreezer);
+ animationCancelledCallback, snapshotAnim);
}
void startAnimation(Transaction t, AnimationAdapter anim, boolean hidden,
@@ -3066,7 +3019,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
void cancelAnimation() {
doAnimationFinished(mSurfaceAnimator.getAnimationType(), mSurfaceAnimator.getAnimation());
mSurfaceAnimator.cancelAnimation();
- mSurfaceFreezer.unfreeze(getSyncTransaction());
}
/** Whether we can start change transition with this window and current display status. */
@@ -3097,7 +3049,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
/**
- * Initializes a change transition. See {@link SurfaceFreezer} for more information.
+ * Initializes a change transition.
*
* For now, this will only be called for the following cases:
* 1. {@link Task} is changing windowing mode between fullscreen and freeform.
@@ -3109,8 +3061,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
* use case.
*
* @param startBounds The original bounds (on screen) of the surface we are snapshotting.
- * @param freezeTarget The surface to take snapshot from. If {@code null}, we will take a
- * snapshot from {@link #getFreezeSnapshotTarget()}.
*/
void initializeChangeTransition(Rect startBounds, @Nullable SurfaceControl freezeTarget) {
if (mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
@@ -3122,7 +3072,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
// Calculate the relative position in parent container.
final Rect parentBounds = getParent().getBounds();
mTmpPoint.set(startBounds.left - parentBounds.left, startBounds.top - parentBounds.top);
- mSurfaceFreezer.freeze(getSyncTransaction(), startBounds, mTmpPoint, freezeTarget);
}
void initializeChangeTransition(Rect startBounds) {
@@ -3134,23 +3083,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
@Override
- public SurfaceControl getFreezeSnapshotTarget() {
- // Only allow freezing if this window is in a TRANSIT_CHANGE
- if (!mDisplayContent.mAppTransition.containsTransitRequest(TRANSIT_CHANGE)
- || !mDisplayContent.mChangingContainers.contains(this)) {
- return null;
- }
- return getSurfaceControl();
- }
-
- @Override
- public void onUnfrozen() {
- if (mDisplayContent != null) {
- mDisplayContent.mChangingContainers.remove(this);
- }
- }
-
- @Override
public Builder makeAnimationLeash() {
return makeSurface().setContainerLayer();
}
@@ -3279,7 +3211,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
this, mTmpPoint, localBounds, screenBounds, closingStartBounds,
showBackdrop, false /* shouldCreateSnapshot */);
} else {
- final Rect startBounds = isChanging ? mSurfaceFreezer.mFreezeBounds : null;
+ final Rect startBounds = null;
adapters = controller.createRemoteAnimationRecord(
this, mTmpPoint, localBounds, screenBounds, startBounds, showBackdrop);
}
@@ -3298,16 +3230,12 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
mTmpRect.offsetTo(mTmpPoint.x, mTmpPoint.y);
final AnimationAdapter adapter = new LocalAnimationAdapter(
- new WindowChangeAnimationSpec(mSurfaceFreezer.mFreezeBounds, mTmpRect,
+ new WindowChangeAnimationSpec(null /* startBounds */, mTmpRect,
displayInfo, durationScale, true /* isAppAnimation */,
false /* isThumbnail */),
getSurfaceAnimationRunner());
- final AnimationAdapter thumbnailAdapter = mSurfaceFreezer.mSnapshot != null
- ? new LocalAnimationAdapter(new WindowChangeAnimationSpec(
- mSurfaceFreezer.mFreezeBounds, mTmpRect, displayInfo, durationScale,
- true /* isAppAnimation */, true /* isThumbnail */), getSurfaceAnimationRunner())
- : null;
+ final AnimationAdapter thumbnailAdapter = null;
resultAdapters = new Pair<>(adapter, thumbnailAdapter);
mTransit = transit;
mTransitFlags = getDisplayContent().mAppTransition.getTransitFlags();
@@ -3731,7 +3659,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
void updateSurfacePosition(Transaction t) {
- if (mSurfaceControl == null || mSurfaceAnimator.hasLeash() || mSurfaceFreezer.hasLeash()) {
+ if (mSurfaceControl == null || mSurfaceAnimator.hasLeash()) {
return;
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index c77b1d9a7bcf..6e224f07fcdc 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -1137,6 +1137,15 @@ public abstract class WindowManagerInternal {
* Returns an instance of {@link ScreenshotHardwareBuffer} containing the current
* screenshot.
*/
- public abstract ScreenshotHardwareBuffer takeAssistScreenshot(
- Set<Integer> windowTypesToExclude);
+ public abstract ScreenshotHardwareBuffer takeAssistScreenshot();
+
+ /**
+ * Returns an instance of {@link ScreenshotHardwareBuffer} containing the current
+ * screenshot, excluding layers that are not appropriate to pass to contextual search
+ * services - such as the cursor or any current contextual search window.
+ *
+ * @param uid the UID of the contextual search application. System alert windows belonging
+ * to this UID will be excluded from the screenshot.
+ */
+ public abstract ScreenshotHardwareBuffer takeContextualSearchScreenshot(int uid);
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 3a1d652f82d4..7f1924005b2f 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -325,6 +325,7 @@ import android.window.WindowContainerToken;
import android.window.WindowContextInfo;
import com.android.internal.R;
+import com.android.internal.util.ToBooleanFunction;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
@@ -4159,7 +4160,8 @@ public class WindowManagerService extends IWindowManager.Stub
}
@Nullable
- private ScreenshotHardwareBuffer takeAssistScreenshot(Set<Integer> windowTypesToExclude) {
+ private ScreenshotHardwareBuffer takeAssistScreenshot(
+ @Nullable ToBooleanFunction<WindowState> predicate) {
if (!checkCallingPermission(READ_FRAME_BUFFER, "requestAssistScreenshot()")) {
throw new SecurityException("Requires READ_FRAME_BUFFER permission");
}
@@ -4174,7 +4176,7 @@ public class WindowManagerService extends IWindowManager.Stub
}
captureArgs = null;
} else {
- captureArgs = displayContent.getLayerCaptureArgs(windowTypesToExclude);
+ captureArgs = displayContent.getLayerCaptureArgs(predicate);
}
}
@@ -4204,8 +4206,7 @@ public class WindowManagerService extends IWindowManager.Stub
*/
@Override
public boolean requestAssistScreenshot(final IAssistDataReceiver receiver) {
- final ScreenshotHardwareBuffer shb =
- takeAssistScreenshot(/* windowTypesToExclude= */ Set.of());
+ final ScreenshotHardwareBuffer shb = takeAssistScreenshot(/* predicate= */ null);
final Bitmap bm = shb != null ? shb.asBitmap() : null;
FgThread.getHandler().post(() -> {
try {
@@ -8618,9 +8619,27 @@ public class WindowManagerService extends IWindowManager.Stub
}
@Override
- public ScreenshotHardwareBuffer takeAssistScreenshot(Set<Integer> windowTypesToExclude) {
+ public ScreenshotHardwareBuffer takeAssistScreenshot() {
// WMS.takeAssistScreenshot takes care of the locking.
- return WindowManagerService.this.takeAssistScreenshot(windowTypesToExclude);
+ return WindowManagerService.this.takeAssistScreenshot(/* predicate */ null);
+ }
+
+ @Override
+ public ScreenshotHardwareBuffer takeContextualSearchScreenshot(int uid) {
+ // WMS.takeAssistScreenshot takes care of the locking.
+ return WindowManagerService.this.takeAssistScreenshot(win -> {
+ switch (win.getWindowType()) {
+ case LayoutParams.TYPE_STATUS_BAR:
+ case LayoutParams.TYPE_NAVIGATION_BAR:
+ case LayoutParams.TYPE_NAVIGATION_BAR_PANEL:
+ case LayoutParams.TYPE_POINTER:
+ return false;
+ case LayoutParams.TYPE_APPLICATION_OVERLAY:
+ return uid != win.getOwningUid();
+ default:
+ return true;
+ }
+ });
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 582cd4ed8003..e11c31c88c87 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -2723,16 +2723,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
}
- /**
- * Apply default restrictions that haven't been applied to a given admin yet.
- */
+ /** Apply default restrictions that haven't been applied to a given admin yet. */
private void maybeSetDefaultRestrictionsForAdminLocked(int userId, ActiveAdmin admin) {
- Set<String> defaultRestrictions =
- UserRestrictionsUtils.getDefaultEnabledForManagedProfiles();
- if (defaultRestrictions.equals(admin.defaultEnabledRestrictionsAlreadySet)) {
+ Set<String> newDefaultRestrictions = new HashSet(
+ UserRestrictionsUtils.getDefaultEnabledForManagedProfiles());
+ newDefaultRestrictions.removeAll(admin.defaultEnabledRestrictionsAlreadySet);
+ if (newDefaultRestrictions.isEmpty()) {
return; // The same set of default restrictions has been already applied.
}
- for (String restriction : defaultRestrictions) {
+
+ for (String restriction : newDefaultRestrictions) {
mDevicePolicyEngine.setLocalPolicy(
PolicyDefinition.getPolicyDefinitionForUserRestriction(restriction),
EnforcingAdmin.createEnterpriseEnforcingAdmin(
@@ -2740,10 +2740,9 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
admin.getUserHandle().getIdentifier()),
new BooleanPolicyValue(true),
userId);
+ admin.defaultEnabledRestrictionsAlreadySet.add(restriction);
+ Slogf.i(LOG_TAG, "Enabled the following restriction by default: " + restriction);
}
- admin.defaultEnabledRestrictionsAlreadySet.addAll(defaultRestrictions);
- Slogf.i(LOG_TAG, "Enabled the following restrictions by default: "
- + defaultRestrictions);
}
private void maybeStartSecurityLogMonitorOnActivityManagerReady() {
@@ -10282,7 +10281,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
return false;
}
- if (isAdb(caller)) {
+ boolean isAdb = isAdb(caller);
+ if (isAdb) {
// Log profile owner provisioning was started using adb.
MetricsLogger.action(mContext, PROVISIONING_ENTRY_POINT_ADB, LOG_TAG_PROFILE_OWNER);
DevicePolicyEventLogger
@@ -10305,6 +10305,18 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
ensureUnknownSourcesRestrictionForProfileOwnerLocked(userHandle, admin,
true /* newOwner */);
}
+ if(isAdb) {
+ // DISALLOW_DEBUGGING_FEATURES is being added to newly-created
+ // work profile by default due to b/382064697 . This would have
+ // impacted certain CTS test flows when they interact with the
+ // work profile via ADB (for example installing an app into the
+ // work profile). Remove DISALLOW_DEBUGGING_FEATURES here to
+ // reduce the potential impact.
+ setLocalUserRestrictionInternal(
+ EnforcingAdmin.createEnterpriseEnforcingAdmin(who, userHandle),
+ UserManager.DISALLOW_DEBUGGING_FEATURES, false, userHandle);
+ }
+
sendOwnerChangedBroadcast(DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED,
userHandle);
});
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
index c62cd6e962b3..7128af5464e8 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
@@ -467,11 +467,17 @@ class PermissionService(private val service: AccessCheckingService) :
override fun getPermissionRequestState(
packageName: String,
permissionName: String,
- deviceId: String
+ deviceId: Int,
+ persistentDeviceId: String
): Int {
val pid = Binder.getCallingPid()
val uid = Binder.getCallingUid()
- val result = context.checkPermission(permissionName, pid, uid)
+ val deviceContext = if (deviceId == context.deviceId){
+ context
+ } else {
+ context.createDeviceContext(deviceId)
+ }
+ val result = deviceContext.checkPermission(permissionName, pid, uid)
if (result == PackageManager.PERMISSION_GRANTED) {
return Context.PERMISSION_REQUEST_STATE_GRANTED
}
@@ -497,14 +503,14 @@ class PermissionService(private val service: AccessCheckingService) :
val permissionFlags =
service.getState {
- getPermissionFlagsWithPolicy(appId, userId, permissionName, deviceId)
+ getPermissionFlagsWithPolicy(appId, userId, permissionName, persistentDeviceId)
}
val isUnreqestable = permissionFlags.hasAnyBit(UNREQUESTABLE_MASK)
// Special case for READ_MEDIA_IMAGES due to photo picker
if ((permissionName == Manifest.permission.READ_MEDIA_IMAGES ||
permissionName == Manifest.permission.READ_MEDIA_VIDEO) && isUnreqestable) {
val isUserSelectedGranted =
- context.checkPermission(
+ deviceContext.checkPermission(
Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED,
pid,
uid,
@@ -515,7 +521,7 @@ class PermissionService(private val service: AccessCheckingService) :
appId,
userId,
Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED,
- deviceId,
+ persistentDeviceId,
)
}
if (
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 8dc657ed75a6..b4e885fe5661 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -141,6 +141,7 @@ public final class UserManagerServiceTest {
.spyStatic(ActivityManager.class)
.mockStatic(Settings.Global.class)
.mockStatic(Settings.Secure.class)
+ .mockStatic(Resources.class)
.build();
@Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(
@@ -202,6 +203,7 @@ public final class UserManagerServiceTest {
doReturn(0)
.when(mSpyResources)
.getInteger(com.android.internal.R.integer.config_hsumBootStrategy);
+ doReturn(mSpyResources).when(() -> Resources.getSystem());
// Must construct UserManagerService in the UiThread
mTestDir = new File(mRealContext.getDataDir(), "umstest");
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 d80fd20dd1e6..ef77a0ee067f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java
@@ -1028,6 +1028,154 @@ 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.
+ // 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);
+ 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);
+
+ 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.mFallbackWallpaper.connection.containsDisplay(incompatibleDisplayId))
+ .isTrue();
+ assertThat(mService.mLastLockWallpaper).isNull();
+ }
+
+ // Test setWallpaperComponent on multiple displays.
+ // GIVEN 3 displays, 0, 2, 3, the existing wallpaper is only compatible for display 0 and 3 but
+ // not 2.
+ // GIVEN the new wallpaper is compatible to 2.
+ // WHEN the new wallpaper is set for system and lock via setWallpaperComponent.
+ // THEN there are 3 connections in mLastWallpaper and 0 connection in mFallbackWallpaper.
+ @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(),
+ 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.mLastLockWallpaper).isNull();
+ }
+
+ // Test setWallpaperComponent on multiple displays.
+ // GIVEN 3 displays, 0, 2, 3, the existing wallpaper is only compatible for display 0 and 3 but
+ // not 2.
+ // GIVEN the new wallpaper is incompatible to 2 and 3.
+ // WHEN the new wallpaper is set for system and lock via setWallpaperComponent.
+ // THEN there are 1 connections in mLastWallpaper and 2 connection in mFallbackWallpaper.
+ @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(),
+ 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.mFallbackWallpaper.connection.containsDisplay(display2)).isTrue();
+ assertThat(mService.mFallbackWallpaper.connection.containsDisplay(display3)).isTrue();
+ assertThat(mService.mLastLockWallpaper).isNull();
+ }
+
+ // 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 two different wallpapers set for system and lock via setWallpaperComponent.
+ // THEN there are two connections in mLastWallpaper, two connection in mLastLockWallpaper and
+ // one connection in mFallbackWallpaper.
+ @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));
+ 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);
+ 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.mLastLockWallpaper.connection.containsDisplay(DEFAULT_DISPLAY))
+ .isTrue();
+ assertThat(mService.mLastLockWallpaper.connection.containsDisplay(compatibleDisplayId))
+ .isTrue();
+ assertThat(mService.mFallbackWallpaper.connection.getConnectedEngineSize()).isEqualTo(1);
+ assertThat(mService.mFallbackWallpaper.connection.containsDisplay(incompatibleDisplayId))
+ .isTrue();
+ }
+
// Verify that after continue switch user from userId 0 to lastUserId, the wallpaper data for
// non-current user must not bind to wallpaper service.
private void verifyNoConnectionBeforeLastUser(int lastUserId) {
diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
index 6b138b986fe7..29a17e1c85ab 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -89,9 +89,13 @@ import android.os.PowerManager;
import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
import android.os.UserHandle;
+import android.os.WorkSource;
import android.os.test.TestLooper;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.DeviceConfig;
import android.provider.Settings;
@@ -200,6 +204,9 @@ public class PowerManagerServiceTest {
@Rule public SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
private PowerManagerService mService;
private ContextWrapper mContextSpy;
private BatteryReceiver mBatteryReceiver;
@@ -3045,6 +3052,40 @@ public class PowerManagerServiceTest {
}
/**
+ * Test IPowerManager.updateWakeLockUids() updates the workchain with the new uids
+ */
+ @Test
+ @RequiresFlagsEnabled({Flags.FLAG_WAKELOCK_ATTRIBUTION_VIA_WORKCHAIN})
+ public void test_updateWakelockUids_updatesWorkchain() {
+ createService();
+ startSystem();
+ final String tag = "wakelock1";
+ final String packageName = "pkg.name";
+ final IBinder token = new Binder();
+ int flags = PowerManager.PARTIAL_WAKE_LOCK;
+ final IWakeLockCallback callback1 = Mockito.mock(IWakeLockCallback.class);
+ final IBinder callbackBinder1 = Mockito.mock(Binder.class);
+ when(callback1.asBinder()).thenReturn(callbackBinder1);
+ WorkSource oldWorksource = new WorkSource();
+ oldWorksource.createWorkChain().addNode(1000, null);
+ mService.getBinderServiceInstance().acquireWakeLock(token, flags, tag, packageName,
+ oldWorksource, null /* historyTag */, Display.INVALID_DISPLAY, callback1);
+ verify(mNotifierMock).onWakeLockAcquired(anyInt(), eq(tag), eq(packageName),
+ anyInt(), anyInt(), eq(oldWorksource), any(), same(callback1));
+
+ WorkSource newWorksource = new WorkSource();
+ newWorksource.createWorkChain().addNode(1011, null)
+ .addNode(Binder.getCallingUid(), null);
+ newWorksource.createWorkChain().addNode(1012, null)
+ .addNode(Binder.getCallingUid(), null);
+ mService.getBinderServiceInstance().updateWakeLockUids(token, new int[]{1011, 1012});
+ verify(mNotifierMock).onWakeLockChanging(anyInt(), eq(tag), eq(packageName),
+ anyInt(), anyInt(), eq(oldWorksource), any(), any(),
+ anyInt(), eq(tag), eq(packageName), anyInt(), anyInt(), eq(newWorksource), any(),
+ any());
+ }
+
+ /**
* Test IPowerManager.updateWakeLockCallback() with a new IWakeLockCallback.
*/
@Test
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockStatsFrameworkEventsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockStatsFrameworkEventsTest.java
index 1fe3f58a9dcb..f24e9ef03398 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockStatsFrameworkEventsTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockStatsFrameworkEventsTest.java
@@ -186,14 +186,16 @@ public class WakelockStatsFrameworkEventsTest {
public void wakelockOpen() throws Exception {
mEvents.noteStartWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1, TS_1);
- ArrayList<WakelockInfo> info = pullResults(TS_3);
-
- assertEquals("size", 1, info.size());
- assertEquals("uid", UID_1, info.get(0).uid);
- assertEquals("tag", TAG_1, info.get(0).tag);
- assertEquals("type", WAKELOCK_TYPE_1, info.get(0).type);
- assertEquals("duration", TS_3 - TS_1, info.get(0).uptimeMillis);
- assertEquals("count", 0, info.get(0).completedCount);
+ for (int i = 0; i < 5; i++) {
+ ArrayList<WakelockInfo> info = pullResults(TS_3);
+
+ assertEquals("size", 1, info.size());
+ assertEquals("uid", UID_1, info.get(0).uid);
+ assertEquals("tag", TAG_1, info.get(0).tag);
+ assertEquals("type", WAKELOCK_TYPE_1, info.get(0).type);
+ assertEquals("duration", TS_3 - TS_1, info.get(0).uptimeMillis);
+ assertEquals("count", 0, info.get(0).completedCount);
+ }
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
index f02bdae1d9e6..457fde8d74d0 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java
@@ -20,11 +20,11 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat
import static com.android.server.testutils.MockitoUtilsKt.eq;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
-import static org.testng.AssertJUnit.assertEquals;
-import static org.testng.AssertJUnit.assertNotNull;
-import static org.testng.AssertJUnit.assertNull;
import android.content.Context;
import android.platform.test.annotations.DisableFlags;
@@ -35,6 +35,7 @@ import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
import android.view.InputDevice;
+import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
@@ -82,69 +83,89 @@ public class AutoclickControllerTest {
@Test
public void onMotionEvent_lazyInitClickScheduler() {
- assertNull(mController.mClickScheduler);
+ assertThat(mController.mClickScheduler).isNull();
injectFakeMouseActionDownEvent();
- assertNotNull(mController.mClickScheduler);
+ assertThat(mController.mClickScheduler).isNotNull();
}
@Test
public void onMotionEvent_nonMouseSource_notInitClickScheduler() {
- assertNull(mController.mClickScheduler);
+ assertThat(mController.mClickScheduler).isNull();
injectFakeNonMouseActionDownEvent();
- assertNull(mController.mClickScheduler);
+ assertThat(mController.mClickScheduler).isNull();
}
@Test
public void onMotionEvent_lazyInitAutoclickSettingsObserver() {
- assertNull(mController.mAutoclickSettingsObserver);
+ assertThat(mController.mAutoclickSettingsObserver).isNull();
injectFakeMouseActionDownEvent();
- assertNotNull(mController.mAutoclickSettingsObserver);
+ assertThat(mController.mAutoclickSettingsObserver).isNotNull();
}
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onMotionEvent_flagOn_lazyInitAutoclickIndicatorScheduler() {
- assertNull(mController.mAutoclickIndicatorScheduler);
+ assertThat(mController.mAutoclickIndicatorScheduler).isNull();
injectFakeMouseActionDownEvent();
- assertNotNull(mController.mAutoclickIndicatorScheduler);
+ assertThat(mController.mAutoclickIndicatorScheduler).isNotNull();
}
@Test
@DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onMotionEvent_flagOff_notInitAutoclickIndicatorScheduler() {
- assertNull(mController.mAutoclickIndicatorScheduler);
+ assertThat(mController.mAutoclickIndicatorScheduler).isNull();
injectFakeMouseActionDownEvent();
- assertNull(mController.mAutoclickIndicatorScheduler);
+ assertThat(mController.mAutoclickIndicatorScheduler).isNull();
}
@Test
@EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onMotionEvent_flagOn_lazyInitAutoclickIndicatorView() {
- assertNull(mController.mAutoclickIndicatorView);
+ assertThat(mController.mAutoclickIndicatorView).isNull();
injectFakeMouseActionDownEvent();
- assertNotNull(mController.mAutoclickIndicatorView);
+ assertThat(mController.mAutoclickIndicatorView).isNotNull();
}
@Test
@DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
public void onMotionEvent_flagOff_notInitAutoclickIndicatorView() {
- assertNull(mController.mAutoclickIndicatorView);
+ assertThat(mController.mAutoclickIndicatorView).isNull();
+
+ injectFakeMouseActionDownEvent();
+
+ assertThat(mController.mAutoclickIndicatorView).isNull();
+ }
+
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void onMotionEvent_flagOn_lazyInitAutoclickTypePanelView() {
+ assertThat(mController.mAutoclickTypePanel).isNull();
+
+ injectFakeMouseActionDownEvent();
+
+ assertThat(mController.mAutoclickTypePanel).isNotNull();
+ }
+
+ @Test
+ @DisableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void onMotionEvent_flagOff_notInitAutoclickTypePanelView() {
+ assertThat(mController.mAutoclickTypePanel).isNull();
injectFakeMouseActionDownEvent();
- assertNull(mController.mAutoclickIndicatorView);
+ assertThat(mController.mAutoclickTypePanel).isNull();
}
@Test
@@ -166,6 +187,18 @@ public class AutoclickControllerTest {
}
@Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void onDestroy_flagOn_removeAutoclickTypePanelViewToWindowManager() {
+ injectFakeMouseActionDownEvent();
+ AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class);
+ mController.mAutoclickTypePanel = mockAutoclickTypePanel;
+
+ mController.onDestroy();
+
+ verify(mockAutoclickTypePanel).hide();
+ }
+
+ @Test
public void onMotionEvent_initClickSchedulerDelayFromSetting() {
injectFakeMouseActionDownEvent();
@@ -175,7 +208,7 @@ public class AutoclickControllerTest {
Settings.Secure.ACCESSIBILITY_AUTOCLICK_DELAY,
AccessibilityManager.AUTOCLICK_DELAY_DEFAULT,
mTestableContext.getUserId());
- assertEquals(delay, mController.mClickScheduler.getDelayForTesting());
+ assertThat(mController.mClickScheduler.getDelayForTesting()).isEqualTo(delay);
}
@Test
@@ -189,7 +222,40 @@ public class AutoclickControllerTest {
Settings.Secure.ACCESSIBILITY_AUTOCLICK_CURSOR_AREA_SIZE,
AccessibilityManager.AUTOCLICK_CURSOR_AREA_SIZE_DEFAULT,
mTestableContext.getUserId());
- assertEquals(size, mController.mAutoclickIndicatorView.getRadiusForTesting());
+ assertThat(mController.mAutoclickIndicatorView.getRadiusForTesting()).isEqualTo(size);
+ }
+
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void onKeyEvent_modifierKey_doNotUpdateMetaStateWhenControllerIsNull() {
+ assertThat(mController.mClickScheduler).isNull();
+
+ injectFakeKeyEvent(KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.META_ALT_ON);
+
+ assertThat(mController.mClickScheduler).isNull();
+ }
+
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void onKeyEvent_modifierKey_updateMetaStateWhenControllerNotNull() {
+ injectFakeMouseActionDownEvent();
+
+ int metaState = KeyEvent.META_ALT_ON | KeyEvent.META_META_ON;
+ injectFakeKeyEvent(KeyEvent.KEYCODE_ALT_LEFT, metaState);
+
+ assertThat(mController.mClickScheduler).isNotNull();
+ assertThat(mController.mClickScheduler.getMetaStateForTesting()).isEqualTo(metaState);
+ }
+
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void onKeyEvent_modifierKey_cancelAutoClickWhenAdditionalRegularKeyPresssed() {
+ injectFakeMouseActionDownEvent();
+
+ injectFakeKeyEvent(KeyEvent.KEYCODE_J, KeyEvent.META_ALT_ON);
+
+ assertThat(mController.mClickScheduler).isNotNull();
+ assertThat(mController.mClickScheduler.getMetaStateForTesting()).isEqualTo(0);
}
@Test
@@ -198,7 +264,7 @@ public class AutoclickControllerTest {
mController.onDestroy();
- assertNull(mController.mClickScheduler);
+ assertThat(mController.mClickScheduler).isNull();
}
@Test
@@ -207,7 +273,7 @@ public class AutoclickControllerTest {
mController.onDestroy();
- assertNull(mController.mAutoclickSettingsObserver);
+ assertThat(mController.mAutoclickSettingsObserver).isNull();
}
@Test
@@ -217,7 +283,7 @@ public class AutoclickControllerTest {
mController.onDestroy();
- assertNull(mController.mAutoclickIndicatorScheduler);
+ assertThat(mController.mAutoclickIndicatorScheduler).isNull();
}
private void injectFakeMouseActionDownEvent() {
@@ -232,6 +298,17 @@ public class AutoclickControllerTest {
mController.onMotionEvent(event, event, /* policyFlags= */ 0);
}
+ private void injectFakeKeyEvent(int keyCode, int modifiers) {
+ KeyEvent keyEvent = new KeyEvent(
+ /* downTime= */ 0,
+ /* eventTime= */ 0,
+ /* action= */ KeyEvent.ACTION_DOWN,
+ /* code= */ keyCode,
+ /* repeat= */ 0,
+ /* metaState= */ modifiers);
+ mController.onKeyEvent(keyEvent, /* policyFlags= */ 0);
+ }
+
private MotionEvent getFakeMotionDownEvent() {
return MotionEvent.obtain(
/* downTime= */ 0,
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 06958b81d846..1627f683cd3e 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -25,6 +25,7 @@ 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;
@@ -115,7 +116,6 @@ 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 send the keyguard shown event
- ArgumentCaptor<ActivityTaskManagerInternal.ScreenObserver> captor = ArgumentCaptor.forClass(
- ActivityTaskManagerInternal.ScreenObserver.class);
- verify(mInjector.mActivityTaskManagerInternal).registerScreenObserver(captor.capture());
- captor.getValue().onKeyguardStateChanged(true);
+ // 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);
// verify the switch now moves on...
Thread.sleep(1000);
@@ -1757,7 +1757,6 @@ 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;
@@ -1779,7 +1778,6 @@ 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);
@@ -1843,11 +1841,6 @@ public class UserControllerTest {
}
@Override
- ActivityTaskManagerInternal getActivityTaskManagerInternal() {
- return mActivityTaskManagerInternal;
- }
-
- @Override
PowerManagerInternal getPowerManagerInternal() {
return mPowerManagerInternal;
}
diff --git a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
index 0bbae247d8bb..2d81f72e3319 100644
--- a/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/VolumeHelperTest.java
@@ -117,6 +117,12 @@ public class VolumeHelperTest {
/** Choose a default stream volume value which does not depend on min/max. */
private static final int DEFAULT_STREAM_VOLUME = 2;
+ /**
+ * The default ringer mode affected stream value since the ringer mode delegate is not used
+ * for unit testing.
+ */
+ private static final int DEFAULT_RINGER_MODE_AFFECTED_STREAMS = 0x1a6;
+
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
@@ -186,6 +192,10 @@ public class VolumeHelperTest {
public void setMuteAffectedStreams(int muteAffectedStreams) {
mMuteAffectedStreams = muteAffectedStreams;
}
+
+ public void setRingerModeAffectedStreams(int ringerModeAffectedStreams) {
+ mRingerModeAffectedStreams = ringerModeAffectedStreams;
+ }
}
private static class TestDeviceVolumeBehaviorDispatcherStub
@@ -550,6 +560,48 @@ public class VolumeHelperTest {
assertEquals(RINGER_MODE_VIBRATE, mAudioService.getRingerModeInternal());
}
+ @Test
+ public void setStreamVolume_doesNotUnmuteStreamAffectedByRingerMode() throws Exception {
+ assumeFalse("Skipping ringer mode test on automotive", mIsAutomotive);
+ mAudioService.setRingerModeAffectedStreams(DEFAULT_RINGER_MODE_AFFECTED_STREAMS);
+ mAudioService.setRingerModeInternal(RINGER_MODE_VIBRATE, mContext.getOpPackageName());
+
+ mAudioService.setStreamVolume(STREAM_NOTIFICATION, /*index=*/1, /*flags=*/0,
+ mContext.getOpPackageName());
+ mTestLooper.dispatchAll();
+
+ assertEquals(0, mAudioService.getStreamVolume(STREAM_NOTIFICATION));
+ }
+
+ @Test
+ public void adjustUnmuteStreamVolume_doesNotUnmuteStreamAffectedByRingerMode()
+ throws Exception {
+ assumeFalse("Skipping ringer mode test on automotive", mIsAutomotive);
+ mAudioService.setRingerModeAffectedStreams(DEFAULT_RINGER_MODE_AFFECTED_STREAMS);
+ mAudioService.setRingerModeInternal(RINGER_MODE_VIBRATE, mContext.getOpPackageName());
+
+ mAudioService.adjustStreamVolume(STREAM_NOTIFICATION, ADJUST_UNMUTE, /*flags=*/0,
+ mContext.getOpPackageName());
+ mTestLooper.dispatchAll();
+
+ assertEquals(0, mAudioService.getStreamVolume(STREAM_NOTIFICATION));
+ }
+
+ @Test
+ public void adjustRaiseStreamVolume_doesNotUnmuteStreamAffectedByRingerMode()
+ throws Exception {
+ assumeFalse("Skipping ringer mode test on automotive", mIsAutomotive);
+ mAudioService.setRingerModeAffectedStreams(DEFAULT_RINGER_MODE_AFFECTED_STREAMS);
+ mAudioService.setRingerModeInternal(RINGER_MODE_VIBRATE, mContext.getOpPackageName());
+
+ mAudioService.adjustStreamVolume(STREAM_NOTIFICATION, ADJUST_RAISE, /*flags=*/0,
+ mContext.getOpPackageName());
+ mTestLooper.dispatchAll();
+
+ assertEquals(0, mAudioService.getStreamVolume(STREAM_NOTIFICATION));
+ }
+
+
// --------------------- Permission tests ---------------------
@Test
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
index 6d863015231c..fcde4055cf17 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ActiveSourceActionTest.java
@@ -16,12 +16,15 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.Constants.ADDR_TV;
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.spy;
import android.annotation.RequiresPermission;
@@ -60,6 +63,9 @@ public class ActiveSourceActionTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
FakeAudioFramework audioFramework = new FakeAudioFramework();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
index a5f7bb117e7d..9a6a24c52143 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcInitiationActionFromAvrTest.java
@@ -15,11 +15,14 @@
*/
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.spy;
import android.annotation.RequiresPermission;
@@ -59,6 +62,9 @@ public class ArcInitiationActionFromAvrTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
FakeAudioFramework audioFramework = new FakeAudioFramework();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
index 857ee1aa176f..ee2f1767d849 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ArcTerminationActionFromAvrTest.java
@@ -15,11 +15,14 @@
*/
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.spy;
import android.annotation.RequiresPermission;
@@ -64,6 +67,9 @@ public class ArcTerminationActionFromAvrTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
MockitoAnnotations.initMocks(this);
mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
index 2296911a4e7e..40f9dbc074c5 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DevicePowerStatusActionTest.java
@@ -16,12 +16,15 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
import static com.android.server.hdmi.Constants.ADDR_TV;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -73,6 +76,9 @@ public class DevicePowerStatusActionTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
MockitoAnnotations.initMocks(this);
mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
index 47cfa4218435..461e98b4bf06 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromPlaybackTest.java
@@ -16,6 +16,7 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_ON;
import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_STANDBY;
import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON;
@@ -30,6 +31,8 @@ import static com.android.server.hdmi.DeviceSelectActionFromPlayback.STATE_WAIT_
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import android.annotation.RequiresPermission;
import android.content.Context;
import android.content.Intent;
@@ -94,6 +97,9 @@ public class DeviceSelectActionFromPlaybackTest {
@Before
public void setUp() {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
MockitoAnnotations.initMocks(this);
Context context = InstrumentationRegistry.getTargetContext();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
index 792faab5b196..a4c71bd6094e 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/DeviceSelectActionFromTvTest.java
@@ -16,6 +16,7 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_ON;
import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_STANDBY;
import static android.hardware.hdmi.HdmiControlManager.POWER_STATUS_TRANSIENT_TO_ON;
@@ -30,6 +31,8 @@ import static com.android.server.hdmi.DeviceSelectActionFromTv.STATE_WAIT_FOR_RE
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import android.annotation.RequiresPermission;
import android.content.Context;
import android.content.Intent;
@@ -106,6 +109,9 @@ public class DeviceSelectActionFromTvTest {
@Before
public void setUp() {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
Context context = InstrumentationRegistry.getTargetContext();
mMyLooper = mTestLooper.getLooper();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
index 30dac9f3813d..0e9f21948907 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTest.java
@@ -15,12 +15,15 @@
*/
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_BOOT_COMPLETED;
import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
import static com.android.server.hdmi.Constants.ADDR_TV;
import static com.android.server.hdmi.Constants.PATH_RELATIONSHIP_ANCESTOR;
import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -92,6 +95,9 @@ public class HdmiCecAtomLoggingTest {
@Before
public void setUp() throws RemoteException {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
mHdmiCecAtomWriterSpy = spy(new HdmiCecAtomWriter());
mLooper = mTestLooper.getLooper();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTvTest.java
index e1e101fc1724..a0e21ed1bdb1 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecAtomLoggingTvTest.java
@@ -15,10 +15,13 @@
*/
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_PENDING;
import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_UNKNOWN;
import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
@@ -78,6 +81,9 @@ public class HdmiCecAtomLoggingTvTest {
@Before
public void setUp() throws RemoteException {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
mHdmiCecAtomWriterSpy = spy(new HdmiCecAtomWriter());
mLooper = mTestLooper.getLooper();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java
index d8c58a8e16b6..e66026735ec4 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecConfigTest.java
@@ -15,10 +15,13 @@
*/
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.google.common.truth.Truth.assertThat;
import static junit.framework.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -65,6 +68,9 @@ public final class HdmiCecConfigTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
MockitoAnnotations.initMocks(this);
mContext = FakeHdmiCecConfig.buildContext(InstrumentationRegistry.getTargetContext());
}
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
index 4f551119b42e..314fe05b6367 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java
@@ -15,6 +15,8 @@
*/
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
@@ -26,6 +28,8 @@ import static com.android.server.hdmi.HdmiControlService.STANDBY_SCREEN_OFF;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import android.annotation.RequiresPermission;
import android.content.Context;
import android.content.Intent;
@@ -85,6 +89,9 @@ public class HdmiCecLocalDeviceAudioSystemTest {
@Before
public void setUp() {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
Context context = InstrumentationRegistry.getTargetContext();
mMyLooper = mTestLooper.getLooper();
mLocalDeviceTypes.add(HdmiDeviceInfo.DEVICE_PLAYBACK);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index cfdf17668229..d600e16c6f13 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -15,6 +15,8 @@
*/
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.Constants.ABORT_UNRECOGNIZED_OPCODE;
import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
@@ -29,6 +31,8 @@ import static com.android.server.hdmi.PowerStatusMonitorActionFromPlayback.MONIT
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import android.annotation.RequiresPermission;
import android.content.Context;
import android.content.Intent;
@@ -97,6 +101,9 @@ public class HdmiCecLocalDevicePlaybackTest {
new FakePowerManagerInternalWrapper();
@Before
public void setUp() {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
MockitoAnnotations.initMocks(this);
Context context = InstrumentationRegistry.getTargetContext();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 5be4490e67ef..f74e2ace7ae3 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -15,6 +15,8 @@
*/
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.Constants.ABORT_UNRECOGNIZED_OPCODE;
import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
@@ -29,8 +31,8 @@ import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_WAKE_UP_ME
import static com.android.server.hdmi.HdmiControlService.STANDBY_SCREEN_OFF;
import static com.android.server.hdmi.HdmiControlService.WAKE_UP_SCREEN_ON;
import static com.android.server.hdmi.RequestActiveSourceAction.TIMEOUT_WAIT_FOR_TV_ASSERT_ACTIVE_SOURCE_MS;
-import static com.android.server.hdmi.RoutingControlAction.TIMEOUT_ROUTING_INFORMATION_MS;
import static com.android.server.hdmi.RequestSadAction.RETRY_COUNTER_MAX;
+import static com.android.server.hdmi.RoutingControlAction.TIMEOUT_ROUTING_INFORMATION_MS;
import static com.google.common.truth.Truth.assertThat;
@@ -38,6 +40,7 @@ import static junit.framework.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.eq;
@@ -167,6 +170,9 @@ public class HdmiCecLocalDeviceTvTest {
@Before
public void setUp() {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
Context context = InstrumentationRegistry.getTargetContext();
mMyLooper = mTestLooper.getLooper();
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 6577e09a986e..587f4370636c 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
@@ -16,6 +16,8 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
import static com.android.server.hdmi.HdmiCecMessageValidator.ERROR_DESTINATION;
@@ -27,6 +29,8 @@ import static com.android.server.hdmi.HdmiCecMessageValidator.OK;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import android.os.test.TestLooper;
import android.platform.test.annotations.Presubmit;
@@ -53,6 +57,9 @@ public class HdmiCecMessageValidatorTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
FakeAudioFramework audioFramework = new FakeAudioFramework();
HdmiControlService mHdmiControlService = new HdmiControlService(
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
index 4f7f381a33d6..b1460b33cdf8 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecNetworkTest.java
@@ -17,10 +17,13 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORTED;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import android.content.Context;
import android.hardware.hdmi.DeviceFeatures;
import android.hardware.hdmi.HdmiControlManager;
@@ -66,6 +69,9 @@ public class HdmiCecNetworkTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
mContext = InstrumentationRegistry.getTargetContext();
FakeAudioFramework audioFramework = new FakeAudioFramework();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
index 3361e7f359e2..c48e4b6cf710 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecPowerStatusControllerTest.java
@@ -15,10 +15,13 @@
*/
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.spy;
import android.annotation.RequiresPermission;
@@ -63,6 +66,9 @@ public class HdmiCecPowerStatusControllerTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
Context contextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
Looper myLooper = mTestLooper.getLooper();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 126a65863f59..4eb3c15eed95 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -15,6 +15,7 @@
*/
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM;
import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_PLAYBACK;
import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_TV;
@@ -38,6 +39,7 @@ import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static junit.framework.TestCase.assertEquals;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -111,6 +113,9 @@ public class HdmiControlServiceTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
HdmiCecConfig hdmiCecConfig = new FakeHdmiCecConfig(mContextSpy);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTvTest.java
index eed99756abb1..d28306458d55 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTvTest.java
@@ -16,12 +16,15 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
import static android.hardware.hdmi.HdmiDeviceInfo.DEVICE_TV;
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import android.content.Context;
import android.hardware.hdmi.HdmiControlManager;
import android.hardware.hdmi.HdmiDeviceInfo;
@@ -60,6 +63,9 @@ public class HdmiControlServiceTvTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
Context context = InstrumentationRegistry.getTargetContext();
mMyLooper = mTestLooper.getLooper();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
index 185f90f4e803..98d2dfb21a0c 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiEarcLocalDeviceTxTest.java
@@ -16,6 +16,7 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
import static android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE;
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
@@ -25,6 +26,7 @@ import static com.android.server.hdmi.Constants.HDMI_EARC_STATUS_EARC_PENDING;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -84,6 +86,9 @@ public class HdmiEarcLocalDeviceTxTest {
@Before
public void setUp() {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
MockitoAnnotations.initMocks(this);
Context context = InstrumentationRegistry.getTargetContext();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
index 974f64dbd84f..76d4b56f0fb3 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/PowerStatusMonitorActionTest.java
@@ -16,6 +16,8 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_1;
@@ -23,6 +25,7 @@ import static com.android.server.hdmi.Constants.ADDR_PLAYBACK_2;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.spy;
import android.annotation.RequiresPermission;
@@ -64,6 +67,9 @@ public class PowerStatusMonitorActionTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
FakeAudioFramework audioFramework = new FakeAudioFramework();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
index 4cf293758519..02e63f43c6c3 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RequestSadActionTest.java
@@ -16,12 +16,16 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
import static com.android.server.hdmi.RequestSadAction.RETRY_COUNTER_MAX;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import android.annotation.RequiresPermission;
import android.content.Context;
import android.content.Intent;
@@ -95,6 +99,9 @@ public class RequestSadActionTest {
@Before
public void setUp() {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
Context context = InstrumentationRegistry.getTargetContext();
mMyLooper = mTestLooper.getLooper();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionPlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionPlaybackTest.java
index 67a3f2a64d32..fa1d3261b84b 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionPlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionPlaybackTest.java
@@ -16,11 +16,15 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.ResendCecCommandAction.SEND_COMMAND_RETRY_MS;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import android.annotation.RequiresPermission;
import android.content.Context;
import android.content.Intent;
@@ -58,6 +62,9 @@ public class ResendCecCommandActionPlaybackTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
Context context = InstrumentationRegistry.getTargetContext();
FakeAudioFramework audioFramework = new FakeAudioFramework();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionTvTest.java
index 047a04c60176..2f68bab743b3 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/ResendCecCommandActionTvTest.java
@@ -16,11 +16,15 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.ResendCecCommandAction.SEND_COMMAND_RETRY_MS;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import android.annotation.RequiresPermission;
import android.content.Context;
import android.content.Intent;
@@ -56,6 +60,9 @@ public class ResendCecCommandActionTvTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
Context context = InstrumentationRegistry.getTargetContext();
FakeAudioFramework audioFramework = new FakeAudioFramework();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
index 1019db46482d..912392f1b70f 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/RoutingControlActionTest.java
@@ -16,6 +16,8 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
import static com.android.server.hdmi.Constants.ADDR_BROADCAST;
@@ -29,6 +31,8 @@ import static com.android.server.hdmi.RoutingControlAction.STATE_WAIT_FOR_ROUTIN
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
+
import android.annotation.RequiresPermission;
import android.content.Context;
import android.content.Intent;
@@ -143,6 +147,9 @@ public class RoutingControlActionTest {
@Before
public void setUp() {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
Context context = InstrumentationRegistry.getTargetContext();
mMyLooper = mTestLooper.getLooper();
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
index e4297effed92..a1a5ffe55eaa 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SetAudioVolumeLevelDiscoveryActionTest.java
@@ -16,6 +16,7 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
import static android.hardware.hdmi.DeviceFeatures.FEATURE_NOT_SUPPORTED;
import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORTED;
import static android.hardware.hdmi.DeviceFeatures.FEATURE_SUPPORT_UNKNOWN;
@@ -24,6 +25,7 @@ import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
@@ -79,6 +81,9 @@ public class SetAudioVolumeLevelDiscoveryActionTest {
*/
@Before
public void setUp() throws RemoteException {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
mContextSpy = spy(new ContextWrapper(
InstrumentationRegistry.getInstrumentation().getTargetContext()));
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
index effea5abcecb..c358e1d4d5db 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/SystemAudioAutoInitiationActionTest.java
@@ -17,12 +17,15 @@
package com.android.server.hdmi;
+import static android.content.pm.PackageManager.FEATURE_HDMI_CEC;
+
import static com.android.server.SystemService.PHASE_SYSTEM_SERVICES_READY;
import static com.android.server.hdmi.Constants.ADDR_AUDIO_SYSTEM;
import static com.android.server.hdmi.SystemAudioAutoInitiationAction.RETRIES_ON_TIMEOUT;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.spy;
import android.annotation.RequiresPermission;
@@ -65,6 +68,9 @@ public class SystemAudioAutoInitiationActionTest {
@Before
public void setUp() throws Exception {
+ assumeTrue("Test requires FEATURE_HDMI_CEC",
+ InstrumentationRegistry.getTargetContext().getPackageManager()
+ .hasSystemFeature(FEATURE_HDMI_CEC));
mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
Looper myLooper = mTestLooper.getLooper();
diff --git a/services/tests/servicestests/src/com/android/server/zram/ZramMaintenanceTest.kt b/services/tests/servicestests/src/com/android/server/zram/ZramMaintenanceTest.kt
new file mode 100644
index 000000000000..5448a05aafc3
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/zram/ZramMaintenanceTest.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.server.zram
+
+import android.app.job.JobInfo
+import android.app.job.JobParameters
+import android.app.job.JobScheduler
+import android.os.IMmd
+import android.os.PersistableBundle
+import android.os.RemoteException
+import android.testing.TestableContext
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+
+import com.android.server.ZramMaintenance
+import com.google.common.truth.Truth.assertThat
+
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+private fun generateJobParameters(jobId: Int, extras: PersistableBundle): JobParameters {
+ return JobParameters(
+ null, "", jobId, extras, null, null, 0, false, false, false, null, null, null
+ )
+}
+
+@SmallTest
+@RunWith(JUnit4::class)
+class ZramMaintenanceTest {
+ private val context = TestableContext(InstrumentationRegistry.getInstrumentation().context)
+
+ @Captor
+ private lateinit var jobInfoCaptor: ArgumentCaptor<JobInfo>
+
+ @Mock
+ private lateinit var mockJobScheduler: JobScheduler
+
+ @Mock
+ private lateinit var mockMmd: IMmd
+
+ @Before
+ @Throws(RemoteException::class)
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ context.addMockSystemService(JobScheduler::class.java, mockJobScheduler)
+ }
+
+ @Test
+ fun startZramMaintenance() {
+ ZramMaintenance.startZramMaintenance(context)
+
+ verify(mockJobScheduler, times(1)).schedule(jobInfoCaptor.capture())
+ val job = jobInfoCaptor.value
+ assertThat(job.id).isEqualTo(ZramMaintenance.JOB_ID)
+ assertThat(job.extras.getBoolean(ZramMaintenance.KEY_CHECK_STATUS)).isTrue()
+ }
+
+ @Test
+ fun startJobForFirstTime() {
+ val extras = PersistableBundle()
+ extras.putBoolean(ZramMaintenance.KEY_CHECK_STATUS, true)
+ val params = generateJobParameters(
+ ZramMaintenance.JOB_ID,
+ extras,
+ )
+ `when`(mockMmd.isZramMaintenanceSupported()).thenReturn(true)
+
+ ZramMaintenance.startJob(context, params, mockMmd)
+
+ verify(mockMmd, times(1)).isZramMaintenanceSupported()
+ verify(mockMmd, times(1)).doZramMaintenanceAsync()
+ verify(mockJobScheduler, times(1)).schedule(jobInfoCaptor.capture())
+ val nextJob = jobInfoCaptor.value
+ assertThat(nextJob.id).isEqualTo(ZramMaintenance.JOB_ID)
+ assertThat(nextJob.extras.getBoolean(ZramMaintenance.KEY_CHECK_STATUS)).isFalse()
+ }
+
+ @Test
+ fun startJobWithoutCheckStatus() {
+ val extras = PersistableBundle()
+ extras.putBoolean(ZramMaintenance.KEY_CHECK_STATUS, false)
+ val params = generateJobParameters(
+ ZramMaintenance.JOB_ID,
+ extras,
+ )
+
+ ZramMaintenance.startJob(context, params, mockMmd)
+
+ verify(mockMmd, never()).isZramMaintenanceSupported()
+ verify(mockMmd, times(1)).doZramMaintenanceAsync()
+ verify(mockJobScheduler, times(1)).schedule(jobInfoCaptor.capture())
+ val nextJob = jobInfoCaptor.value
+ assertThat(nextJob.id).isEqualTo(ZramMaintenance.JOB_ID)
+ assertThat(nextJob.extras.getBoolean(ZramMaintenance.KEY_CHECK_STATUS)).isFalse()
+ }
+
+ @Test
+ fun startJobZramIsDisabled() {
+ val extras = PersistableBundle()
+ extras.putBoolean(ZramMaintenance.KEY_CHECK_STATUS, true)
+ val params = generateJobParameters(
+ ZramMaintenance.JOB_ID,
+ extras,
+ )
+ `when`(mockMmd.isZramMaintenanceSupported()).thenReturn(false)
+
+ ZramMaintenance.startJob(context, params, mockMmd)
+
+ verify(mockMmd, times(1)).isZramMaintenanceSupported()
+ verify(mockMmd, never()).doZramMaintenanceAsync()
+ verify(mockJobScheduler, never()).schedule(any())
+ }
+
+ @Test
+ fun startJobMmdIsNotReadyYet() {
+ val extras = PersistableBundle()
+ extras.putBoolean(ZramMaintenance.KEY_CHECK_STATUS, true)
+ val params = generateJobParameters(
+ ZramMaintenance.JOB_ID,
+ extras,
+ )
+
+ ZramMaintenance.startJob(context, params, null)
+
+ verify(mockJobScheduler, times(1)).schedule(jobInfoCaptor.capture())
+ val nextJob = jobInfoCaptor.value
+ assertThat(nextJob.id).isEqualTo(ZramMaintenance.JOB_ID)
+ assertThat(nextJob.extras.getBoolean(ZramMaintenance.KEY_CHECK_STATUS)).isTrue()
+ }
+} \ No newline at end of file
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index fa733e85c89c..4a977be2aad9 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -96,12 +96,12 @@ import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
-import java.util.ArrayList;
-import java.util.List;
-
import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;
+import java.util.ArrayList;
+import java.util.List;
+
@SmallTest
@SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the class.
@RunWith(ParameterizedAndroidJunit4.class)
@@ -2671,7 +2671,7 @@ public class GroupHelperTest extends UiServiceTestCase {
r.updateNotificationChannel(channel);
}
mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel,
- notificationList);
+ notificationList, summaryByGroup);
// Check that all notifications are moved to the silent section group
verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
@@ -2735,7 +2735,7 @@ public class GroupHelperTest extends UiServiceTestCase {
}
}
mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel1,
- notificationList);
+ notificationList, summaryByGroup);
// Check that the override group key was cleared
for (NotificationRecord record: notificationList) {
@@ -2812,7 +2812,7 @@ public class GroupHelperTest extends UiServiceTestCase {
}
}
mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel1,
- notificationList);
+ notificationList, summaryByGroup);
// Check that the override group key was cleared
for (NotificationRecord record: notificationList) {
@@ -2864,7 +2864,7 @@ public class GroupHelperTest extends UiServiceTestCase {
// Classify/bundle child notifications
final NotificationChannel socialChannel = new NotificationChannel(
NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
- IMPORTANCE_DEFAULT);
+ IMPORTANCE_LOW);
final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg,
AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier());
final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes(
@@ -2872,7 +2872,7 @@ public class GroupHelperTest extends UiServiceTestCase {
NotificationChannel.SOCIAL_MEDIA_ID);
final NotificationChannel newsChannel = new NotificationChannel(
NotificationChannel.NEWS_ID, NotificationChannel.NEWS_ID,
- IMPORTANCE_DEFAULT);
+ IMPORTANCE_LOW);
final String expectedGroupKey_news = GroupHelper.getFullAggregateGroupKey(pkg,
AGGREGATE_GROUP_KEY + "NewsSection", UserHandle.SYSTEM.getIdentifier());
final NotificationAttributes expectedSummaryAttr_news = new NotificationAttributes(
@@ -2944,7 +2944,7 @@ public class GroupHelperTest extends UiServiceTestCase {
// Classify/bundle child notifications
final NotificationChannel socialChannel = new NotificationChannel(
NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
- IMPORTANCE_DEFAULT);
+ IMPORTANCE_LOW);
for (NotificationRecord record: notificationList) {
if (record.getChannel().getId().equals(channel1.getId())
&& record.getNotification().isGroupChild()) {
@@ -2999,7 +2999,7 @@ public class GroupHelperTest extends UiServiceTestCase {
// Classify/bundle child notifications
final NotificationChannel socialChannel = new NotificationChannel(
NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
- IMPORTANCE_DEFAULT);
+ IMPORTANCE_LOW);
final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg,
AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier());
final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes(
@@ -3095,7 +3095,7 @@ public class GroupHelperTest extends UiServiceTestCase {
reset(mCallback);
final NotificationChannel socialChannel = new NotificationChannel(
NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
- IMPORTANCE_DEFAULT);
+ IMPORTANCE_LOW);
final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg,
AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier());
final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes(
@@ -3149,7 +3149,7 @@ public class GroupHelperTest extends UiServiceTestCase {
// adjustments applied while enqueued will use NotificationAdjustmentExtractor.
final NotificationChannel socialChannel = new NotificationChannel(
NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
- IMPORTANCE_DEFAULT);
+ IMPORTANCE_LOW);
final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg,
AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier());
final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes(
@@ -3209,7 +3209,7 @@ public class GroupHelperTest extends UiServiceTestCase {
// Classify/bundle all child notifications: original group & summary is removed
final NotificationChannel socialChannel = new NotificationChannel(
NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
- IMPORTANCE_DEFAULT);
+ IMPORTANCE_LOW);
for (NotificationRecord record: notificationList) {
if (record.getOriginalGroupKey().contains("testGrp")
&& record.getNotification().isGroupChild()) {
@@ -3297,7 +3297,7 @@ public class GroupHelperTest extends UiServiceTestCase {
// Classify/bundle child notifications: all except one, to keep the original group
final NotificationChannel socialChannel = new NotificationChannel(
NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
- IMPORTANCE_DEFAULT);
+ IMPORTANCE_LOW);
final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg,
AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier());
final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes(
@@ -3378,6 +3378,314 @@ public class GroupHelperTest extends UiServiceTestCase {
}
@Test
+ @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+ FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION,
+ FLAG_NOTIFICATION_CLASSIFICATION})
+ public void testUnbundleByImportanceNotification_originalSummaryExists() {
+ // Check that unbundled notifications are moved to the original section and original group
+ // when the original summary is still present
+ final List<NotificationRecord> notificationList = new ArrayList<>();
+ final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+ final String pkg = "package";
+
+ final int summaryId = 0;
+ final int numChildren = AUTOGROUP_AT_COUNT + 1;
+ // Post a regular/valid group: summary + notifications
+ NotificationRecord summary = getNotificationRecord(pkg, summaryId,
+ String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true);
+ notificationList.add(summary);
+ summaryByGroup.put(summary.getGroupKey(), summary);
+ final String originalAppGroupKey = summary.getGroupKey();
+ final NotificationChannel originalChannel = summary.getChannel();
+ for (int i = 0; i < numChildren; i++) {
+ NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42),
+ UserHandle.SYSTEM, "testGrp", false);
+ notificationList.add(child);
+ mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup);
+ }
+
+ // Classify/bundle child notifications: all except one, to keep the original group
+ final NotificationChannel socialChannel = new NotificationChannel(
+ NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
+ IMPORTANCE_LOW);
+ final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg,
+ AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier());
+ final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes(
+ BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
+ NotificationChannel.SOCIAL_MEDIA_ID);
+ int numChildrenBundled = 0;
+ for (NotificationRecord record: notificationList) {
+ if (record.getOriginalGroupKey().contains("testGrp")
+ && record.getNotification().isGroupChild()) {
+ record.updateNotificationChannel(socialChannel);
+ mGroupHelper.onChannelUpdated(record);
+ numChildrenBundled++;
+ if (numChildrenBundled == AUTOGROUP_AT_COUNT) {
+ break;
+ }
+ }
+ }
+
+ // Check that 1 autogroup summaries were created for the social section
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social));
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+ eq(expectedGroupKey_social), eq(true));
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+ any());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).removeAppProvidedSummaryOnClassification(
+ anyString(), eq(originalAppGroupKey));
+
+ // Adjust group key for grouped notifications
+ for (NotificationRecord record: notificationList) {
+ if (record.getOriginalGroupKey().contains("testGrp")
+ && NotificationChannel.SYSTEM_RESERVED_IDS.contains(
+ record.getChannel().getId())) {
+ record.setOverrideGroupKey(expectedGroupKey_social);
+ }
+ }
+
+ // Add 1 ungrouped notification in the original section
+ NotificationRecord ungroupedNotification = getNotificationRecord(pkg, 4242,
+ String.valueOf(4242), UserHandle.SYSTEM);
+ notificationList.add(ungroupedNotification);
+ mGroupHelper.onNotificationPosted(ungroupedNotification, false);
+
+ // Unbundle the bundled notifications by changing the social channel importance to alerting
+ // => social section summary is destroyed
+ // and notifications are moved back to the original group
+ reset(mCallback);
+ socialChannel.setImportance(IMPORTANCE_DEFAULT);
+ for (NotificationRecord record: notificationList) {
+ if (record.getNotification().isGroupChild()
+ && record.getOriginalGroupKey().contains("testGrp")
+ && NotificationChannel.SYSTEM_RESERVED_IDS.contains(
+ record.getChannel().getId())) {
+ record.updateNotificationChannel(socialChannel);
+ }
+ }
+ mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, socialChannel,
+ notificationList, summaryByGroup);
+
+ // Check that the autogroup summary for the social section was removed
+ // and that no new autogroup summaries were created
+ verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+ anyString(), anyInt(), any());
+ verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg),
+ eq(expectedGroupKey_social));
+
+ for (NotificationRecord record: notificationList) {
+ if (record.getNotification().isGroupChild()
+ && record.getOriginalGroupKey().contains("testGrp")) {
+ assertThat(record.getSbn().getOverrideGroupKey()).isNull();
+ assertThat(GroupHelper.getSection(record).mName).isEqualTo("AlertingSection");
+ }
+ }
+ }
+
+ @Test
+ @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+ FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION,
+ FLAG_NOTIFICATION_CLASSIFICATION})
+ public void testUnbundleByImportanceNotification_originalSummaryRemoved() {
+ // Check that unbundled notifications are moved to the original section and autogrouped
+ // when the original summary is not present
+ final List<NotificationRecord> notificationList = new ArrayList<>();
+ final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+ final String pkg = "package";
+
+ final int summaryId = 0;
+ final int numChildren = AUTOGROUP_AT_COUNT + 1;
+ // Post a regular/valid group: summary + notifications
+ NotificationRecord summary = getNotificationRecord(pkg, summaryId,
+ String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true);
+ notificationList.add(summary);
+ summaryByGroup.put(summary.getGroupKey(), summary);
+ final String originalAppGroupKey = summary.getGroupKey();
+ final NotificationChannel originalChannel = summary.getChannel();
+ for (int i = 0; i < numChildren; i++) {
+ NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42),
+ UserHandle.SYSTEM, "testGrp", false);
+ notificationList.add(child);
+ mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup);
+ }
+
+ // Classify/bundle child notifications: all except one, to keep the original group
+ final NotificationChannel socialChannel = new NotificationChannel(
+ NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
+ IMPORTANCE_LOW);
+ final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg,
+ AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier());
+ final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes(
+ BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
+ NotificationChannel.SOCIAL_MEDIA_ID);
+ int numChildrenBundled = 0;
+ for (NotificationRecord record: notificationList) {
+ if (record.getOriginalGroupKey().contains("testGrp")
+ && record.getNotification().isGroupChild()) {
+ record.updateNotificationChannel(socialChannel);
+ mGroupHelper.onChannelUpdated(record);
+ numChildrenBundled++;
+ if (numChildrenBundled == AUTOGROUP_AT_COUNT) {
+ break;
+ }
+ }
+ }
+
+ // Check that 1 autogroup summaries were created for the social section
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social));
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+ eq(expectedGroupKey_social), eq(true));
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+ any());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).removeAppProvidedSummaryOnClassification(
+ anyString(), eq(originalAppGroupKey));
+
+ // Adjust group key
+ for (NotificationRecord record: notificationList) {
+ if (record.getOriginalGroupKey().contains("testGrp")
+ && NotificationChannel.SYSTEM_RESERVED_IDS.contains(
+ record.getChannel().getId())) {
+ record.setOverrideGroupKey(expectedGroupKey_social);
+ }
+ }
+
+ // Remove original summary
+ notificationList.remove(summary);
+ summaryByGroup.remove(summary.getGroupKey());
+
+ // Add 1 ungrouped notification in the original section
+ NotificationRecord ungroupedNotification = getNotificationRecord(pkg, 4242,
+ String.valueOf(4242), UserHandle.SYSTEM);
+ notificationList.add(ungroupedNotification);
+ mGroupHelper.onNotificationPosted(ungroupedNotification, false);
+
+ // Unbundle the bundled notifications by changing the social channel importance to alerting
+ // => social section summary is destroyed
+ // and notifications are moved back to the alerting section and autogrouped
+ reset(mCallback);
+ socialChannel.setImportance(IMPORTANCE_DEFAULT);
+ for (NotificationRecord record: notificationList) {
+ if (record.getNotification().isGroupChild()
+ && record.getOriginalGroupKey().contains("testGrp")
+ && NotificationChannel.SYSTEM_RESERVED_IDS.contains(
+ record.getChannel().getId())) {
+ record.updateNotificationChannel(socialChannel);
+ }
+ }
+ mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, socialChannel,
+ notificationList, summaryByGroup);
+
+ // Check that the autogroup summary for the social section was removed
+ // and that a new autogroup was created in the alerting section
+ final String expectedGroupKey_alerting = GroupHelper.getFullAggregateGroupKey(pkg,
+ AGGREGATE_GROUP_KEY + "AlertingSection", UserHandle.SYSTEM.getIdentifier());
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(expectedGroupKey_alerting), anyInt(), any());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT + 1)).addAutoGroup(anyString(),
+ eq(expectedGroupKey_alerting), eq(true));
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, times(1)).removeAutoGroupSummary(anyInt(), eq(pkg),
+ eq(expectedGroupKey_social));
+ verify(mCallback, never()).removeAppProvidedSummaryOnClassification(anyString(),
+ anyString());
+ }
+
+ @Test
+ @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING,
+ FLAG_NOTIFICATION_CLASSIFICATION,
+ FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION})
+ public void testClassifyWithAlertingImportance_doesNotBundle() {
+ // Check that classified notifications are autogrouped when channel importance
+ // is updated DEFAULT to LOW
+ final List<NotificationRecord> notificationList = new ArrayList<>();
+ final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+ final String pkg = "package";
+
+ final int summaryId = 0;
+ final int numChildren = AUTOGROUP_AT_COUNT + 1;
+ // Post a regular/valid group: summary + notifications
+ NotificationRecord summary = getNotificationRecord(pkg, summaryId,
+ String.valueOf(summaryId), UserHandle.SYSTEM, "testGrp", true);
+ notificationList.add(summary);
+ summaryByGroup.put(summary.getGroupKey(), summary);
+ final String originalAppGroupKey = summary.getGroupKey();
+ final NotificationChannel originalChannel = summary.getChannel();
+ for (int i = 0; i < numChildren; i++) {
+ NotificationRecord child = getNotificationRecord(pkg, i + 42, String.valueOf(i + 42),
+ UserHandle.SYSTEM, "testGrp", false);
+ notificationList.add(child);
+ mGroupHelper.onNotificationPostedWithDelay(child, notificationList, summaryByGroup);
+ }
+
+ // Classify child notifications to Alerting bundle channel => do not "bundle"
+ final NotificationChannel socialChannel = new NotificationChannel(
+ NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
+ IMPORTANCE_DEFAULT);
+ int numChildrenBundled = 0;
+ for (NotificationRecord record: notificationList) {
+ if (record.getOriginalGroupKey().contains("testGrp")
+ && record.getNotification().isGroupChild()) {
+ record.updateNotificationChannel(socialChannel);
+ mGroupHelper.onChannelUpdated(record);
+ numChildrenBundled++;
+ if (numChildrenBundled == AUTOGROUP_AT_COUNT) {
+ break;
+ }
+ }
+ }
+
+ // Check that no autogroup summaries were created for the social section
+ verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+ anyString(), anyInt(), any());
+ verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean());
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+ any());
+ verify(mCallback, never()).removeAppProvidedSummaryOnClassification(anyString(),
+ anyString());
+
+ // Change importance to LOW => autogroup notifications in bundle section
+ reset(mCallback);
+ final String expectedGroupKey_social = GroupHelper.getFullAggregateGroupKey(pkg,
+ AGGREGATE_GROUP_KEY + "SocialSection", UserHandle.SYSTEM.getIdentifier());
+ final NotificationAttributes expectedSummaryAttr_social = new NotificationAttributes(
+ BASE_FLAGS, mSmallIcon, COLOR_DEFAULT, DEFAULT_VISIBILITY, DEFAULT_GROUP_ALERT,
+ NotificationChannel.SOCIAL_MEDIA_ID);
+ socialChannel.setImportance(IMPORTANCE_LOW);
+ for (NotificationRecord record: notificationList) {
+ if (record.getNotification().isGroupChild()
+ && record.getOriginalGroupKey().contains("testGrp")
+ && NotificationChannel.SYSTEM_RESERVED_IDS.contains(
+ record.getChannel().getId())) {
+ record.updateNotificationChannel(socialChannel);
+ }
+ }
+ mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, socialChannel,
+ notificationList, summaryByGroup);
+
+ // Check that 1 autogroup summaries were created for the social section
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(expectedGroupKey_social), anyInt(), eq(expectedSummaryAttr_social));
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+ eq(expectedGroupKey_social), eq(true));
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+ verify(mCallback, never()).updateAutogroupSummary(anyInt(), anyString(), anyString(),
+ any());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).removeAppProvidedSummaryOnClassification(
+ anyString(), eq(originalAppGroupKey));
+ }
+
+ @Test
@EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
public void testMoveAggregateGroups_updateChannel_groupsUngrouped() {
final String pkg = "package";
@@ -3432,7 +3740,7 @@ public class GroupHelperTest extends UiServiceTestCase {
r.updateNotificationChannel(channel);
}
mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel,
- notificationList);
+ notificationList, summaryByGroup);
// Check that all notifications are moved to the silent section group
verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
@@ -3489,7 +3797,7 @@ public class GroupHelperTest extends UiServiceTestCase {
}
}
mGroupHelper.onChannelUpdated(UserHandle.SYSTEM.getIdentifier(), pkg, channel1,
- notificationList);
+ notificationList, new ArrayMap<>());
// Check that channel1's notifications are moved to the silent section & autogroup all
NotificationAttributes expectedSummaryAttr = new NotificationAttributes(BASE_FLAGS,
@@ -3940,14 +4248,14 @@ public class GroupHelperTest extends UiServiceTestCase {
// Check that special categories are grouped in their own sections
final NotificationChannel promoChannel = new NotificationChannel(
NotificationChannel.PROMOTIONS_ID, NotificationChannel.PROMOTIONS_ID,
- IMPORTANCE_DEFAULT);
+ IMPORTANCE_LOW);
final NotificationRecord notification_promotion = getNotificationRecord(mPkg, 0, "", mUser,
"", false, promoChannel);
assertThat(GroupHelper.getSection(notification_promotion).mName).isEqualTo(
"PromotionsSection");
final NotificationChannel newsChannel = new NotificationChannel(NotificationChannel.NEWS_ID,
- NotificationChannel.NEWS_ID, IMPORTANCE_DEFAULT);
+ NotificationChannel.NEWS_ID, IMPORTANCE_LOW);
final NotificationRecord notification_news = getNotificationRecord(mPkg, 0, "", mUser,
"", false, newsChannel);
assertThat(GroupHelper.getSection(notification_news).mName).isEqualTo(
@@ -3955,18 +4263,49 @@ public class GroupHelperTest extends UiServiceTestCase {
final NotificationChannel socialChannel = new NotificationChannel(
NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
- IMPORTANCE_DEFAULT);
+ IMPORTANCE_LOW);
final NotificationRecord notification_social = getNotificationRecord(mPkg, 0, "", mUser,
"", false, socialChannel);
assertThat(GroupHelper.getSection(notification_social).mName).isEqualTo(
"SocialSection");
final NotificationChannel recsChannel = new NotificationChannel(NotificationChannel.RECS_ID,
- NotificationChannel.RECS_ID, IMPORTANCE_DEFAULT);
+ NotificationChannel.RECS_ID, IMPORTANCE_LOW);
final NotificationRecord notification_recs = getNotificationRecord(mPkg, 0, "", mUser,
"", false, recsChannel);
assertThat(GroupHelper.getSection(notification_recs).mName).isEqualTo(
"RecsSection");
+
+ // Check that bundle categories with importance > IMPORTANCE_LOW are grouped into Alerting
+ final NotificationChannel promoChannelAlerting = new NotificationChannel(
+ NotificationChannel.PROMOTIONS_ID, NotificationChannel.PROMOTIONS_ID,
+ IMPORTANCE_DEFAULT);
+ final NotificationRecord notification_promotion_alerting = getNotificationRecord(mPkg, 0,
+ "", mUser, "", false, promoChannelAlerting);
+ assertThat(GroupHelper.getSection(notification_promotion_alerting).mName).isEqualTo(
+ "AlertingSection");
+
+ final NotificationChannel newsChannelAlerting = new NotificationChannel(
+ NotificationChannel.NEWS_ID, NotificationChannel.NEWS_ID, IMPORTANCE_DEFAULT);
+ final NotificationRecord notification_news_alerting = getNotificationRecord(mPkg, 0, "",
+ mUser, "", false, newsChannelAlerting);
+ assertThat(GroupHelper.getSection(notification_news_alerting).mName).isEqualTo(
+ "AlertingSection");
+
+ final NotificationChannel socialChannelAlerting = new NotificationChannel(
+ NotificationChannel.SOCIAL_MEDIA_ID, NotificationChannel.SOCIAL_MEDIA_ID,
+ IMPORTANCE_DEFAULT);
+ final NotificationRecord notification_social_alerting = getNotificationRecord(mPkg, 0, "",
+ mUser, "", false, socialChannelAlerting);
+ assertThat(GroupHelper.getSection(notification_social_alerting).mName).isEqualTo(
+ "AlertingSection");
+
+ final NotificationChannel recsChannelAlerting = new NotificationChannel(
+ NotificationChannel.RECS_ID, NotificationChannel.RECS_ID, IMPORTANCE_DEFAULT);
+ final NotificationRecord notification_recs_alerting = getNotificationRecord(mPkg, 0, "",
+ mUser, "", false, recsChannelAlerting);
+ assertThat(GroupHelper.getSection(notification_recs_alerting).mName).isEqualTo(
+ "AlertingSection");
}
@Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
index 19b90b6b76d9..076e3e9fcc24 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
@@ -22,6 +22,7 @@ import static android.service.notification.Adjustment.KEY_TYPE;
import static android.service.notification.Adjustment.TYPE_CONTENT_RECOMMENDATION;
import static android.service.notification.Adjustment.TYPE_NEWS;
import static android.service.notification.Adjustment.TYPE_PROMOTION;
+import static android.service.notification.Adjustment.TYPE_SOCIAL_MEDIA;
import static com.android.server.notification.NotificationManagerService.DEFAULT_ALLOWED_ADJUSTMENTS;
@@ -611,7 +612,8 @@ public class NotificationAssistantsTest extends UiServiceTestCase {
ManagedServices.ManagedServiceInfo info =
mAssistants.new ManagedServiceInfo(null, mCn, userId, false, null, 35, 2345256);
- mAssistants.setAdjustmentTypeSupportedState(info, Adjustment.KEY_NOT_CONVERSATION, false);
+ mAssistants.setAdjustmentTypeSupportedState(
+ info.userid, Adjustment.KEY_NOT_CONVERSATION, false);
assertThat(mAssistants.getUnsupportedAdjustments(userId)).contains(
Adjustment.KEY_NOT_CONVERSATION);
@@ -632,7 +634,8 @@ public class NotificationAssistantsTest extends UiServiceTestCase {
ManagedServices.ManagedServiceInfo info =
mAssistants.new ManagedServiceInfo(null, mCn, userId, false, null, 35, 2345256);
- mAssistants.setAdjustmentTypeSupportedState(info, Adjustment.KEY_NOT_CONVERSATION, false);
+ mAssistants.setAdjustmentTypeSupportedState(
+ info.userid, Adjustment.KEY_NOT_CONVERSATION, false);
writeXmlAndReload(USER_ALL);
@@ -654,7 +657,6 @@ public class NotificationAssistantsTest extends UiServiceTestCase {
assertNotNull(current);
writeXmlAndReload(USER_ALL);
-
assertThat(mAssistants.getUnsupportedAdjustments(userId).size()).isEqualTo(0);
}
@@ -707,26 +709,29 @@ public class NotificationAssistantsTest extends UiServiceTestCase {
@Test
@EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
public void testSetAssistantAdjustmentKeyTypeState_allow() {
- assertThat(mAssistants.getAllowedClassificationTypes()).asList()
- .containsExactly(TYPE_PROMOTION);
+ mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_CONTENT_RECOMMENDATION, false);
+ assertThat(mAssistants.getAllowedClassificationTypes())
+ .asList().doesNotContain(TYPE_CONTENT_RECOMMENDATION);
mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_CONTENT_RECOMMENDATION, true);
assertThat(mAssistants.getAllowedClassificationTypes()).asList()
- .containsExactlyElementsIn(List.of(TYPE_PROMOTION, TYPE_CONTENT_RECOMMENDATION));
+ .contains(TYPE_CONTENT_RECOMMENDATION);
}
@Test
@EnableFlags(android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION)
public void testSetAssistantAdjustmentKeyTypeState_disallow() {
mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_PROMOTION, false);
- assertThat(mAssistants.getAllowedClassificationTypes()).isEmpty();
+ assertThat(mAssistants.getAllowedClassificationTypes())
+ .asList().doesNotContain(TYPE_PROMOTION);
}
@Test
@EnableFlags(Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI)
public void testDisallowAdjustmentKeyType_readWriteXml() throws Exception {
mAssistants.loadDefaultsFromConfig(true);
+ mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_SOCIAL_MEDIA, false);
mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_PROMOTION, false);
mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_NEWS, true);
mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_CONTENT_RECOMMENDATION, true);
@@ -745,7 +750,8 @@ public class NotificationAssistantsTest extends UiServiceTestCase {
writeXmlAndReload(USER_ALL);
assertThat(mAssistants.getAllowedClassificationTypes()).asList()
- .containsExactly(TYPE_PROMOTION);
+ .containsExactlyElementsIn(List.of(TYPE_PROMOTION, TYPE_NEWS, TYPE_SOCIAL_MEDIA,
+ TYPE_CONTENT_RECOMMENDATION));
}
@Test
@@ -757,18 +763,22 @@ public class NotificationAssistantsTest extends UiServiceTestCase {
String allowedPackage = "allowed.package";
assertThat(mAssistants.isAdjustmentAllowedForPackage(key, allowedPackage)).isTrue();
+ assertThat(mAssistants.getAdjustmentDeniedPackages(key)).isEmpty();
// Set type adjustment disallowed for this package
mAssistants.setAdjustmentSupportedForPackage(key, allowedPackage, false);
// Then the package is marked as denied
assertThat(mAssistants.isAdjustmentAllowedForPackage(key, allowedPackage)).isFalse();
+ assertThat(mAssistants.getAdjustmentDeniedPackages(key)).asList()
+ .containsExactly(allowedPackage);
// Set type adjustment allowed again
mAssistants.setAdjustmentSupportedForPackage(key, allowedPackage, true);
// Then the package is marked as allowed again
assertThat(mAssistants.isAdjustmentAllowedForPackage(key, allowedPackage)).isTrue();
+ assertThat(mAssistants.getAdjustmentDeniedPackages(key)).isEmpty();
}
@Test
@@ -789,6 +799,8 @@ public class NotificationAssistantsTest extends UiServiceTestCase {
assertThat(mAssistants.isAdjustmentAllowedForPackage(key, deniedPkg1)).isFalse();
assertThat(mAssistants.isAdjustmentAllowedForPackage(key, deniedPkg2)).isFalse();
assertThat(mAssistants.isAdjustmentAllowedForPackage(key, deniedPkg3)).isFalse();
+ assertThat(mAssistants.getAdjustmentDeniedPackages(key)).asList()
+ .containsExactlyElementsIn(List.of(deniedPkg1, deniedPkg2, deniedPkg3));
// And when we re-allow one of them,
mAssistants.setAdjustmentSupportedForPackage(key, deniedPkg2, true);
@@ -797,6 +809,8 @@ public class NotificationAssistantsTest extends UiServiceTestCase {
assertThat(mAssistants.isAdjustmentAllowedForPackage(key, deniedPkg1)).isFalse();
assertThat(mAssistants.isAdjustmentAllowedForPackage(key, deniedPkg2)).isTrue();
assertThat(mAssistants.isAdjustmentAllowedForPackage(key, deniedPkg3)).isFalse();
+ assertThat(mAssistants.getAdjustmentDeniedPackages(key)).asList()
+ .containsExactlyElementsIn(List.of(deniedPkg1, deniedPkg3));
}
@Test
@@ -860,8 +874,9 @@ public class NotificationAssistantsTest extends UiServiceTestCase {
mAssistants.new ManagedServiceInfo(null, mCn, userId, false, null, 35, 2345256);
// Ensure bundling is enabled
- mAssistants.setAdjustmentTypeSupportedState(info, KEY_TYPE, true);
+ mAssistants.setAdjustmentTypeSupportedState(info.userid, KEY_TYPE, true);
// Enable these specific bundle types
+ mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_SOCIAL_MEDIA, false);
mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_PROMOTION, false);
mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_NEWS, true);
mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_CONTENT_RECOMMENDATION, true);
@@ -894,7 +909,7 @@ public class NotificationAssistantsTest extends UiServiceTestCase {
.isEqualTo(NotificationProtoEnums.TYPE_CONTENT_RECOMMENDATION);
// Disable the top-level bundling setting
- mAssistants.setAdjustmentTypeSupportedState(info, KEY_TYPE, false);
+ mAssistants.setAdjustmentTypeSupportedState(info.userid, KEY_TYPE, false);
// Enable these specific bundle types
mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_PROMOTION, true);
mAssistants.setAssistantAdjustmentKeyTypeState(TYPE_NEWS, false);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index f8c8a1dbb481..3aa95449cc98 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -2884,7 +2884,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
waitForIdle();
verify(mGroupHelper, times(1)).onChannelUpdated(eq(Process.myUserHandle().getIdentifier()),
- eq(mPkg), eq(mTestNotificationChannel), any());
+ eq(mPkg), eq(mTestNotificationChannel), any(), any());
}
@Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index 832ca51ae580..cc68e4e73a4f 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -18,7 +18,6 @@ package com.android.server.notification;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_DEFAULT;
import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
-import static android.app.Flags.FLAG_MODES_UI;
import static android.app.Flags.FLAG_NOTIFICATION_CLASSIFICATION_UI;
import static android.app.Notification.VISIBILITY_PRIVATE;
import static android.app.Notification.VISIBILITY_SECRET;
@@ -158,6 +157,7 @@ import androidx.test.filters.SmallTest;
import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags;
import com.android.internal.config.sysui.TestableFlagResolver;
+import com.android.internal.notification.NotificationChannelGroupsHelper;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.os.AtomsProto;
@@ -258,9 +258,9 @@ public class PreferencesHelperTest extends UiServiceTestCase {
@Parameters(name = "{0}")
public static List<FlagsParameterization> getParams() {
return FlagsParameterization.allCombinationsOf(
- android.app.Flags.FLAG_API_RICH_ONGOING,
- FLAG_NOTIFICATION_CLASSIFICATION, FLAG_NOTIFICATION_CLASSIFICATION_UI,
- FLAG_MODES_UI, android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS);
+ android.app.Flags.FLAG_UI_RICH_ONGOING,
+ FLAG_NOTIFICATION_CLASSIFICATION_UI,
+ android.app.Flags.FLAG_NM_BINDER_PERF_CACHE_CHANNELS);
}
public PreferencesHelperTest(FlagsParameterization flags) {
@@ -661,6 +661,7 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper.setShowBadge(PKG_N_MR1, UID_N_MR1, true);
if (android.app.Flags.uiRichOngoing()) {
+ mHelper.setCanBePromoted(PKG_N_MR1, UID_N_MR1, false, true);
mHelper.setCanBePromoted(PKG_N_MR1, UID_N_MR1, true, true);
}
@@ -690,7 +691,8 @@ public class PreferencesHelperTest extends UiServiceTestCase {
}
List<NotificationChannelGroup> actualGroups = mXmlHelper.getNotificationChannelGroups(
- PKG_N_MR1, UID_N_MR1, false, true, false, true, null).getList();
+ PKG_N_MR1, UID_N_MR1,
+ NotificationChannelGroupsHelper.Params.forAllChannels(false)).getList();
boolean foundNcg = false;
for (NotificationChannelGroup actual : actualGroups) {
if (ncg.getId().equals(actual.getId())) {
@@ -774,7 +776,8 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mXmlHelper.getNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3.getId(), false));
List<NotificationChannelGroup> actualGroups = mXmlHelper.getNotificationChannelGroups(
- PKG_N_MR1, UID_N_MR1, false, true, false, true, null).getList();
+ PKG_N_MR1, UID_N_MR1,
+ NotificationChannelGroupsHelper.Params.forAllChannels(false)).getList();
boolean foundNcg = false;
for (NotificationChannelGroup actual : actualGroups) {
if (ncg.getId().equals(actual.getId())) {
@@ -3426,8 +3429,8 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper.onPackagesChanged(true, USER_SYSTEM, new String[]{PKG_N_MR1}, new int[]{
UID_N_MR1});
- assertEquals(0, mHelper.getNotificationChannelGroups(
- PKG_N_MR1, UID_N_MR1, true, true, false, true, null).getList().size());
+ assertEquals(0, mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1,
+ NotificationChannelGroupsHelper.Params.forAllChannels(true)).getList().size());
}
@Test
@@ -3566,8 +3569,8 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel3, true, false,
UID_N_MR1, false);
- List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups(
- PKG_N_MR1, UID_N_MR1, true, true, false, true, null).getList();
+ List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups(PKG_N_MR1,
+ UID_N_MR1, NotificationChannelGroupsHelper.Params.forAllChannels(true)).getList();
assertEquals(3, actual.size());
for (NotificationChannelGroup group : actual) {
if (group.getId() == null) {
@@ -3601,15 +3604,15 @@ public class PreferencesHelperTest extends UiServiceTestCase {
channel1.setGroup(ncg.getId());
mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false,
UID_N_MR1, false);
- mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1, true, true, false, true, null)
- .getList();
+ mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1,
+ NotificationChannelGroupsHelper.Params.forAllChannels(true)).getList();
channel1.setImportance(IMPORTANCE_LOW);
mHelper.updateNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true,
UID_N_MR1, false);
- List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups(
- PKG_N_MR1, UID_N_MR1, true, true, false, true, null).getList();
+ List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups(PKG_N_MR1,
+ UID_N_MR1, NotificationChannelGroupsHelper.Params.forAllChannels(true)).getList();
assertEquals(2, actual.size());
for (NotificationChannelGroup group : actual) {
@@ -3634,8 +3637,8 @@ public class PreferencesHelperTest extends UiServiceTestCase {
mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false,
UID_N_MR1, false);
- List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups(
- PKG_N_MR1, UID_N_MR1, false, false, true, true, null).getList();
+ List<NotificationChannelGroup> actual = mHelper.getNotificationChannelGroups(PKG_N_MR1,
+ UID_N_MR1, NotificationChannelGroupsHelper.Params.forAllGroups()).getList();
assertEquals(2, actual.size());
for (NotificationChannelGroup group : actual) {
@@ -6440,8 +6443,9 @@ public class PreferencesHelperTest extends UiServiceTestCase {
Set<String> filter = ImmutableSet.of("id3");
- NotificationChannelGroup actual = mHelper.getNotificationChannelGroups(
- PKG_N_MR1, UID_N_MR1, false, true, false, true, filter).getList().get(0);
+ NotificationChannelGroup actual = mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1,
+ NotificationChannelGroupsHelper.Params.onlySpecifiedOrBlockedChannels(
+ filter)).getList().get(0);
assertEquals(2, actual.getChannels().size());
assertEquals(1, actual.getChannels().stream().filter(c -> c.getId().equals("id3")).count());
assertEquals(1, actual.getChannels().stream().filter(c -> c.getId().equals("id2")).count());
@@ -6468,8 +6472,9 @@ public class PreferencesHelperTest extends UiServiceTestCase {
Set<String> filter = ImmutableSet.of("id3");
- NotificationChannelGroup actual = mHelper.getNotificationChannelGroups(
- PKG_N_MR1, UID_N_MR1, false, true, false, false, filter).getList().get(0);
+ NotificationChannelGroup actual = mHelper.getNotificationChannelGroups(PKG_N_MR1, UID_N_MR1,
+ new NotificationChannelGroupsHelper.Params(false, true, false, false,
+ filter)).getList().get(0);
assertEquals(1, actual.getChannels().size());
assertEquals(1, actual.getChannels().stream().filter(c -> c.getId().equals("id3")).count());
assertEquals(0, actual.getChannels().stream().filter(c -> c.getId().equals("id2")).count());
@@ -6638,6 +6643,8 @@ public class PreferencesHelperTest extends UiServiceTestCase {
@Test
@EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING)
public void testSetCanBePromoted_allowlistNotOverrideUser() {
+ // default value is true. So we need to set it false to trigger the change.
+ mHelper.setCanBePromoted(PKG_P, UID_P, false, true);
mHelper.setCanBePromoted(PKG_P, UID_P, true, true);
assertThat(mHelper.canBePromoted(PKG_P, UID_P)).isTrue();
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index 767c02bd268f..b248218b6cef 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -109,7 +109,7 @@ import com.android.internal.util.test.FakeSettingsProviderRule;
import com.android.server.LocalServices;
import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import com.android.server.pm.BackgroundUserSoundNotifier;
-import com.android.server.pm.UserManagerService;
+import com.android.server.pm.UserManagerInternal;
import com.android.server.vibrator.VibrationSession.Status;
import org.junit.After;
@@ -898,7 +898,7 @@ public class VibratorManagerServiceTest {
@Test
public void vibrate_thenFgUserRequestsMute_getsCancelled() throws Throwable {
- assumeTrue(UserManagerService.shouldShowNotificationForBackgroundUserSounds());
+ assumeTrue(UserManagerInternal.shouldShowNotificationForBackgroundUserSounds());
mockVibrators(1);
VibratorManagerService service = createSystemReadyService();
@@ -2760,7 +2760,7 @@ public class VibratorManagerServiceTest {
@Test
public void onExternalVibration_thenFgUserRequestsMute_doNotCancelVibration() throws Throwable {
- assumeTrue(UserManagerService.shouldShowNotificationForBackgroundUserSounds());
+ assumeTrue(UserManagerInternal.shouldShowNotificationForBackgroundUserSounds());
mockVibrators(1);
mVibratorProviders.get(1).setCapabilities(IVibrator.CAP_EXTERNAL_CONTROL);
VibratorManagerService service = createSystemReadyService();
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index 70f57eb40385..3c74ad06a21f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -19,6 +19,7 @@ package com.android.server.wm;
import static android.app.ActivityManager.START_DELIVERED_TO_TOP;
import static android.app.ActivityManager.START_TASK_TO_FRONT;
import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
@@ -44,20 +45,26 @@ import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
+import android.annotation.NonNull;
import android.app.ActivityOptions;
import android.app.WaitResult;
+import android.app.WindowConfiguration;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
+import android.graphics.Rect;
import android.os.Binder;
import android.os.ConditionVariable;
import android.os.IBinder;
import android.os.RemoteException;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.view.Display;
import androidx.test.filters.MediumTest;
+import com.android.window.flags.Flags;
+
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
@@ -424,4 +431,95 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase {
assertThat(activity.mLaunchCookie).isNull();
verify(mAtm).moveTaskToFrontLocked(any(), eq(null), anyInt(), anyInt(), eq(safeOptions));
}
+
+ @Test
+ public void testOpaque_leafTask_occludingActivity_isOpaque() {
+ final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ activity.setOccludesParent(true);
+ final TaskFragment tf = activity.getTaskFragment();
+
+ assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(tf)).isTrue();
+ }
+
+ @Test
+ public void testOpaque_leafTask_nonOccludingActivity_isTranslucent() {
+ final ActivityRecord activity = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ activity.setOccludesParent(false);
+ final TaskFragment tf = activity.getTaskFragment();
+
+ assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(tf)).isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ public void testOpaque_rootTask_translucentFillingChild_isTranslucent() {
+ final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build();
+ createChildTaskFragment(/* parent */ rootTask,
+ WINDOWING_MODE_FREEFORM, /* opaque */ false, /* filling */ true);
+
+ assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ public void testOpaque_rootTask_opaqueAndNotFillingChild_isTranslucent() {
+ final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build();
+ createChildTaskFragment(/* parent */ rootTask,
+ WINDOWING_MODE_FREEFORM, /* opaque */ true, /* filling */ false);
+
+ assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isFalse();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ public void testOpaque_rootTask_opaqueAndFillingChild_isOpaque() {
+ final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build();
+ createChildTaskFragment(/* parent */ rootTask,
+ WINDOWING_MODE_FREEFORM, /* opaque */ true, /* filling */ true);
+
+ assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue();
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ public void testOpaque_rootTask_nonFillingOpaqueAdjacentChildren_isOpaque() {
+ final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build();
+ final TaskFragment tf1 = createChildTaskFragment(/* parent */ rootTask,
+ WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false);
+ final TaskFragment tf2 = createChildTaskFragment(/* parent */ rootTask,
+ WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false);
+ tf1.setAdjacentTaskFragment(tf2);
+
+ assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue();
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS})
+ public void testOpaque_rootTask_nonFillingOpaqueAdjacentChildren_multipleAdjacent_isOpaque() {
+ final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build();
+ final TaskFragment tf1 = createChildTaskFragment(/* parent */ rootTask,
+ WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false);
+ final TaskFragment tf2 = createChildTaskFragment(/* parent */ rootTask,
+ WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false);
+ final TaskFragment tf3 = createChildTaskFragment(/* parent */ rootTask,
+ WINDOWING_MODE_MULTI_WINDOW, /* opaque */ true, /* filling */ false);
+ tf1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf1, tf2, tf3));
+
+ assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue();
+ }
+
+ @NonNull
+ private TaskFragment createChildTaskFragment(@NonNull Task parent,
+ @WindowConfiguration.WindowingMode int windowingMode,
+ boolean opaque,
+ boolean filling) {
+ final ActivityRecord activity = new ActivityBuilder(mAtm)
+ .setCreateTask(true).setParentTask(parent).build();
+ activity.setOccludesParent(opaque);
+ final TaskFragment tf = activity.getTaskFragment();
+ tf.setWindowingMode(windowingMode);
+ tf.setBounds(filling ? new Rect() : new Rect(100, 100, 200, 200));
+ return tf;
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java
deleted file mode 100644
index 169968c75fc5..000000000000
--- a/services/tests/wmtests/src/com/android/server/wm/AppChangeTransitionTests.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.wm;
-
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-import android.os.IBinder;
-import android.platform.test.annotations.Presubmit;
-import android.view.Display;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
-import android.view.RemoteAnimationAdapter;
-import android.view.RemoteAnimationDefinition;
-import android.view.RemoteAnimationTarget;
-import android.view.WindowManager;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Tests for change transitions
- *
- * Build/Install/Run:
- * atest WmTests:AppChangeTransitionTests
- */
-@SmallTest
-@Presubmit
-@RunWith(WindowTestRunner.class)
-public class AppChangeTransitionTests extends WindowTestsBase {
-
- private Task mTask;
- private ActivityRecord mActivity;
-
- public void setUpOnDisplay(DisplayContent dc) {
- mActivity = createActivityRecord(dc, WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_STANDARD);
- mTask = mActivity.getTask();
-
- // Set a remote animator with snapshot disabled. Snapshots don't work in wmtests.
- RemoteAnimationDefinition definition = new RemoteAnimationDefinition();
- RemoteAnimationAdapter adapter =
- new RemoteAnimationAdapter(new TestRemoteAnimationRunner(), 10, 1, false);
- definition.addRemoteAnimation(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE, adapter);
- dc.registerRemoteAnimations(definition);
- }
-
- class TestRemoteAnimationRunner implements IRemoteAnimationRunner {
- @Override
- public void onAnimationStart(@WindowManager.TransitionOldType int transit,
- RemoteAnimationTarget[] apps,
- RemoteAnimationTarget[] wallpapers,
- RemoteAnimationTarget[] nonApps,
- IRemoteAnimationFinishedCallback finishedCallback) {
- for (RemoteAnimationTarget target : apps) {
- assertNotNull(target.startBounds);
- }
- try {
- finishedCallback.onAnimationFinished();
- } catch (Exception e) {
- throw new RuntimeException("Something went wrong");
- }
- }
-
- @Override
- public void onAnimationCancelled() {
- }
-
- @Override
- public IBinder asBinder() {
- return null;
- }
- }
-
- @Test
- public void testModeChangeRemoteAnimatorNoSnapshot() {
- // setup currently defaults to no snapshot.
- setUpOnDisplay(mDisplayContent);
-
- mTask.setWindowingMode(WINDOWING_MODE_FREEFORM);
- assertEquals(1, mDisplayContent.mChangingContainers.size());
-
- // Verify we are in a change transition, but without a snapshot.
- // Though, the test will actually have crashed by now if a snapshot is attempted.
- assertNull(mTask.mSurfaceFreezer.mSnapshot);
- assertTrue(mTask.isInChangeTransition());
-
- waitUntilHandlersIdle();
- mActivity.removeImmediately();
- }
-
- @Test
- public void testCancelPendingChangeOnRemove() {
- // setup currently defaults to no snapshot.
- setUpOnDisplay(mDisplayContent);
-
- mTask.setWindowingMode(WINDOWING_MODE_FREEFORM);
- assertEquals(1, mDisplayContent.mChangingContainers.size());
- assertTrue(mTask.isInChangeTransition());
-
- // Removing the app-token from the display should clean-up the
- // the change leash.
- mDisplayContent.removeAppToken(mActivity.token);
- assertEquals(0, mDisplayContent.mChangingContainers.size());
- assertFalse(mTask.isInChangeTransition());
-
- waitUntilHandlersIdle();
- mActivity.removeImmediately();
- }
-
- @Test
- public void testNoChangeOnOldDisplayWhenMoveDisplay() {
- mDisplayContent.getDefaultTaskDisplayArea().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
- final DisplayContent dc1 = createNewDisplay(Display.STATE_ON);
- dc1.getDefaultTaskDisplayArea().setWindowingMode(WINDOWING_MODE_FREEFORM);
- setUpOnDisplay(dc1);
-
- assertEquals(WINDOWING_MODE_FREEFORM, mTask.getWindowingMode());
-
- // Reparenting to a display with different windowing mode may trigger
- // a change transition internally, but it should be cleaned-up once
- // the display change is complete.
- mTask.reparent(mDisplayContent.getDefaultTaskDisplayArea(), true);
-
- assertEquals(WINDOWING_MODE_FULLSCREEN, mTask.getWindowingMode());
-
- // Make sure the change transition is not the old display
- assertFalse(dc1.mChangingContainers.contains(mTask));
-
- waitUntilHandlersIdle();
- mActivity.removeImmediately();
- }
-
- @Test
- public void testCancelPendingChangeOnHide() {
- // setup currently defaults to no snapshot.
- setUpOnDisplay(mDisplayContent);
-
- mTask.setWindowingMode(WINDOWING_MODE_FREEFORM);
- assertEquals(1, mDisplayContent.mChangingContainers.size());
- assertTrue(mTask.isInChangeTransition());
-
- // Changing visibility should cancel the change transition and become closing
- mActivity.setVisibility(false);
- assertEquals(0, mDisplayContent.mChangingContainers.size());
- assertFalse(mTask.isInChangeTransition());
-
- waitUntilHandlersIdle();
- mActivity.removeImmediately();
- }
-}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index 03d904283e83..8553fbd30ab8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -409,7 +409,6 @@ public class AppTransitionTests extends WindowTestsBase {
task.getBounds(taskBounds);
taskFragment.setBounds(0, 0, taskBounds.right / 2, taskBounds.bottom);
spyOn(taskFragment);
- mockSurfaceFreezerSnapshot(taskFragment.mSurfaceFreezer);
assertTrue(mDc.mChangingContainers.isEmpty());
assertFalse(mDc.mAppTransition.isTransitionSet());
@@ -422,7 +421,6 @@ public class AppTransitionTests extends WindowTestsBase {
verify(taskFragment).initializeChangeTransition(activity.getBounds(), activityLeash);
assertTrue(mDc.mChangingContainers.contains(taskFragment));
assertTrue(mDc.mAppTransition.containsTransitRequest(TRANSIT_CHANGE));
- assertEquals(startBounds, taskFragment.mSurfaceFreezer.mFreezeBounds);
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
index 716f86418bcb..560725241853 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -158,7 +158,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
public void testIsCameraRunningAndWindowingModeEligible_notFreeformWindowing_returnsFalse() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
}
@@ -169,7 +169,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
public void testIsCameraRunningAndWindowingModeEligible_optInFreeformCameraRunning_true() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertTrue(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
}
@@ -179,7 +179,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
public void testIsFreeformLetterboxingForCameraAllowed_overrideDisabled_returnsFalse() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
}
@@ -199,7 +199,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
public void testIsFreeformLetterboxingForCameraAllowed_notFreeformWindowing_returnsFalse() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
}
@@ -210,7 +210,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
public void testIsFreeformLetterboxingForCameraAllowed_optInFreeformCameraRunning_true() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertTrue(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
}
@@ -222,7 +222,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
doReturn(false).when(mActivity).inFreeformWindowingMode();
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertNotInCameraCompatMode();
}
@@ -250,7 +250,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
setDisplayRotation(ROTATION_0);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT);
assertActivityRefreshRequested(/* refreshRequested */ false);
@@ -262,7 +263,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() throws Exception {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
setDisplayRotation(ROTATION_270);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
assertActivityRefreshRequested(/* refreshRequested */ false);
@@ -274,7 +276,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() throws Exception {
configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
setDisplayRotation(ROTATION_0);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT);
assertActivityRefreshRequested(/* refreshRequested */ false);
@@ -286,7 +289,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() throws Exception {
configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
setDisplayRotation(ROTATION_270);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE);
assertActivityRefreshRequested(/* refreshRequested */ false);
@@ -299,12 +303,12 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
setDisplayRotation(ROTATION_270);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true,
/* lastLetterbox= */ false);
assertActivityRefreshRequested(/* refreshRequested */ true);
- mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraClosed(CAMERA_ID_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
// Activity is letterboxed from the previous configuration change.
callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true,
/* lastLetterbox= */ true);
@@ -319,7 +323,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
public void testCameraOpenedForDifferentPackage_notInCameraCompatMode() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2);
assertNotInCameraCompatMode();
}
@@ -329,7 +333,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
public void testShouldApplyCameraCompatFreeformTreatment_overrideNotEnabled_returnsFalse() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertFalse(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity,
/* checkOrientation */ true));
@@ -341,7 +345,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
public void testShouldApplyCameraCompatFreeformTreatment_enabledByOverride_returnsTrue() {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertTrue(mActivity.info
.isChangeEnabled(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT));
@@ -356,7 +360,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
Configuration oldConfiguration = createConfiguration(/* letterbox= */ false);
Configuration newConfiguration = createConfiguration(/* letterbox= */ true);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
oldConfiguration));
@@ -372,7 +376,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
oldConfiguration.windowConfiguration.setDisplayRotation(0);
newConfiguration.windowConfiguration.setDisplayRotation(90);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
oldConfiguration));
@@ -388,7 +392,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
oldConfiguration.windowConfiguration.setDisplayRotation(0);
newConfiguration.windowConfiguration.setDisplayRotation(0);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
assertFalse(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
oldConfiguration));
@@ -404,7 +408,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
doReturn(false).when(mActivity.mAppCompatController.getCameraOverrides())
.shouldRefreshActivityForCameraCompat();
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
callOnActivityConfigurationChanging(mActivity);
assertActivityRefreshRequested(/* refreshRequested */ false);
@@ -419,7 +423,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
callOnActivityConfigurationChanging(mActivity);
assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
@@ -434,7 +438,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides())
.shouldRefreshActivityViaPauseForCameraCompat();
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
callOnActivityConfigurationChanging(mActivity);
assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
@@ -446,7 +450,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
public void testGetCameraCompatAspectRatio_activityNotInCameraCompat_returnsDefaultAspRatio() {
configureActivity(SCREEN_ORIENTATION_FULL_USER);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
callOnActivityConfigurationChanging(mActivity);
assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO,
@@ -462,7 +466,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
final float configAspectRatio = 1.5f;
mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
callOnActivityConfigurationChanging(mActivity);
assertEquals(configAspectRatio,
@@ -480,7 +484,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides())
.isOverrideMinAspectRatioForCameraEnabled();
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
callOnActivityConfigurationChanging(mActivity);
assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO,
@@ -496,7 +500,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
configureActivity(SCREEN_ORIENTATION_PORTRAIT);
setDisplayRotation(ROTATION_270);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
// This is a portrait rotation for a device with portrait natural orientation (most common,
// currently the only one supported).
@@ -511,7 +515,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
setDisplayRotation(ROTATION_0);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
// This is a landscape rotation for a device with portrait natural orientation (most common,
// currently the only one supported).
@@ -616,6 +620,16 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
.inFreeformWindowingMode();
}
+ private void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) {
+ mCameraAvailabilityCallback.onCameraOpened(cameraId, packageName);
+ waitHandlerIdle(mDisplayContent.mWmService.mH);
+ }
+
+ private void onCameraClosed(@NonNull String cameraId) {
+ mCameraAvailabilityCallback.onCameraClosed(cameraId);
+ waitHandlerIdle(mDisplayContent.mWmService.mH);
+ }
+
private void assertInCameraCompatMode(@CameraCompatTaskInfo.FreeformCameraCompatMode int mode) {
assertEquals(mode, mCameraCompatFreeformPolicy.getCameraCompatMode(mActivity));
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
index cdb51fc1c645..fdde3b38f19f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -1345,7 +1345,7 @@ public class DesktopModeLaunchParamsModifierTests extends
private void setupDesktopModeLaunchParamsModifier(boolean isDesktopModeSupported,
boolean enforceDeviceRestrictions) {
doReturn(isDesktopModeSupported)
- .when(() -> DesktopModeHelper.isDesktopModeSupported(any()));
+ .when(() -> DesktopModeHelper.isDeviceEligibleForDesktopMode(any()));
doReturn(enforceDeviceRestrictions)
.when(DesktopModeHelper::shouldEnforceDeviceRestrictions);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 0d9772492e59..ee8d7308f6b3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -152,7 +152,6 @@ public class TaskFragmentTest extends WindowTestsBase {
ACTIVITY_TYPE_STANDARD);
task.setBoundsUnchecked(new Rect(0, 0, 1000, 1000));
mTaskFragment = createTaskFragmentWithEmbeddedActivity(task, mOrganizer);
- mockSurfaceFreezerSnapshot(mTaskFragment.mSurfaceFreezer);
final Rect startBounds = new Rect(0, 0, 500, 1000);
final Rect endBounds = new Rect(500, 0, 1000, 1000);
mTaskFragment.setRelativeEmbeddedBounds(startBounds);
@@ -179,44 +178,6 @@ public class TaskFragmentTest extends WindowTestsBase {
}
@Test
- public void testStartChangeTransition_resetSurface() {
- final Task task = createTask(mDisplayContent, WINDOWING_MODE_MULTI_WINDOW,
- ACTIVITY_TYPE_STANDARD);
- task.setBoundsUnchecked(new Rect(0, 0, 1000, 1000));
- mTaskFragment = createTaskFragmentWithEmbeddedActivity(task, mOrganizer);
- doReturn(mTransaction).when(mTaskFragment).getSyncTransaction();
- doReturn(mTransaction).when(mTaskFragment).getPendingTransaction();
- mLeash = mTaskFragment.getSurfaceControl();
- mockSurfaceFreezerSnapshot(mTaskFragment.mSurfaceFreezer);
- final Rect startBounds = new Rect(0, 0, 1000, 1000);
- final Rect endBounds = new Rect(500, 500, 1000, 1000);
- mTaskFragment.setRelativeEmbeddedBounds(startBounds);
- mTaskFragment.recomputeConfiguration();
- doReturn(true).when(mTaskFragment).isVisible();
- doReturn(true).when(mTaskFragment).isVisibleRequested();
-
- clearInvocations(mTransaction);
- final Rect relStartBounds = new Rect(mTaskFragment.getRelativeEmbeddedBounds());
- mTaskFragment.deferOrganizedTaskFragmentSurfaceUpdate();
- mTaskFragment.setRelativeEmbeddedBounds(endBounds);
- mTaskFragment.recomputeConfiguration();
- assertTrue(mTaskFragment.shouldStartChangeTransition(startBounds, relStartBounds));
- mTaskFragment.initializeChangeTransition(startBounds);
- mTaskFragment.continueOrganizedTaskFragmentSurfaceUpdate();
-
- // Surface reset when prepare transition.
- verify(mTransaction).setPosition(mLeash, 0, 0);
- verify(mTransaction).setWindowCrop(mLeash, 0, 0);
-
- clearInvocations(mTransaction);
- mTaskFragment.mSurfaceFreezer.unfreeze(mTransaction);
-
- // Update surface after animation.
- verify(mTransaction).setPosition(mLeash, 500, 500);
- verify(mTransaction).setWindowCrop(mLeash, 500, 500);
- }
-
- @Test
public void testStartChangeTransition_doNotFreezeWhenOnlyMoved() {
final Rect startBounds = new Rect(0, 0, 1000, 1000);
final Rect endBounds = new Rect(startBounds);
@@ -235,7 +196,6 @@ public class TaskFragmentTest extends WindowTestsBase {
@Test
public void testNotOkToAnimate_doNotStartChangeTransition() {
- mockSurfaceFreezerSnapshot(mTaskFragment.mSurfaceFreezer);
final Rect startBounds = new Rect(0, 0, 1000, 1000);
final Rect endBounds = new Rect(500, 500, 1000, 1000);
mTaskFragment.setRelativeEmbeddedBounds(startBounds);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index cc447a18758c..001446550304 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -32,7 +32,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE;
-import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
@@ -64,7 +63,6 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -1014,30 +1012,6 @@ public class WindowContainerTests extends WindowTestsBase {
}
@Test
- public void testOnDisplayChanged_cleanupChanging() {
- final Task task = createTask(mDisplayContent);
- addLocalInsets(task);
- spyOn(task.mSurfaceFreezer);
- mDisplayContent.mChangingContainers.add(task);
-
- // Don't remove the changing transition of this window when it is still the old display.
- // This happens on display info changed.
- task.onDisplayChanged(mDisplayContent);
-
- assertTrue(task.mLocalInsetsSources.size() == 1);
- assertTrue(mDisplayContent.mChangingContainers.contains(task));
- verify(task.mSurfaceFreezer, never()).unfreeze(any());
-
- // Remove the changing transition of this window when it is moved or reparented from the old
- // display.
- final DisplayContent newDc = createNewDisplay();
- task.onDisplayChanged(newDc);
-
- assertFalse(mDisplayContent.mChangingContainers.contains(task));
- verify(task.mSurfaceFreezer).unfreeze(any());
- }
-
- @Test
public void testHandleCompleteDeferredRemoval() {
final DisplayContent displayContent = createNewDisplay();
// Do not reparent activity to default display when removing the display.
@@ -1290,157 +1264,17 @@ public class WindowContainerTests extends WindowTestsBase {
final WindowContainer container = new WindowContainer(mWm);
container.mSurfaceControl = mock(SurfaceControl.class);
final SurfaceAnimator surfaceAnimator = container.mSurfaceAnimator;
- final SurfaceFreezer surfaceFreezer = container.mSurfaceFreezer;
final SurfaceControl relativeParent = mock(SurfaceControl.class);
final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
spyOn(container);
spyOn(surfaceAnimator);
- spyOn(surfaceFreezer);
doReturn(t).when(container).getSyncTransaction();
container.setLayer(t, 1);
container.setRelativeLayer(t, relativeParent, 2);
- // Set through surfaceAnimator if surfaceFreezer doesn't have leash.
verify(surfaceAnimator).setLayer(t, 1);
verify(surfaceAnimator).setRelativeLayer(t, relativeParent, 2);
- verify(surfaceFreezer, never()).setLayer(any(), anyInt());
- verify(surfaceFreezer, never()).setRelativeLayer(any(), any(), anyInt());
-
- clearInvocations(surfaceAnimator);
- clearInvocations(surfaceFreezer);
- doReturn(true).when(surfaceFreezer).hasLeash();
-
- container.setLayer(t, 1);
- container.setRelativeLayer(t, relativeParent, 2);
-
- // Set through surfaceFreezer if surfaceFreezer has leash.
- verify(surfaceFreezer).setLayer(t, 1);
- verify(surfaceFreezer).setRelativeLayer(t, relativeParent, 2);
- verify(surfaceAnimator, never()).setLayer(any(), anyInt());
- verify(surfaceAnimator, never()).setRelativeLayer(any(), any(), anyInt());
- }
-
- @Test
- public void testStartChangeTransitionWhenPreviousIsNotFinished() {
- final WindowContainer container = createTaskFragmentWithActivity(
- createTask(mDisplayContent));
- container.mSurfaceControl = mock(SurfaceControl.class);
- final SurfaceAnimator surfaceAnimator = container.mSurfaceAnimator;
- final SurfaceFreezer surfaceFreezer = container.mSurfaceFreezer;
- final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
- spyOn(container);
- spyOn(surfaceAnimator);
- mockSurfaceFreezerSnapshot(surfaceFreezer);
- doReturn(t).when(container).getPendingTransaction();
- doReturn(t).when(container).getSyncTransaction();
-
- // Leash and snapshot created for change transition.
- container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
-
- assertNotNull(surfaceFreezer.mLeash);
- assertNotNull(surfaceFreezer.mSnapshot);
- assertEquals(surfaceFreezer.mLeash, container.getAnimationLeash());
-
- // Start animation: surfaceAnimator take over the leash and snapshot from surfaceFreezer.
- container.applyAnimationUnchecked(null /* lp */, true /* enter */,
- TRANSIT_OLD_TASK_FRAGMENT_CHANGE, false /* isVoiceInteraction */,
- null /* sources */);
-
- assertNull(surfaceFreezer.mLeash);
- assertNull(surfaceFreezer.mSnapshot);
- assertNotNull(surfaceAnimator.mLeash);
- assertNotNull(surfaceAnimator.mSnapshot);
- final SurfaceControl prevLeash = surfaceAnimator.mLeash;
- final SurfaceFreezer.Snapshot prevSnapshot = surfaceAnimator.mSnapshot;
-
- // Prepare another change transition.
- container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
-
- assertNotNull(surfaceFreezer.mLeash);
- assertNotNull(surfaceFreezer.mSnapshot);
- assertEquals(surfaceFreezer.mLeash, container.getAnimationLeash());
- assertNotEquals(prevLeash, container.getAnimationLeash());
-
- // Start another animation before the previous one is finished, it should reset the previous
- // one, but not change the current one.
- container.applyAnimationUnchecked(null /* lp */, true /* enter */,
- TRANSIT_OLD_TASK_FRAGMENT_CHANGE, false /* isVoiceInteraction */,
- null /* sources */);
-
- verify(container, never()).onAnimationLeashLost(any());
- verify(surfaceFreezer, never()).unfreeze(any());
- assertNotNull(surfaceAnimator.mLeash);
- assertNotNull(surfaceAnimator.mSnapshot);
- assertEquals(surfaceAnimator.mLeash, container.getAnimationLeash());
- assertNotEquals(prevLeash, surfaceAnimator.mLeash);
- assertNotEquals(prevSnapshot, surfaceAnimator.mSnapshot);
-
- // Clean up after animation finished.
- surfaceAnimator.mInnerAnimationFinishedCallback.onAnimationFinished(
- ANIMATION_TYPE_APP_TRANSITION, surfaceAnimator.getAnimation());
-
- verify(container).onAnimationLeashLost(any());
- assertNull(surfaceAnimator.mLeash);
- assertNull(surfaceAnimator.mSnapshot);
- }
-
- @Test
- public void testUnfreezeWindow_removeWindowFromChanging() {
- final WindowContainer container = createTaskFragmentWithActivity(
- createTask(mDisplayContent));
- mockSurfaceFreezerSnapshot(container.mSurfaceFreezer);
- final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
-
- container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
-
- assertTrue(mDisplayContent.mChangingContainers.contains(container));
-
- container.mSurfaceFreezer.unfreeze(t);
-
- assertFalse(mDisplayContent.mChangingContainers.contains(container));
- }
-
- @Test
- public void testFailToTaskSnapshot_unfreezeWindow() {
- final WindowContainer container = createTaskFragmentWithActivity(
- createTask(mDisplayContent));
- final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
- spyOn(container.mSurfaceFreezer);
-
- container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
-
- verify(container.mSurfaceFreezer).freeze(any(), any(), any(), any());
- verify(container.mSurfaceFreezer).unfreeze(any());
- assertTrue(mDisplayContent.mChangingContainers.isEmpty());
- }
-
- @Test
- public void testRemoveUnstartedFreezeSurfaceWhenFreezeAgain() {
- final WindowContainer container = createTaskFragmentWithActivity(
- createTask(mDisplayContent));
- container.mSurfaceControl = mock(SurfaceControl.class);
- final SurfaceFreezer surfaceFreezer = container.mSurfaceFreezer;
- mockSurfaceFreezerSnapshot(surfaceFreezer);
- final SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
- spyOn(container);
- doReturn(t).when(container).getPendingTransaction();
- doReturn(t).when(container).getSyncTransaction();
-
- // Leash and snapshot created for change transition.
- container.initializeChangeTransition(new Rect(0, 0, 1000, 2000));
-
- assertNotNull(surfaceFreezer.mLeash);
- assertNotNull(surfaceFreezer.mSnapshot);
-
- final SurfaceControl prevLeash = surfaceFreezer.mLeash;
- final SurfaceFreezer.Snapshot prevSnapshot = surfaceFreezer.mSnapshot;
- spyOn(prevSnapshot);
-
- container.initializeChangeTransition(new Rect(0, 0, 1500, 2500));
-
- verify(t).remove(prevLeash);
- verify(prevSnapshot).destroy(t);
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 2c390c504e9f..b16f5283d532 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -78,7 +78,6 @@ import android.content.pm.ApplicationInfo;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.graphics.Rect;
-import android.hardware.HardwareBuffer;
import android.hardware.display.DisplayManager;
import android.os.Binder;
import android.os.Build;
@@ -113,7 +112,6 @@ import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
import android.window.ITaskFragmentOrganizer;
import android.window.ITransitionPlayer;
-import android.window.ScreenCapture;
import android.window.StartingWindowInfo;
import android.window.StartingWindowRemovalInfo;
import android.window.TaskFragmentOrganizer;
@@ -1112,21 +1110,6 @@ public class WindowTestsBase extends SystemServiceTestsBase {
displayContent -> displayContent.mMinSizeOfResizeableTaskDp = 1);
}
- /** Mocks the behavior of taking a snapshot. */
- void mockSurfaceFreezerSnapshot(SurfaceFreezer surfaceFreezer) {
- final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer =
- mock(ScreenCapture.ScreenshotHardwareBuffer.class);
- final HardwareBuffer hardwareBuffer = mock(HardwareBuffer.class);
- spyOn(surfaceFreezer);
- doReturn(screenshotBuffer).when(surfaceFreezer)
- .createSnapshotBufferInner(any(), any());
- doReturn(null).when(surfaceFreezer)
- .createFromHardwareBufferInner(any());
- doReturn(hardwareBuffer).when(screenshotBuffer).getHardwareBuffer();
- doReturn(100).when(hardwareBuffer).getWidth();
- doReturn(100).when(hardwareBuffer).getHeight();
- }
-
static ComponentName getUniqueComponentName() {
return getUniqueComponentName(DEFAULT_COMPONENT_PACKAGE_NAME);
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 7e7a53148603..cd1c48554be9 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -2747,8 +2747,7 @@ public class VoiceInteractionManagerService extends SystemService {
isManagedProfileVisible = true;
}
}
- final ScreenCapture.ScreenshotHardwareBuffer shb =
- mWmInternal.takeAssistScreenshot(/* windowTypesToExclude= */ Set.of());
+ final ScreenCapture.ScreenshotHardwareBuffer shb = mWmInternal.takeAssistScreenshot();
final Bitmap bm = shb != null ? shb.asBitmap() : null;
// Now that everything is fetched, putting it in the launchIntent.
if (bm != null) {
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
index 9e9d014c622d..55d6fd9b4a73 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt
@@ -87,14 +87,18 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) :
wmHelper: WindowManagerStateHelper,
device: UiDevice,
motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH),
+ shouldUseDragToDesktop: Boolean = false,
) {
innerHelper.launchViaIntent(wmHelper)
- if (!isInDesktopWindowingMode(wmHelper)) {
+ if (isInDesktopWindowingMode(wmHelper)) return
+ if (shouldUseDragToDesktop) {
enterDesktopModeWithDrag(
wmHelper = wmHelper,
device = device,
motionEventHelper = motionEventHelper
)
+ } else {
+ enterDesktopModeFromAppHandleMenu(wmHelper, device)
}
}
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 36db955c3085..37bdf6b8614d 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -1111,9 +1111,9 @@ class KeyGestureControllerTests {
intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
),
TestData(
- "FULLSCREEN -> Maximizes a task to fit the screen",
+ "FULLSCREEN -> Turns a task into fullscreen",
intArrayOf(KeyEvent.KEYCODE_FULLSCREEN),
- KeyGestureEvent.KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW,
+ KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION,
intArrayOf(KeyEvent.KEYCODE_FULLSCREEN),
0,
intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java
index 61fa7b542bc0..83d22d923c78 100644
--- a/tests/utils/testutils/java/android/os/test/TestLooper.java
+++ b/tests/utils/testutils/java/android/os/test/TestLooper.java
@@ -18,24 +18,18 @@ package android.os.test;
import static org.junit.Assert.assertTrue;
-import android.os.Build;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.os.SystemClock;
-import android.os.TestLooperManager;
import android.util.Log;
-import androidx.test.InstrumentationRegistry;
-
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
-import java.util.ArrayDeque;
-import java.util.Queue;
import java.util.concurrent.Executor;
/**
@@ -50,9 +44,7 @@ import java.util.concurrent.Executor;
* The Robolectric class also allows advancing time.
*/
public class TestLooper {
- private final Looper mLooper;
- private final TestLooperManager mTestLooperManager;
- private final Clock mClock;
+ protected final Looper mLooper;
private static final Constructor<Looper> LOOPER_CONSTRUCTOR;
private static final Field THREAD_LOCAL_LOOPER_FIELD;
@@ -62,14 +54,9 @@ public class TestLooper {
private static final Method MESSAGE_MARK_IN_USE_METHOD;
private static final String TAG = "TestLooper";
- private AutoDispatchThread mAutoDispatchThread;
+ private final Clock mClock;
- /**
- * Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection.
- */
- private static boolean isAtLeastBaklava() {
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
- }
+ private AutoDispatchThread mAutoDispatchThread;
static {
try {
@@ -77,22 +64,14 @@ public class TestLooper {
LOOPER_CONSTRUCTOR.setAccessible(true);
THREAD_LOCAL_LOOPER_FIELD = Looper.class.getDeclaredField("sThreadLocal");
THREAD_LOCAL_LOOPER_FIELD.setAccessible(true);
-
- if (isAtLeastBaklava()) {
- MESSAGE_QUEUE_MESSAGES_FIELD = null;
- MESSAGE_NEXT_FIELD = null;
- MESSAGE_WHEN_FIELD = null;
- MESSAGE_MARK_IN_USE_METHOD = null;
- } else {
- MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
- MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
- MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
- MESSAGE_NEXT_FIELD.setAccessible(true);
- MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
- MESSAGE_WHEN_FIELD.setAccessible(true);
- MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
- MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
- }
+ MESSAGE_QUEUE_MESSAGES_FIELD = MessageQueue.class.getDeclaredField("mMessages");
+ MESSAGE_QUEUE_MESSAGES_FIELD.setAccessible(true);
+ MESSAGE_NEXT_FIELD = Message.class.getDeclaredField("next");
+ MESSAGE_NEXT_FIELD.setAccessible(true);
+ MESSAGE_WHEN_FIELD = Message.class.getDeclaredField("when");
+ MESSAGE_WHEN_FIELD.setAccessible(true);
+ MESSAGE_MARK_IN_USE_METHOD = Message.class.getDeclaredMethod("markInUse");
+ MESSAGE_MARK_IN_USE_METHOD.setAccessible(true);
} catch (NoSuchFieldException | NoSuchMethodException e) {
throw new RuntimeException("Failed to initialize TestLooper", e);
}
@@ -127,13 +106,6 @@ public class TestLooper {
throw new RuntimeException("Reflection error constructing or accessing looper", e);
}
- if (isAtLeastBaklava()) {
- mTestLooperManager =
- InstrumentationRegistry.getInstrumentation().acquireLooperManager(mLooper);
- } else {
- mTestLooperManager = null;
- }
-
mClock = clock;
}
@@ -145,72 +117,19 @@ public class TestLooper {
return new HandlerExecutor(new Handler(getLooper()));
}
- private Message getMessageLinkedListLegacy() {
+ private Message getMessageLinkedList() {
try {
MessageQueue queue = mLooper.getQueue();
return (Message) MESSAGE_QUEUE_MESSAGES_FIELD.get(queue);
} catch (IllegalAccessException e) {
throw new RuntimeException("Access failed in TestLooper: get - MessageQueue.mMessages",
- e);
+ e);
}
}
public void moveTimeForward(long milliSeconds) {
- if (isAtLeastBaklava()) {
- moveTimeForwardBaklava(milliSeconds);
- } else {
- moveTimeForwardLegacy(milliSeconds);
- }
- }
-
- private void moveTimeForwardBaklava(long milliSeconds) {
- // Drain all Messages from the queue.
- Queue<Message> messages = new ArrayDeque<>();
- while (true) {
- Message message = mTestLooperManager.poll();
- if (message == null) {
- break;
- }
-
- // Adjust the Message's delivery time.
- long newWhen = message.when - milliSeconds;
- if (newWhen < 0) {
- newWhen = 0;
- }
- message.when = newWhen;
- messages.add(message);
- }
-
- // Repost all Messages back to the queuewith a new time.
- while (true) {
- Message message = messages.poll();
- if (message == null) {
- break;
- }
-
- Runnable callback = message.getCallback();
- Handler handler = message.getTarget();
- long when = message.getWhen();
-
- // The Message cannot be re-enqueued because it is marked in use.
- // Make a copy of the Message and recycle the original.
- // This resets {@link Message#isInUse()} but retains all other content.
- {
- Message newMessage = Message.obtain();
- newMessage.copyFrom(message);
- newMessage.setCallback(callback);
- mTestLooperManager.recycle(message);
- message = newMessage;
- }
-
- // Send the Message back to its Handler to be re-enqueued.
- handler.sendMessageAtTime(message, when);
- }
- }
-
- private void moveTimeForwardLegacy(long milliSeconds) {
try {
- Message msg = getMessageLinkedListLegacy();
+ Message msg = getMessageLinkedList();
while (msg != null) {
long updatedWhen = msg.getWhen() - milliSeconds;
if (updatedWhen < 0) {
@@ -228,12 +147,12 @@ public class TestLooper {
return mClock.uptimeMillis();
}
- private Message messageQueueNextLegacy() {
+ private Message messageQueueNext() {
try {
long now = currentTime();
Message prevMsg = null;
- Message msg = getMessageLinkedListLegacy();
+ Message msg = getMessageLinkedList();
if (msg != null && msg.getTarget() == null) {
// Stalled by a barrier. Find the next asynchronous message in
// the queue.
@@ -266,46 +185,18 @@ public class TestLooper {
/**
* @return true if there are pending messages in the message queue
*/
- public boolean isIdle() {
- if (isAtLeastBaklava()) {
- return isIdleBaklava();
- } else {
- return isIdleLegacy();
- }
- }
+ public synchronized boolean isIdle() {
+ Message messageList = getMessageLinkedList();
- private boolean isIdleBaklava() {
- Long when = mTestLooperManager.peekWhen();
- return when != null && currentTime() >= when;
- }
-
- private synchronized boolean isIdleLegacy() {
- Message messageList = getMessageLinkedListLegacy();
return messageList != null && currentTime() >= messageList.getWhen();
}
/**
* @return the next message in the Looper's message queue or null if there is none
*/
- public Message nextMessage() {
- if (isAtLeastBaklava()) {
- return nextMessageBaklava();
- } else {
- return nextMessageLegacy();
- }
- }
-
- private Message nextMessageBaklava() {
+ public synchronized Message nextMessage() {
if (isIdle()) {
- return mTestLooperManager.poll();
- } else {
- return null;
- }
- }
-
- private synchronized Message nextMessageLegacy() {
- if (isIdle()) {
- return messageQueueNextLegacy();
+ return messageQueueNext();
} else {
return null;
}
@@ -315,26 +206,9 @@ public class TestLooper {
* Dispatch the next message in the queue
* Asserts that there is a message in the queue
*/
- public void dispatchNext() {
- if (isAtLeastBaklava()) {
- dispatchNextBaklava();
- } else {
- dispatchNextLegacy();
- }
- }
-
- private void dispatchNextBaklava() {
- assertTrue(isIdle());
- Message msg = mTestLooperManager.poll();
- if (msg == null) {
- return;
- }
- msg.getTarget().dispatchMessage(msg);
- }
-
- private synchronized void dispatchNextLegacy() {
+ public synchronized void dispatchNext() {
assertTrue(isIdle());
- Message msg = messageQueueNextLegacy();
+ Message msg = messageQueueNext();
if (msg == null) {
return;
}
diff --git a/tools/localedata/extract_icu_data.py b/tools/localedata/extract_icu_data.py
index ec531275af1c..899cd7f9ce5e 100755
--- a/tools/localedata/extract_icu_data.py
+++ b/tools/localedata/extract_icu_data.py
@@ -180,7 +180,14 @@ def pack_script_to_uint32(script):
def dump_representative_locales(representative_locales):
"""Dump the set of representative locales."""
- print()
+ print('''
+/*
+ * TODO: Consider turning the below switch statement into binary search
+ * to save the disk space when the table is larger in the future.
+ * Disassembled code shows that the jump table emitted by clang can be
+ * 4x larger than the data in disk size, but it depends on the optimization option.
+ * However, a switch statement will benefit from the future of compiler improvement.
+ */''')
print('bool isLocaleRepresentative(uint32_t language_and_region, const char* script) {')
print(' const uint64_t packed_locale =')
print(' ((static_cast<uint64_t>(language_and_region)) << 32u) |')