summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--AconfigFlags.bp1
-rw-r--r--cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java2
-rw-r--r--cmds/svc/src/com/android/commands/svc/UsbCommand.java5
-rw-r--r--core/api/test-current.txt2
-rw-r--r--core/java/android/app/ActivityManager.java13
-rw-r--r--core/java/android/app/ActivityManagerInternal.java8
-rw-r--r--core/java/android/app/AppCompatTaskInfo.java23
-rw-r--r--core/java/android/app/IActivityManager.aidl4
-rw-r--r--core/java/android/app/TaskInfo.java6
-rw-r--r--core/java/android/app/backup/BackupManagerMonitor.java3
-rw-r--r--core/java/android/app/notification.aconfig10
-rw-r--r--core/java/android/content/Context.java11
-rw-r--r--core/java/android/content/pm/RegisteredServicesCache.java33
-rw-r--r--core/java/android/hardware/camera2/CameraManager.java8
-rw-r--r--core/java/android/hardware/input/IKeyGestureHandler.aidl11
-rw-r--r--core/java/android/hardware/input/InputManager.java7
-rw-r--r--core/java/android/hardware/input/InputManagerGlobal.java17
-rw-r--r--core/java/android/hardware/input/KeyGestureEvent.java6
-rw-r--r--core/java/android/os/vibrator/flags.aconfig11
-rw-r--r--core/java/android/service/chooser/flags.aconfig10
-rw-r--r--core/java/android/service/dreams/DreamService.java97
-rw-r--r--core/java/android/text/Layout.java9
-rw-r--r--core/java/android/view/Choreographer.java6
-rw-r--r--core/java/android/view/LayoutInflater.java5
-rw-r--r--core/java/android/window/DesktopExperienceFlags.java17
-rw-r--r--core/java/android/window/WindowContainerTransaction.java39
-rw-r--r--core/java/com/android/internal/app/ChooserActivity.java76
-rw-r--r--core/java/com/android/internal/app/ChooserListAdapter.java20
-rw-r--r--core/java/com/android/internal/app/ResolverListAdapter.java6
-rw-r--r--core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java20
-rw-r--r--core/jni/com_android_internal_content_FileSystemUtils.cpp21
-rw-r--r--core/jni/com_android_internal_content_FileSystemUtils.h14
-rw-r--r--core/jni/com_android_internal_content_NativeLibraryHelper.cpp12
-rw-r--r--core/res/AndroidManifest.xml3
-rw-r--r--core/res/res/values/config.xml5
-rw-r--r--core/res/res/values/symbols.xml1
-rw-r--r--core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java6
-rw-r--r--core/tests/coretests/src/android/text/LayoutTest.java79
-rw-r--r--core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java3
-rw-r--r--libs/WindowManager/Shell/multivalentTests/Android.bp2
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt73
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt6
-rw-r--r--libs/WindowManager/Shell/res/values/strings.xml8
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java7
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt86
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt3
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt36
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt61
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt33
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt82
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt150
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt32
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt174
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java45
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt100
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt16
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java17
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt72
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt258
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt32
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt19
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt11
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt485
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt143
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt210
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt123
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java26
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt13
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java46
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt17
-rw-r--r--libs/androidfw/ResourceTypes.cpp94
-rw-r--r--libs/androidfw/include/androidfw/ResourceTypes.h5
-rw-r--r--libs/hostgraphics/HostBufferQueue.cpp1
-rw-r--r--media/java/android/media/IMediaRouter2.aidl3
-rw-r--r--media/java/android/media/IMediaRouter2Manager.aidl3
-rw-r--r--media/java/android/media/IMediaRouterService.aidl9
-rw-r--r--media/java/android/media/MediaCodecInfo.java145
-rw-r--r--media/java/android/media/MediaRouter2.java226
-rw-r--r--media/java/android/media/MediaRouter2Manager.java8
-rw-r--r--media/java/android/media/SuggestedDeviceInfo.aidl19
-rw-r--r--media/java/android/media/SuggestedDeviceInfo.java235
-rw-r--r--media/java/android/media/flags/media_better_together.aconfig8
-rw-r--r--media/java/android/media/quality/MediaQualityContract.java2
-rw-r--r--media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl2
-rw-r--r--packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java8
-rw-r--r--packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java4
-rw-r--r--packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java14
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig27
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt52
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt32
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt3
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt21
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt34
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt15
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt108
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json54
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json56
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json32
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json54
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json40
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json70
-rw-r--r--packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json54
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt27
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt2
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt3
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt4
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt5
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt29
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt144
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java119
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt20
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt240
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt147
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java31
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/DetailsViewModelTest.kt62
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt11
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt30
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt63
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java124
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt109
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt10
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt36
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt27
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt15
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt2
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java7
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt16
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt113
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryTest.kt95
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractorTest.kt63
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt3
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt1
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt20
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt (renamed from packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt)14
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt188
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/TileDetailsViewModel.kt12
-rw-r--r--packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml35
-rw-r--r--packages/SystemUI/res/layout/clipboard_overlay.xml3
-rw-r--r--packages/SystemUI/res/layout/media_output_dialog.xml27
-rw-r--r--packages/SystemUI/res/layout/notification_2025_smart_action_button.xml35
-rw-r--r--packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml36
-rw-r--r--packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json2
-rw-r--r--packages/SystemUI/res/values/dimens.xml2
-rw-r--r--packages/SystemUI/res/values/strings.xml4
-rw-r--r--packages/SystemUI/src/com/android/keyguard/ClockEventController.kt6
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java15
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java70
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java7
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java7
-rw-r--r--packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java21
-rw-r--r--packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt55
-rw-r--r--packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt148
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt54
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt32
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt170
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java27
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt91
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java112
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java52
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java15
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt126
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java13
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java153
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java55
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt96
-rw-r--r--packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/TileDetails.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt21
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsViewModel.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ModesDetailsViewModel.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsViewModel.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java17
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java91
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt66
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java29
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt9
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java34
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt8
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java19
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.kt35
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java53
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt46
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java5
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt10
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt33
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt119
-rw-r--r--packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt53
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt17
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt27
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java7
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java3
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt26
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java72
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java37
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java22
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt6
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/FakeTileDetailsViewModel.kt10
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt19
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt5
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt21
-rw-r--r--ravenwood/Android.bp18
-rw-r--r--ravenwood/Framework.bp12
-rw-r--r--ravenwood/TEST_MAPPING8
-rw-r--r--ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java22
-rw-r--r--ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java63
-rw-r--r--ravenwood/texts/ravenizer-standard-options.txt13
-rw-r--r--ravenwood/texts/ravenwood-standard-annotations.txt37
-rw-r--r--ravenwood/texts/ravenwood-standard-options.txt38
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt11
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt75
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt10
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt2
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt27
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt38
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt85
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt10
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt75
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt60
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt44
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt79
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt52
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt2
-rw-r--r--ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt7
-rw-r--r--ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt6
-rw-r--r--ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt4
-rw-r--r--ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt6
-rw-r--r--ravenwood/tools/ravenizer/Android.bp5
-rw-r--r--ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt110
-rw-r--r--ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt13
-rw-r--r--ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt14
-rw-r--r--ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt4
-rw-r--r--ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt6
-rw-r--r--services/accessibility/accessibility.aconfig20
-rw-r--r--services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java9
-rw-r--r--services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java49
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java48
-rw-r--r--services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java90
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java8
-rw-r--r--services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java23
-rw-r--r--services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java100
-rw-r--r--services/autofill/java/com/android/server/autofill/Session.java32
-rw-r--r--services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java7
-rw-r--r--services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java8
-rw-r--r--services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java1
-rw-r--r--services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java2
-rw-r--r--services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java10
-rw-r--r--services/core/Android.bp9
-rw-r--r--services/core/java/com/android/server/adb/AdbDebuggingManager.java6
-rw-r--r--services/core/java/com/android/server/adb/AdbService.java2
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java18
-rw-r--r--services/core/java/com/android/server/am/BroadcastProcessQueue.java4
-rw-r--r--services/core/java/com/android/server/am/BroadcastQueueImpl.java18
-rw-r--r--services/core/java/com/android/server/am/ConnectionRecord.java7
-rw-r--r--services/core/java/com/android/server/am/OomAdjuster.java15
-rw-r--r--services/core/java/com/android/server/am/OomAdjusterModernImpl.java9
-rw-r--r--services/core/java/com/android/server/am/UserController.java56
-rw-r--r--services/core/java/com/android/server/am/UserSwitchingDialog.java27
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java8
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsRegistry.java17
-rw-r--r--services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java7
-rw-r--r--services/core/java/com/android/server/audio/AudioDeviceBroker.java4
-rw-r--r--services/core/java/com/android/server/audio/AudioDeviceInventory.java4
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java6
-rw-r--r--services/core/java/com/android/server/audio/SoundDoseHelper.java2
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java4
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiControlService.java11
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java24
-rw-r--r--services/core/java/com/android/server/input/KeyGestureController.java109
-rw-r--r--services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java250
-rw-r--r--services/core/java/com/android/server/media/MediaRouterService.java33
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityService.java32
-rw-r--r--services/core/java/com/android/server/os/instrumentation/OWNERS1
-rw-r--r--services/core/java/com/android/server/policy/PhoneWindowManager.java81
-rw-r--r--services/core/java/com/android/server/updates/CertPinInstallReceiver.java20
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperCropper.java81
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperDataParser.java8
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java207
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java102
-rw-r--r--services/core/java/com/android/server/wallpaper/WallpaperManagerService.java16
-rw-r--r--services/core/java/com/android/server/wm/ActivityMetricsLogger.java11
-rw-r--r--services/core/java/com/android/server/wm/ActivityRecord.java24
-rw-r--r--services/core/java/com/android/server/wm/ActivitySnapshotController.java21
-rw-r--r--services/core/java/com/android/server/wm/ActivityStartController.java40
-rw-r--r--services/core/java/com/android/server/wm/ActivityStarter.java95
-rw-r--r--services/core/java/com/android/server/wm/ActivityTaskSupervisor.java15
-rw-r--r--services/core/java/com/android/server/wm/AppCompatController.java8
-rw-r--r--services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java39
-rw-r--r--services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java5
-rw-r--r--services/core/java/com/android/server/wm/AppCompatUtils.java3
-rw-r--r--services/core/java/com/android/server/wm/BackNavigationController.java40
-rw-r--r--services/core/java/com/android/server/wm/BackgroundActivityStartController.java11
-rw-r--r--services/core/java/com/android/server/wm/DeferredDisplayUpdater.java26
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeHelper.java10
-rw-r--r--services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java54
-rw-r--r--services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java31
-rw-r--r--services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java4
-rw-r--r--services/core/java/com/android/server/wm/InputManagerCallback.java7
-rw-r--r--services/core/java/com/android/server/wm/RootWindowContainer.java26
-rw-r--r--services/core/java/com/android/server/wm/Task.java41
-rw-r--r--services/core/java/com/android/server/wm/TaskDisplayArea.java56
-rw-r--r--services/core/java/com/android/server/wm/TaskFragment.java187
-rw-r--r--services/core/java/com/android/server/wm/Transition.java3
-rw-r--r--services/core/java/com/android/server/wm/WallpaperController.java22
-rw-r--r--services/core/java/com/android/server/wm/WindowContainer.java7
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerInternal.java7
-rw-r--r--services/core/java/com/android/server/wm/WindowManagerService.java45
-rw-r--r--services/core/java/com/android/server/wm/WindowOrganizerController.java34
-rw-r--r--services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java9
-rw-r--r--services/tests/DynamicInstrumentationManagerServiceTests/OWNERS1
-rw-r--r--services/tests/InputMethodSystemServerTests/Android.bp5
-rw-r--r--services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java1
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java30
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java101
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java340
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java2
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java108
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java77
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java32
-rw-r--r--services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java77
-rw-r--r--services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java32
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java46
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java13
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java9
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java873
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java1
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java37
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java71
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java2
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java5
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java6
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java12
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java7
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java22
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/TaskTests.java3
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java3
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java15
-rw-r--r--telephony/java/android/telephony/TelephonyManager.java11
-rw-r--r--tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/AppClose/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/FlickerService/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/IME/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/Notification/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/Rotation/AndroidTestTemplate.xml2
-rw-r--r--tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt4
-rw-r--r--tests/Input/src/com/android/server/input/BatteryControllerTests.kt4
-rw-r--r--tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt110
-rw-r--r--tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java8
-rw-r--r--tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java10
-rw-r--r--tests/utils/testutils/java/android/os/test/TestLooper.java11
-rw-r--r--tools/aapt2/Resource.h2
-rw-r--r--tools/aapt2/ResourceTable.cpp9
-rw-r--r--tools/aapt2/ResourceTable.h5
-rw-r--r--tools/aapt2/cmd/Link.cpp12
-rw-r--r--tools/aapt2/format/binary/BinaryResourceParser.cpp2
-rw-r--r--tools/aapt2/format/binary/ResEntryWriter.cpp4
-rw-r--r--tools/aapt2/format/binary/ResEntryWriter.h2
-rw-r--r--tools/aapt2/format/binary/TableFlattener.cpp3
-rw-r--r--tools/aapt2/format/binary/TableFlattener_test.cpp19
-rw-r--r--tools/aapt2/link/FlaggedResources_test.cpp48
-rw-r--r--tools/aapt2/link/FlaggedXmlVersioner.cpp27
454 files changed, 11426 insertions, 3988 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 2dd16de3e188..4d9fdf041d58 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -587,6 +587,7 @@ aconfig_declarations {
java_aconfig_library {
name: "android.view.inputmethod.flags-aconfig-java",
aconfig_declarations: "android.view.inputmethod.flags-aconfig",
+ host_supported: true,
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
diff --git a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
index 696bc82a9ffc..ae150aece432 100644
--- a/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
+++ b/cmds/bmgr/src/com/android/commands/bmgr/Bmgr.java
@@ -1309,6 +1309,8 @@ public class Bmgr {
return "AGENT_FAILURE_DURING_RESTORE";
case BackupManagerMonitor.LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT:
return "FAILED_TO_READ_DATA_FROM_TRANSPORT";
+ case BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN:
+ return "LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN";
default:
return "UNKNOWN_ID";
}
diff --git a/cmds/svc/src/com/android/commands/svc/UsbCommand.java b/cmds/svc/src/com/android/commands/svc/UsbCommand.java
index 26e20f601c7a..6542d08d6384 100644
--- a/cmds/svc/src/com/android/commands/svc/UsbCommand.java
+++ b/cmds/svc/src/com/android/commands/svc/UsbCommand.java
@@ -89,6 +89,11 @@ public class UsbCommand extends Svc.Command {
IUsbManager usbMgr = IUsbManager.Stub.asInterface(ServiceManager.getService(
Context.USB_SERVICE));
+ if (usbMgr == null) {
+ System.err.println("Could not obtain USB service. Try again later.");
+ return;
+ }
+
Executor executor = context.getMainExecutor();
Consumer<Integer> consumer = new Consumer<Integer>(){
public void accept(Integer status){
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index bb023f25094e..12bfccf2172c 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -4507,7 +4507,7 @@ package android.window {
method @NonNull public android.window.WindowContainerTransaction requestFocusOnTaskFragment(@NonNull android.os.IBinder);
method @NonNull public android.window.WindowContainerTransaction scheduleFinishEnterPip(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
method @NonNull public android.window.WindowContainerTransaction setActivityWindowingMode(@NonNull android.window.WindowContainerToken, int);
- method @NonNull public android.window.WindowContainerTransaction setAdjacentRoots(@NonNull android.window.WindowContainerToken, @NonNull android.window.WindowContainerToken);
+ method @Deprecated @NonNull public android.window.WindowContainerTransaction setAdjacentRoots(@NonNull android.window.WindowContainerToken, @NonNull android.window.WindowContainerToken);
method @NonNull public android.window.WindowContainerTransaction setAdjacentTaskFragments(@NonNull android.os.IBinder, @NonNull android.os.IBinder, @Nullable android.window.WindowContainerTransaction.TaskFragmentAdjacentParams);
method @NonNull public android.window.WindowContainerTransaction setAppBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
method @NonNull public android.window.WindowContainerTransaction setBounds(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index b38f5da6b638..54ab3b8f185b 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -55,9 +55,7 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Point;
-import android.graphics.Rect;
import android.graphics.drawable.Icon;
-import android.hardware.HardwareBuffer;
import android.os.BatteryStats;
import android.os.Binder;
import android.os.Build;
@@ -86,7 +84,6 @@ import android.util.Log;
import android.util.Singleton;
import android.util.Size;
import android.view.WindowInsetsController.Appearance;
-import android.window.TaskSnapshot;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.LocalePicker;
@@ -5420,10 +5417,11 @@ public class ActivityManager {
*
* @hide
*/
+ @Nullable
@RequiresPermission(Manifest.permission.MANAGE_USERS)
- public @Nullable String getSwitchingFromUserMessage() {
+ public String getSwitchingFromUserMessage(@UserIdInt int userId) {
try {
- return getService().getSwitchingFromUserMessage();
+ return getService().getSwitchingFromUserMessage(userId);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
@@ -5434,10 +5432,11 @@ public class ActivityManager {
*
* @hide
*/
+ @Nullable
@RequiresPermission(Manifest.permission.MANAGE_USERS)
- public @Nullable String getSwitchingToUserMessage() {
+ public String getSwitchingToUserMessage(@UserIdInt int userId) {
try {
- return getService().getSwitchingToUserMessage();
+ return getService().getSwitchingToUserMessage(userId);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index a12c0674998e..e5f7889859c1 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -292,14 +292,14 @@ public abstract class ActivityManagerInternal {
public abstract boolean canStartMoreUsers();
/**
- * Sets the user switcher message for switching from {@link android.os.UserHandle#SYSTEM}.
+ * Sets the user switcher message for switching from a user.
*/
- public abstract void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage);
+ public abstract void setSwitchingFromUserMessage(@UserIdInt int user, @Nullable String message);
/**
- * Sets the user switcher message for switching to {@link android.os.UserHandle#SYSTEM}.
+ * Sets the user switcher message for switching to a user.
*/
- public abstract void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage);
+ public abstract void setSwitchingToUserMessage(@UserIdInt int user, @Nullable String message);
/**
* Returns maximum number of users that can run simultaneously.
diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java
index 599f1a8be233..ea4646aa9eb9 100644
--- a/core/java/android/app/AppCompatTaskInfo.java
+++ b/core/java/android/app/AppCompatTaskInfo.java
@@ -102,6 +102,8 @@ public class AppCompatTaskInfo implements Parcelable {
private static final int FLAG_FULLSCREEN_OVERRIDE_USER = FLAG_BASE << 8;
/** Top activity flag for whether min aspect ratio of the activity has been overridden.*/
public static final int FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE = FLAG_BASE << 9;
+ /** Top activity flag for whether restart menu is shown due to display move. */
+ private static final int FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE = FLAG_BASE << 10;
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {
@@ -115,7 +117,8 @@ public class AppCompatTaskInfo implements Parcelable {
FLAG_ELIGIBLE_FOR_USER_ASPECT_RATIO_BUTTON,
FLAG_FULLSCREEN_OVERRIDE_SYSTEM,
FLAG_FULLSCREEN_OVERRIDE_USER,
- FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE
+ FLAG_HAS_MIN_ASPECT_RATIO_OVERRIDE,
+ FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE
})
public @interface TopActivityFlag {}
@@ -133,7 +136,8 @@ public class AppCompatTaskInfo implements Parcelable {
@TopActivityFlag
private static final int FLAGS_COMPAT_UI_INTERESTED = FLAGS_ORGANIZER_INTERESTED
- | FLAG_IN_SIZE_COMPAT | FLAG_ELIGIBLE_FOR_LETTERBOX_EDU | FLAG_LETTERBOX_EDU_ENABLED;
+ | FLAG_IN_SIZE_COMPAT | FLAG_ELIGIBLE_FOR_LETTERBOX_EDU | FLAG_LETTERBOX_EDU_ENABLED
+ | FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE;
private AppCompatTaskInfo() {
// Do nothing
@@ -300,6 +304,21 @@ public class AppCompatTaskInfo implements Parcelable {
}
/**
+ * @return {@code true} if the restart menu is enabled for the top activity due to display move.
+ */
+ public boolean isRestartMenuEnabledForDisplayMove() {
+ return isTopActivityFlagEnabled(FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE);
+ }
+
+ /**
+ * Sets the top activity flag for whether the restart menu is enabled for the top activity due
+ * to display move.
+ */
+ public void setRestartMenuEnabledForDisplayMove(boolean enable) {
+ setTopActivityFlag(FLAG_ENABLE_RESTART_MENU_FOR_DISPLAY_MOVE, enable);
+ }
+
+ /**
* @return {@code true} if the top activity bounds are letterboxed.
*/
public boolean isTopActivityLetterboxed() {
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index ad01ad57b2d8..6cdfb97520ae 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -403,8 +403,8 @@ interface IActivityManager {
void setPackageScreenCompatMode(in String packageName, int mode);
@UnsupportedAppUsage
boolean switchUser(int userid);
- String getSwitchingFromUserMessage();
- String getSwitchingToUserMessage();
+ String getSwitchingFromUserMessage(int userId);
+ String getSwitchingToUserMessage(int userId);
@UnsupportedAppUsage
void setStopUserOnSwitch(int value);
boolean removeTask(int taskId);
diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java
index 6936ddc5eeac..59247934ba46 100644
--- a/core/java/android/app/TaskInfo.java
+++ b/core/java/android/app/TaskInfo.java
@@ -655,8 +655,10 @@ public class TaskInfo {
+ " effectiveUid=" + effectiveUid
+ " displayId=" + displayId
+ " isRunning=" + isRunning
- + " baseIntent=" + baseIntent + " baseActivity=" + baseActivity
- + " topActivity=" + topActivity + " origActivity=" + origActivity
+ + " baseIntent=" + baseIntent
+ + " baseActivity=" + baseActivity
+ + " topActivity=" + topActivity
+ + " origActivity=" + origActivity
+ " realActivity=" + realActivity
+ " numActivities=" + numActivities
+ " lastActiveTime=" + lastActiveTime
diff --git a/core/java/android/app/backup/BackupManagerMonitor.java b/core/java/android/app/backup/BackupManagerMonitor.java
index e741bc2bf608..19c24cd8a14a 100644
--- a/core/java/android/app/backup/BackupManagerMonitor.java
+++ b/core/java/android/app/backup/BackupManagerMonitor.java
@@ -297,6 +297,9 @@ public class BackupManagerMonitor {
@hide */
public static final int LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT = 81;
+ /** The pipe between the BackupAgent and the framework was broken during full backup. @hide */
+ public static final int LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN = 82;
+
/**
* This method will be called each time something important happens on BackupManager.
*
diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig
index 5c267c9f6475..4afe75f7814c 100644
--- a/core/java/android/app/notification.aconfig
+++ b/core/java/android/app/notification.aconfig
@@ -265,6 +265,16 @@ flag {
}
flag {
+ name: "expanding_public_view"
+ namespace: "systemui"
+ description: "enables user expanding the public view of a notification"
+ bug: "398853084"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "api_rich_ongoing"
is_exported: true
namespace: "systemui"
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 55d78f9b8c48..cc288b1f5601 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -744,15 +744,22 @@ public abstract class Context {
*/
public static final long BIND_MATCH_QUARANTINED_COMPONENTS = 0x2_0000_0000L;
+ /**
+ * Flag for {@link #bindService} that allows the bound app to be frozen if it is eligible.
+ *
+ * @hide
+ */
+ public static final long BIND_ALLOW_FREEZE = 0x4_0000_0000L;
/**
* These bind flags reduce the strength of the binding such that we shouldn't
* consider it as pulling the process up to the level of the one that is bound to it.
* @hide
*/
- public static final int BIND_REDUCTION_FLAGS =
+ public static final long BIND_REDUCTION_FLAGS =
Context.BIND_ALLOW_OOM_MANAGEMENT | Context.BIND_WAIVE_PRIORITY
- | Context.BIND_NOT_PERCEPTIBLE | Context.BIND_NOT_VISIBLE;
+ | Context.BIND_NOT_PERCEPTIBLE | Context.BIND_NOT_VISIBLE
+ | Context.BIND_ALLOW_FREEZE;
/** @hide */
@IntDef(flag = true, prefix = { "RECEIVER_VISIBLE" }, value = {
diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java
index 74da62c85ed2..10d3051cff6f 100644
--- a/core/java/android/content/pm/RegisteredServicesCache.java
+++ b/core/java/android/content/pm/RegisteredServicesCache.java
@@ -166,7 +166,15 @@ public abstract class RegisteredServicesCache<V> {
@UnsupportedAppUsage
public RegisteredServicesCache(Context context, String interfaceName, String metaDataName,
String attributeName, XmlSerializerAndParser<V> serializerAndParser) {
- mContext = context;
+ this(new Injector<V>(context), interfaceName, metaDataName, attributeName,
+ serializerAndParser);
+ }
+
+ /** Provides the basic functionality for unit tests. */
+ @VisibleForTesting
+ public RegisteredServicesCache(Injector<V> injector, String interfaceName, String metaDataName,
+ String attributeName, XmlSerializerAndParser<V> serializerAndParser) {
+ mContext = injector.getContext();
mInterfaceName = interfaceName;
mMetaDataName = metaDataName;
mAttributesName = attributeName;
@@ -184,7 +192,7 @@ public abstract class RegisteredServicesCache<V> {
if (isCore) {
intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
}
- mBackgroundHandler = BackgroundThread.getHandler();
+ mBackgroundHandler = injector.getBackgroundHandler();
mContext.registerReceiverAsUser(
mPackageReceiver, UserHandle.ALL, intentFilter, null, mBackgroundHandler);
@@ -918,4 +926,25 @@ public abstract class RegisteredServicesCache<V> {
return null;
}
}
+
+ /**
+ * Point of injection for test dependencies.
+ * @param <V> The type of the value.
+ */
+ @VisibleForTesting
+ public static class Injector<V> {
+ private final Context mContext;
+
+ public Injector(Context context) {
+ mContext = context;
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public Handler getBackgroundHandler() {
+ return BackgroundThread.getHandler();
+ }
+ }
}
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index bcb7ebfb286f..6e9dcf5a83a1 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -1714,7 +1714,7 @@ public final class CameraManager {
final TaskInfo taskInfo = appTask.getTaskInfo();
final int freeformCameraCompatMode = taskInfo.appCompatTaskInfo
.cameraCompatTaskInfo.freeformCameraCompatMode;
- if (freeformCameraCompatMode != 0
+ if (isInCameraCompatMode(freeformCameraCompatMode)
&& taskInfo.topActivity != null
&& taskInfo.topActivity.getPackageName().equals(packageName)) {
// WindowManager has requested rotation override.
@@ -1741,6 +1741,12 @@ public final class CameraManager {
: ICameraService.ROTATION_OVERRIDE_NONE;
}
+ private static boolean isInCameraCompatMode(@CameraCompatTaskInfo.FreeformCameraCompatMode int
+ freeformCameraCompatMode) {
+ return (freeformCameraCompatMode != CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_UNSPECIFIED)
+ && (freeformCameraCompatMode != CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE);
+ }
+
private static int getRotationOverrideForCompatFreeform(
@CameraCompatTaskInfo.FreeformCameraCompatMode int freeformCameraCompatMode) {
// Only rotate-and-crop if the app and device orientations do not match.
diff --git a/core/java/android/hardware/input/IKeyGestureHandler.aidl b/core/java/android/hardware/input/IKeyGestureHandler.aidl
index 509b9482154e..4da991ee85b1 100644
--- a/core/java/android/hardware/input/IKeyGestureHandler.aidl
+++ b/core/java/android/hardware/input/IKeyGestureHandler.aidl
@@ -28,15 +28,4 @@ interface IKeyGestureHandler {
* to that gesture.
*/
boolean handleKeyGesture(in AidlKeyGestureEvent event, in IBinder focusedToken);
-
- /**
- * Called to know if a particular gesture type is supported by the handler.
- *
- * TODO(b/358569822): Remove this call to reduce the binder calls to single call for
- * handleKeyGesture. For this we need to remove dependency of multi-key gestures to identify if
- * a key gesture is supported on first relevant key down.
- * Also, for now we prioritize handlers in the system server process above external handlers to
- * reduce IPC binder calls.
- */
- boolean isKeyGestureSupported(int gestureType);
}
diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java
index 49db54d81e65..d6419afb2a5a 100644
--- a/core/java/android/hardware/input/InputManager.java
+++ b/core/java/android/hardware/input/InputManager.java
@@ -1758,13 +1758,6 @@ public final class InputManager {
*/
boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
@Nullable IBinder focusedToken);
-
- /**
- * Called to identify if a particular gesture is of interest to a handler.
- *
- * NOTE: If no active handler supports certain gestures, the gestures will not be captured.
- */
- boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType);
}
/** @hide */
diff --git a/core/java/android/hardware/input/InputManagerGlobal.java b/core/java/android/hardware/input/InputManagerGlobal.java
index a9a45ae45ec3..c4b4831ba76e 100644
--- a/core/java/android/hardware/input/InputManagerGlobal.java
+++ b/core/java/android/hardware/input/InputManagerGlobal.java
@@ -1193,23 +1193,6 @@ public final class InputManagerGlobal {
}
return false;
}
-
- @Override
- public boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
- synchronized (mKeyGestureEventHandlerLock) {
- if (mKeyGestureEventHandlers == null) {
- return false;
- }
- final int numHandlers = mKeyGestureEventHandlers.size();
- for (int i = 0; i < numHandlers; i++) {
- KeyGestureEventHandler handler = mKeyGestureEventHandlers.get(i);
- if (handler.isKeyGestureSupported(gestureType)) {
- return true;
- }
- }
- }
- return false;
- }
}
/**
diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java
index 1a712d2b3f31..9dd1fed4a85a 100644
--- a/core/java/android/hardware/input/KeyGestureEvent.java
+++ b/core/java/android/hardware/input/KeyGestureEvent.java
@@ -108,7 +108,8 @@ public final class KeyGestureEvent {
public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD = 55;
public static final int KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD = 56;
public static final int KEY_GESTURE_TYPE_GLOBAL_ACTIONS = 57;
- public static final int KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58;
+ @Deprecated
+ public static final int DEPRECATED_KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD = 58;
public static final int KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT = 59;
public static final int KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT = 60;
public static final int KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS = 61;
@@ -200,7 +201,6 @@ public final class KeyGestureEvent {
KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD,
KEY_GESTURE_TYPE_GLOBAL_ACTIONS,
- KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT,
KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT,
KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS,
@@ -777,8 +777,6 @@ public final class KeyGestureEvent {
return "KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD";
case KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
return "KEY_GESTURE_TYPE_GLOBAL_ACTIONS";
- case KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
- return "KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD";
case KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
return "KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT";
case KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index 414f27498414..176c0c8ab966 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -165,3 +165,14 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "haptics"
+ name: "remove_hidl_support"
+ description: "Remove framework code to support HIDL vibrator HALs."
+ bug: "308452413"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_FEATURE
+ }
+}
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index ae0b56e6f009..45a21beabd89 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -44,6 +44,16 @@ flag {
}
flag {
+ name: "notify_single_item_change_on_icon_load"
+ namespace: "intentresolver"
+ description: "ChooserGridAdapter to notify specific items change when the target icon is loaded (instead of all-item change)."
+ bug: "298193161"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "fix_resolver_memory_leak"
is_exported: true
namespace: "intentresolver"
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index f026614f2a35..ce31e1ea7e38 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -455,7 +455,7 @@ public class DreamService extends Service implements Window.Callback {
// Simply wake up in the case the device is not locked.
if (!keyguardManager.isKeyguardLocked()) {
- wakeUp(false);
+ wakeUp();
return true;
}
@@ -477,11 +477,11 @@ public class DreamService extends Service implements Window.Callback {
if (!mInteractive) {
if (mDebug) Slog.v(mTag, "Waking up on keyEvent");
- wakeUp(false);
+ wakeUp();
return true;
} else if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (mDebug) Slog.v(mTag, "Waking up on back key");
- wakeUp(false);
+ wakeUp();
return true;
}
return mWindow.superDispatchKeyEvent(event);
@@ -492,7 +492,7 @@ public class DreamService extends Service implements Window.Callback {
public boolean dispatchKeyShortcutEvent(KeyEvent event) {
if (!mInteractive) {
if (mDebug) Slog.v(mTag, "Waking up on keyShortcutEvent");
- wakeUp(false);
+ wakeUp();
return true;
}
return mWindow.superDispatchKeyShortcutEvent(event);
@@ -505,7 +505,7 @@ public class DreamService extends Service implements Window.Callback {
// but finish()es on any other kind of activity
if (!mInteractive && event.getActionMasked() == MotionEvent.ACTION_UP) {
if (mDebug) Slog.v(mTag, "Waking up on touchEvent");
- wakeUp(false);
+ wakeUp();
return true;
}
return mWindow.superDispatchTouchEvent(event);
@@ -516,7 +516,7 @@ public class DreamService extends Service implements Window.Callback {
public boolean dispatchTrackballEvent(MotionEvent event) {
if (!mInteractive) {
if (mDebug) Slog.v(mTag, "Waking up on trackballEvent");
- wakeUp(false);
+ wakeUp();
return true;
}
return mWindow.superDispatchTrackballEvent(event);
@@ -527,7 +527,7 @@ public class DreamService extends Service implements Window.Callback {
public boolean dispatchGenericMotionEvent(MotionEvent event) {
if (!mInteractive) {
if (mDebug) Slog.v(mTag, "Waking up on genericMotionEvent");
- wakeUp(false);
+ wakeUp();
return true;
}
return mWindow.superDispatchGenericMotionEvent(event);
@@ -925,37 +925,32 @@ public class DreamService extends Service implements Window.Callback {
}
}
- /**
- * Updates doze state. Note that this must be called on the mHandler.
- */
- private void updateDoze() {
- mHandler.post(() -> {
- if (mDreamToken == null) {
- Slog.w(mTag, "Updating doze without a dream token.");
- return;
- }
+ private synchronized void updateDoze() {
+ if (mDreamToken == null) {
+ Slog.w(mTag, "Updating doze without a dream token.");
+ return;
+ }
- if (mDozing) {
- try {
- Slog.v(mTag, "UpdateDoze mDozeScreenState=" + mDozeScreenState
- + " mDozeScreenBrightness=" + mDozeScreenBrightness
- + " mDozeScreenBrightnessFloat=" + mDozeScreenBrightnessFloat);
- if (startAndStopDozingInBackground()) {
- mDreamManager.startDozingOneway(
- mDreamToken, mDozeScreenState, mDozeScreenStateReason,
- mDozeScreenBrightnessFloat, mDozeScreenBrightness,
- mUseNormalBrightnessForDoze);
- } else {
- mDreamManager.startDozing(
- mDreamToken, mDozeScreenState, mDozeScreenStateReason,
- mDozeScreenBrightnessFloat, mDozeScreenBrightness,
- mUseNormalBrightnessForDoze);
- }
- } catch (RemoteException ex) {
- // system server died
+ if (mDozing) {
+ try {
+ Slog.v(mTag, "UpdateDoze mDozeScreenState=" + mDozeScreenState
+ + " mDozeScreenBrightness=" + mDozeScreenBrightness
+ + " mDozeScreenBrightnessFloat=" + mDozeScreenBrightnessFloat);
+ if (startAndStopDozingInBackground()) {
+ mDreamManager.startDozingOneway(
+ mDreamToken, mDozeScreenState, mDozeScreenStateReason,
+ mDozeScreenBrightnessFloat, mDozeScreenBrightness,
+ mUseNormalBrightnessForDoze);
+ } else {
+ mDreamManager.startDozing(
+ mDreamToken, mDozeScreenState, mDozeScreenStateReason,
+ mDozeScreenBrightnessFloat, mDozeScreenBrightness,
+ mUseNormalBrightnessForDoze);
}
+ } catch (RemoteException ex) {
+ // system server died
}
- });
+ }
}
/**
@@ -971,16 +966,14 @@ public class DreamService extends Service implements Window.Callback {
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public void stopDozing() {
- mHandler.post(() -> {
- if (mDozing) {
- mDozing = false;
- try {
- mDreamManager.stopDozing(mDreamToken);
- } catch (RemoteException ex) {
- // system server died
- }
+ if (mDozing) {
+ mDozing = false;
+ try {
+ mDreamManager.stopDozing(mDreamToken);
+ } catch (RemoteException ex) {
+ // system server died
}
- });
+ }
}
/**
@@ -1208,7 +1201,7 @@ public class DreamService extends Service implements Window.Callback {
@Override
public void onExitRequested() {
// Simply finish dream when exit is requested.
- mHandler.post(() -> finishInternal());
+ mHandler.post(() -> finish());
}
@Override
@@ -1306,13 +1299,9 @@ public class DreamService extends Service implements Window.Callback {
* </p>
*/
public final void finish() {
- mHandler.post(this::finishInternal);
- }
-
- private void finishInternal() {
// If there is an active overlay connection, signal that the dream is ending before
- // continuing. Note that the overlay cannot rely on the unbound state, since another
- // dream might have bound to it in the meantime.
+ // continuing. Note that the overlay cannot rely on the unbound state, since another dream
+ // might have bound to it in the meantime.
if (mOverlayConnection != null) {
mOverlayConnection.addConsumer(overlay -> {
try {
@@ -1368,7 +1357,7 @@ public class DreamService extends Service implements Window.Callback {
* </p>
*/
public final void wakeUp() {
- mHandler.post(()-> wakeUp(false));
+ wakeUp(false);
}
/**
@@ -1570,7 +1559,7 @@ public class DreamService extends Service implements Window.Callback {
if (mActivity != null && !mActivity.isFinishing()) {
mActivity.finishAndRemoveTask();
} else {
- finishInternal();
+ finish();
}
mDreamToken = null;
@@ -1730,7 +1719,7 @@ public class DreamService extends Service implements Window.Callback {
// the window reference in order to fully release the DreamActivity.
mWindow = null;
mActivity = null;
- finishInternal();
+ finish();
}
if (mOverlayConnection != null && mDreamStartOverlayConsumer != null) {
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 44c3f9a8244e..0152c52a6753 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -75,9 +75,12 @@ public abstract class Layout {
// These should match the constants in framework/base/libs/hwui/hwui/DrawTextFunctor.h
private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX = 0f;
private static final float HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR = 0f;
- private static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP = 5f;
// since we're not using soft light yet, this needs to be much lower than the spec'd 0.8
private static final float HIGH_CONTRAST_TEXT_BACKGROUND_ALPHA_PERCENTAGE = 0.7f;
+ @VisibleForTesting
+ static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP = 5f;
+ @VisibleForTesting
+ static final float HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR = 0.5f;
/** @hide */
@IntDef(prefix = { "BREAK_STRATEGY_" }, value = {
@@ -1030,7 +1033,9 @@ public abstract class Layout {
var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX,
mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR);
- var cornerRadius = mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP;
+ var cornerRadius = Math.max(
+ mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP,
+ mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR);
// We set the alpha on the color itself instead of Paint.setAlpha(), because that function
// actually mutates the color in... *ehem* very strange ways. Also the color might get reset
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index 7c1e4976b9d3..3a2ec91b3b20 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -967,8 +967,11 @@ public final class Choreographer {
DisplayEventReceiver.VsyncEventData vsyncEventData) {
final long startNanos;
final long frameIntervalNanos = vsyncEventData.frameInterval;
- boolean resynced = false;
+ // Original intended vsync time that is not adjusted by jitter
+ // or buffer stuffing recovery. Reported for jank tracking.
+ final long intendedFrameTimeNanos = frameTimeNanos;
long offsetFrameTimeNanos = frameTimeNanos;
+ boolean resynced = false;
// Evaluate if buffer stuffing recovery needs to start or end, and
// what actions need to be taken for recovery.
@@ -1012,7 +1015,6 @@ public final class Choreographer {
+ ((offsetFrameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
}
- long intendedFrameTimeNanos = offsetFrameTimeNanos;
startNanos = System.nanoTime();
// Calculating jitter involves using the original frame time without
// adjustments from buffer stuffing
diff --git a/core/java/android/view/LayoutInflater.java b/core/java/android/view/LayoutInflater.java
index aad3bf2679d9..901fc02f0040 100644
--- a/core/java/android/view/LayoutInflater.java
+++ b/core/java/android/view/LayoutInflater.java
@@ -516,8 +516,9 @@ public abstract class LayoutInflater {
mConstructorArgs[0] = inflaterContext;
View result = root;
- if (root != null && root.getViewRootImpl() != null) {
- root.getViewRootImpl().notifyRendererOfExpensiveFrame();
+ ViewRootImpl viewRootImpl = root != null ? root.getViewRootImpl() : null;
+ if (viewRootImpl != null) {
+ viewRootImpl.notifyRendererOfExpensiveFrame();
}
try {
diff --git a/core/java/android/window/DesktopExperienceFlags.java b/core/java/android/window/DesktopExperienceFlags.java
index 7b87eea67004..866c16cb566d 100644
--- a/core/java/android/window/DesktopExperienceFlags.java
+++ b/core/java/android/window/DesktopExperienceFlags.java
@@ -47,17 +47,19 @@ public enum DesktopExperienceFlags {
com.android.server.display.feature.flags.Flags::baseDensityForExternalDisplays, true),
CONNECTED_DISPLAYS_CURSOR(com.android.input.flags.Flags::connectedDisplaysCursor, true),
DISPLAY_TOPOLOGY(com.android.server.display.feature.flags.Flags::displayTopology, true),
- ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY(Flags::enableBugFixesForSecondaryDisplay, false),
+ ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY(Flags::enableBugFixesForSecondaryDisplay, true),
ENABLE_CONNECTED_DISPLAYS_DND(Flags::enableConnectedDisplaysDnd, false),
ENABLE_CONNECTED_DISPLAYS_PIP(Flags::enableConnectedDisplaysPip, false),
- ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG(Flags::enableConnectedDisplaysWindowDrag, false),
+ ENABLE_CONNECTED_DISPLAYS_WALLPAPER(
+ android.app.Flags::enableConnectedDisplaysWallpaper, false),
+ ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG(Flags::enableConnectedDisplaysWindowDrag, true),
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_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS(Flags::enableDisplayFocusInShellTransitions, true),
+ ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING(Flags::enableDisplayWindowingModeSwitching, true),
ENABLE_DRAG_TO_MAXIMIZE(Flags::enableDragToMaximize, true),
- ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT(Flags::enableMoveToNextDisplayShortcut, false),
+ ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT(Flags::enableMoveToNextDisplayShortcut, true),
ENABLE_MULTIPLE_DESKTOPS_BACKEND(Flags::enableMultipleDesktopsBackend, false),
ENABLE_MULTIPLE_DESKTOPS_FRONTEND(Flags::enableMultipleDesktopsFrontend, false),
ENABLE_PERSISTING_DISPLAY_SIZE_FOR_CONNECTED_DISPLAYS(
@@ -66,9 +68,10 @@ public enum DesktopExperienceFlags {
false),
ENABLE_PER_DISPLAY_PACKAGE_CONTEXT_CACHE_IN_STATUSBAR_NOTIF(
Flags::enablePerDisplayPackageContextCacheInStatusbarNotif, false),
- ENABLE_TASKBAR_CONNECTED_DISPLAYS(Flags::enableTaskbarConnectedDisplays, false),
+ ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE(Flags::enableProjectedDisplayDesktopMode, false),
+ ENABLE_TASKBAR_CONNECTED_DISPLAYS(Flags::enableTaskbarConnectedDisplays, true),
ENTER_DESKTOP_BY_DEFAULT_ON_FREEFORM_DISPLAYS(Flags::enterDesktopByDefaultOnFreeformDisplays,
- false),
+ true),
FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH(Flags::formFactorBasedDesktopFirstSwitch, false),
REPARENT_WINDOW_TOKEN_API(Flags::reparentWindowTokenApi, true)
// go/keep-sorted end
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 1156503cf8e8..485e7b33f3a7 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -684,16 +684,13 @@ public final class WindowContainerTransaction implements Parcelable {
* organizer.
* @param root1 the first root.
* @param root2 the second root.
+ * @deprecated replace with {@link #setAdjacentRootSet}
*/
+ @SuppressWarnings("UnflaggedApi") // @TestApi without associated feature.
+ @Deprecated
@NonNull
public WindowContainerTransaction setAdjacentRoots(
@NonNull WindowContainerToken root1, @NonNull WindowContainerToken root2) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- mHierarchyOps.add(HierarchyOp.createForAdjacentRoots(
- root1.asBinder(),
- root2.asBinder()));
- return this;
- }
return setAdjacentRootSet(root1, root2);
}
@@ -710,14 +707,10 @@ public final class WindowContainerTransaction implements Parcelable {
*
* @param roots the Tasks that should be adjacent to each other.
* @throws IllegalArgumentException if roots have size < 2.
- * @hide // TODO(b/373709676) Rename to setAdjacentRoots and update CTS.
+ * @hide // TODO(b/373709676) Rename to setAdjacentRoots and update CTS in 25Q4.
*/
@NonNull
public WindowContainerTransaction setAdjacentRootSet(@NonNull WindowContainerToken... roots) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalArgumentException("allowMultipleAdjacentTaskFragments is not enabled."
- + " Use #setAdjacentRoots instead.");
- }
if (roots.length < 2) {
throw new IllegalArgumentException("setAdjacentRootSet must have size >= 2");
}
@@ -1013,7 +1006,7 @@ public final class WindowContainerTransaction implements Parcelable {
/**
* Sets to TaskFragments adjacent to each other. Containers below two visible adjacent
* TaskFragments will be made invisible. This is similar to
- * {@link #setAdjacentRoots(WindowContainerToken, WindowContainerToken)}, but can be used with
+ * {@link #setAdjacentRootSet(WindowContainerToken...)}, but can be used with
* fragmentTokens when that TaskFragments haven't been created (but will be created in the same
* {@link WindowContainerTransaction}).
* @param fragmentToken1 client assigned unique token to create TaskFragment with specified
@@ -1973,13 +1966,6 @@ public final class WindowContainerTransaction implements Parcelable {
return mContainers;
}
- /** @deprecated b/373709676 replace with {@link #getContainers()}. */
- @Deprecated
- @NonNull
- public IBinder getAdjacentRoot() {
- return mReparent;
- }
-
public boolean getToTop() {
return mToTop;
}
@@ -2127,17 +2113,12 @@ public final class WindowContainerTransaction implements Parcelable {
sb.append(mContainer).append(" to ").append(mToTop ? "top" : "bottom");
break;
case HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS:
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- for (IBinder container : mContainers) {
- if (container == mContainers[0]) {
- sb.append("adjacentRoots=").append(container);
- } else {
- sb.append(", ").append(container);
- }
+ for (IBinder container : mContainers) {
+ if (container == mContainers[0]) {
+ sb.append("adjacentRoots=").append(container);
+ } else {
+ sb.append(", ").append(container);
}
- } else {
- sb.append("container=").append(mContainer)
- .append(" adjacentRoot=").append(mReparent);
}
break;
case HIERARCHY_OP_TYPE_LAUNCH_TASK:
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index c009fc3b7e63..9bc6671bbc31 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -23,6 +23,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
import static android.content.ContentProvider.getUriWithoutUserId;
import static android.content.ContentProvider.getUserIdFromUri;
+import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad;
import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
@@ -163,9 +164,11 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@@ -3212,6 +3215,8 @@ public class ChooserActivity extends ResolverActivity implements
private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20;
+ private final Set<ViewHolderBase> mBoundViewHolders = new HashSet<>();
+
ChooserGridAdapter(ChooserListAdapter wrappedAdapter) {
super();
mChooserListAdapter = wrappedAdapter;
@@ -3232,6 +3237,31 @@ public class ChooserActivity extends ResolverActivity implements
notifyDataSetChanged();
}
});
+ if (notifySingleItemChangeOnIconLoad()) {
+ wrappedAdapter.setOnIconLoadedListener(this::onTargetIconLoaded);
+ }
+ }
+
+ private void onTargetIconLoaded(DisplayResolveInfo info) {
+ for (ViewHolderBase holder : mBoundViewHolders) {
+ switch (holder.getViewType()) {
+ case VIEW_TYPE_NORMAL:
+ TargetInfo itemInfo =
+ mChooserListAdapter.getItem(
+ ((ItemViewHolder) holder).mListPosition);
+ if (info == itemInfo) {
+ notifyItemChanged(holder.getAdapterPosition());
+ }
+ break;
+ case VIEW_TYPE_CALLER_AND_RANK:
+ ItemGroupViewHolder groupHolder = (ItemGroupViewHolder) holder;
+ if (suggestedAppsGroupContainsTarget(groupHolder, info)) {
+ notifyItemChanged(holder.getAdapterPosition());
+ }
+ break;
+ }
+
+ }
}
public void setFooterHeight(int height) {
@@ -3382,6 +3412,9 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ if (notifySingleItemChangeOnIconLoad()) {
+ mBoundViewHolders.add((ViewHolderBase) holder);
+ }
int viewType = ((ViewHolderBase) holder).getViewType();
switch (viewType) {
case VIEW_TYPE_DIRECT_SHARE:
@@ -3396,6 +3429,22 @@ public class ChooserActivity extends ResolverActivity implements
}
@Override
+ public void onViewRecycled(RecyclerView.ViewHolder holder) {
+ if (notifySingleItemChangeOnIconLoad()) {
+ mBoundViewHolders.remove((ViewHolderBase) holder);
+ }
+ super.onViewRecycled(holder);
+ }
+
+ @Override
+ public boolean onFailedToRecycleView(RecyclerView.ViewHolder holder) {
+ if (notifySingleItemChangeOnIconLoad()) {
+ mBoundViewHolders.remove((ViewHolderBase) holder);
+ }
+ return super.onFailedToRecycleView(holder);
+ }
+
+ @Override
public int getItemViewType(int position) {
int count;
@@ -3604,6 +3653,33 @@ public class ChooserActivity extends ResolverActivity implements
}
}
+ /**
+ * Checks whether the suggested apps group, {@code holder}, contains the target,
+ * {@code info}.
+ */
+ private boolean suggestedAppsGroupContainsTarget(
+ ItemGroupViewHolder holder, DisplayResolveInfo info) {
+
+ int position = holder.getAdapterPosition();
+ int start = getListPosition(position);
+ int startType = getRowType(start);
+
+ int columnCount = holder.getColumnCount();
+ int end = start + columnCount - 1;
+ while (getRowType(end) != startType && end >= start) {
+ end--;
+ }
+
+ for (int i = 0; i < columnCount; i++) {
+ if (start + i <= end) {
+ if (mChooserListAdapter.getItem(holder.getItemIndex(i)) == info) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
int getListPosition(int position) {
position -= getSystemRowCount() + getProfileRowCount();
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index d38689c7505b..1b8c36db3908 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -16,9 +16,12 @@
package com.android.internal.app;
+import static android.service.chooser.Flags.notifySingleItemChangeOnIconLoad;
+
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
+import android.annotation.Nullable;
import android.app.prediction.AppPredictor;
import android.content.ComponentName;
import android.content.Context;
@@ -56,6 +59,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
public class ChooserListAdapter extends ResolverListAdapter {
private static final String TAG = "ChooserListAdapter";
@@ -108,6 +112,9 @@ public class ChooserListAdapter extends ResolverListAdapter {
// Represents the UserSpace in which the Initial Intents should be resolved.
private final UserHandle mInitialIntentsUserSpace;
+ @Nullable
+ private Consumer<DisplayResolveInfo> mOnIconLoadedListener;
+
// For pinned direct share labels, if the text spans multiple lines, the TextView will consume
// the full width, even if the characters actually take up less than that. Measure the actual
// line widths and constrain the View's width based upon that so that the pin doesn't end up
@@ -218,6 +225,10 @@ public class ChooserListAdapter extends ResolverListAdapter {
true);
}
+ public void setOnIconLoadedListener(Consumer<DisplayResolveInfo> onIconLoadedListener) {
+ mOnIconLoadedListener = onIconLoadedListener;
+ }
+
AppPredictor getAppPredictor() {
return mAppPredictor;
}
@@ -329,6 +340,15 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
}
+ @Override
+ protected void onIconLoaded(DisplayResolveInfo info) {
+ if (notifySingleItemChangeOnIconLoad() && mOnIconLoadedListener != null) {
+ mOnIconLoadedListener.accept(info);
+ } else {
+ notifyDataSetChanged();
+ }
+ }
+
private void loadDirectShareIcon(SelectableTargetInfo info) {
LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info);
if (task == null) {
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 54c0e61fd5cd..4d9ce86096c7 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -680,6 +680,10 @@ public class ResolverListAdapter extends BaseAdapter {
}
}
+ protected void onIconLoaded(DisplayResolveInfo info) {
+ notifyDataSetChanged();
+ }
+
private void loadLabel(DisplayResolveInfo info) {
LoadLabelTask task = mLabelLoaders.get(info);
if (task == null) {
@@ -1004,7 +1008,7 @@ public class ResolverListAdapter extends BaseAdapter {
mResolverListCommunicator.updateProfileViewButton();
} else if (!mDisplayResolveInfo.hasDisplayIcon()) {
mDisplayResolveInfo.setDisplayIcon(d);
- notifyDataSetChanged();
+ onIconLoaded(mDisplayResolveInfo);
}
}
}
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index 2d989943800e..6f7e5ad51b89 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -79,6 +79,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@@ -864,5 +865,24 @@ public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implemen
throw new RuntimeException("Both mMessageString and mMessageHash should never be null");
}
}
+
+ /**
+ * This is only used by unit tests to wait until {@link #connectToConfigurationService} is
+ * done. Because unit tests are sensitive to concurrent accesses.
+ */
+ @VisibleForTesting
+ public static void waitForInitialization() {
+ final IProtoLog currentInstance = ProtoLog.getSingleInstance();
+ if (!(currentInstance instanceof PerfettoProtoLogImpl protoLog)) {
+ return;
+ }
+ try {
+ protoLog.mBackgroundLoggingService.submit(() -> {
+ Log.i(LOG_TAG, "Complete initialization");
+ }).get();
+ } catch (InterruptedException | ExecutionException e) {
+ Log.e(LOG_TAG, "Failed to wait for tracing service", e);
+ }
+ }
}
diff --git a/core/jni/com_android_internal_content_FileSystemUtils.cpp b/core/jni/com_android_internal_content_FileSystemUtils.cpp
index 48c92c87f54e..886b9f1ae658 100644
--- a/core/jni/com_android_internal_content_FileSystemUtils.cpp
+++ b/core/jni/com_android_internal_content_FileSystemUtils.cpp
@@ -201,8 +201,8 @@ bool punchHoles(const char *filePath, const uint64_t offset,
return true;
}
-bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
- std::vector<Elf64_Phdr> &programHeaders) {
+read_elf_status_t getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
+ std::vector<Elf64_Phdr> &programHeaders) {
// Open Elf file
Elf64_Ehdr ehdr;
std::ifstream inputStream(filePath, std::ifstream::in);
@@ -212,13 +212,13 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
// read executable headers
inputStream.read((char *)&ehdr, sizeof(ehdr));
if (!inputStream.good()) {
- return false;
+ return ELF_READ_ERROR;
}
- // only consider elf64 for punching holes
+ // only consider ELF64 files
if (ehdr.e_ident[EI_CLASS] != ELFCLASS64) {
ALOGW("Provided file is not ELF64");
- return false;
+ return ELF_IS_NOT_64_BIT;
}
// read the program headers from elf file
@@ -229,7 +229,7 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
uint64_t phOffset;
if (__builtin_add_overflow(offset, programHeaderOffset, &phOffset)) {
ALOGE("Overflow occurred when calculating phOffset");
- return false;
+ return ELF_READ_ERROR;
}
inputStream.seekg(phOffset);
@@ -237,7 +237,7 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
Elf64_Phdr header;
inputStream.read((char *)&header, sizeof(header));
if (!inputStream.good()) {
- return false;
+ return ELF_READ_ERROR;
}
if (header.p_type != PT_LOAD) {
@@ -246,13 +246,14 @@ bool getLoadSegmentPhdrs(const char *filePath, const uint64_t offset,
programHeaders.push_back(header);
}
- return true;
+ return ELF_READ_OK;
}
bool punchHolesInElf64(const char *filePath, const uint64_t offset) {
std::vector<Elf64_Phdr> programHeaders;
- if (!getLoadSegmentPhdrs(filePath, offset, programHeaders)) {
- ALOGE("Failed to read program headers from ELF file.");
+ read_elf_status_t status = getLoadSegmentPhdrs(filePath, offset, programHeaders);
+ if (status != ELF_READ_OK) {
+ ALOGE("Failed to read program headers from 64 bit ELF file.");
return false;
}
return punchHoles(filePath, offset, programHeaders);
diff --git a/core/jni/com_android_internal_content_FileSystemUtils.h b/core/jni/com_android_internal_content_FileSystemUtils.h
index 4a95686c5a0c..c4dc1115e6c4 100644
--- a/core/jni/com_android_internal_content_FileSystemUtils.h
+++ b/core/jni/com_android_internal_content_FileSystemUtils.h
@@ -22,6 +22,12 @@
namespace android {
+enum read_elf_status_t {
+ ELF_IS_NOT_64_BIT = -2,
+ ELF_READ_ERROR = -1,
+ ELF_READ_OK = 0,
+};
+
/*
* This function deallocates space used by zero padding at the end of LOAD segments in given
* uncompressed ELF file. Read ELF headers and find out the offset and sizes of LOAD segments.
@@ -39,10 +45,10 @@ bool punchHolesInElf64(const char* filePath, uint64_t offset);
bool punchHolesInZip(const char* filePath, uint64_t offset, uint16_t extraFieldLen);
/*
- * This function reads program headers from ELF file. ELF can be specified with file path directly
- * or it should be at offset inside Apk. Program headers passed to function is populated.
+ * This function reads program headers from 64 bit ELF file. ELF can be specified with file path
+ * directly or it should be at offset inside Apk. Program headers passed to function is populated.
*/
-bool getLoadSegmentPhdrs(const char* filePath, const uint64_t offset,
- std::vector<Elf64_Phdr>& programHeaders);
+read_elf_status_t getLoadSegmentPhdrs(const char* filePath, const uint64_t offset,
+ std::vector<Elf64_Phdr>& programHeaders);
} // namespace android \ No newline at end of file
diff --git a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
index 14132e61ff0e..7cf523f18a90 100644
--- a/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
+++ b/core/jni/com_android_internal_content_NativeLibraryHelper.cpp
@@ -640,7 +640,17 @@ com_android_internal_content_NativeLibraryHelper_openApkFd(JNIEnv *env, jclass,
static jint checkLoadSegmentAlignment(const char* fileName, off64_t offset) {
std::vector<Elf64_Phdr> programHeaders;
- if (!getLoadSegmentPhdrs(fileName, offset, programHeaders)) {
+ read_elf_status_t status = getLoadSegmentPhdrs(fileName, offset, programHeaders);
+ // Ignore the ELFs which are not 64 bit.
+ if (status == ELF_IS_NOT_64_BIT) {
+ ALOGW("ELF file is not 64 Bit");
+ // PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED is equivalent of skipping the current file.
+ // on return, flag is OR'ed with flags from other ELF files. If some app has 32 bit ELF in
+ // 64 bit directory, alignment of that ELF will be ignored.
+ return PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED;
+ }
+
+ if (status == ELF_READ_ERROR) {
ALOGE("Failed to read program headers from ELF file.");
return PAGE_SIZE_APP_COMPAT_FLAG_ERROR;
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 36b65ba43162..e16ce9849ff2 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -9292,9 +9292,6 @@
<action android:name="android.intent.action.UPDATE_PINS" />
<data android:scheme="content" android:host="*" android:mimeType="*/*" />
</intent-filter>
- <intent-filter>
- <action android:name="android.intent.action.BOOT_COMPLETED" />
- </intent-filter>
</receiver>
<receiver android:name="com.android.server.updates.IntentFirewallInstallReceiver"
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 8c5b20da73ef..7a38dce296de 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4184,6 +4184,11 @@
<!-- Whether device supports double tap to wake -->
<bool name="config_supportDoubleTapWake">false</bool>
+ <!-- Whether device supports double tap to sleep. This will allow the user to enable/disable
+ double tap gestures in non-action areas in the lock screen and launcher workspace to go to
+ sleep. -->
+ <bool name="config_supportDoubleTapSleep">false</bool>
+
<!-- The RadioAccessFamilies supported by the device.
Empty is viewed as "all". Only used on devices which
don't support RIL_REQUEST_GET_RADIO_CAPABILITY
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3de30f7f25a8..46d18e3d3302 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3142,6 +3142,7 @@
<java-symbol type="color" name="chooser_row_divider" />
<java-symbol type="layout" name="chooser_row_direct_share" />
<java-symbol type="bool" name="config_supportDoubleTapWake" />
+ <java-symbol type="bool" name="config_supportDoubleTapSleep" />
<java-symbol type="drawable" name="ic_perm_device_info" />
<java-symbol type="string" name="config_radio_access_family" />
<java-symbol type="string" name="notification_inbox_ellipsis" />
diff --git a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
index ee4761b9d024..dccbf4036b3e 100644
--- a/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
+++ b/core/tests/coretests/src/android/content/pm/RegisteredServicesCacheTest.java
@@ -245,6 +245,12 @@ public class RegisteredServicesCacheTest extends AndroidTestCase {
SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME, new TestSerializer());
}
+ TestServicesCache(Injector<TestServiceType> injector,
+ XmlSerializerAndParser<TestServiceType> serializerAndParser) {
+ super(injector, SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME,
+ serializerAndParser);
+ }
+
@Override
public TestServiceType parseServiceAttributes(Resources res, String packageName,
AttributeSet attrs) {
diff --git a/core/tests/coretests/src/android/text/LayoutTest.java b/core/tests/coretests/src/android/text/LayoutTest.java
index 9e78af57b470..11ec9f8e1912 100644
--- a/core/tests/coretests/src/android/text/LayoutTest.java
+++ b/core/tests/coretests/src/android/text/LayoutTest.java
@@ -16,6 +16,9 @@
package android.text;
+import static android.text.Layout.HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR;
+import static android.text.Layout.HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP;
+
import static com.android.graphics.hwui.flags.Flags.FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT;
import static org.junit.Assert.assertArrayEquals;
@@ -1073,6 +1076,68 @@ public class LayoutTest {
}
}
+ @Test
+ @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+ public void highContrastTextEnabled_testRoundedRectSize_belowMinimum_usesMinimumValue() {
+ mTextPaint.setColor(Color.BLACK);
+ mTextPaint.setTextSize(8); // Value chosen so that N * RADIUS_FACTOR < RADIUS_MIN_DP
+ Layout layout = new StaticLayout("Test text", mTextPaint, mWidth,
+ mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false);
+
+ MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256);
+ c.setHighContrastTextEnabled(true);
+ layout.draw(
+ c,
+ /* highlightPaths= */ null,
+ /* highlightPaints= */ null,
+ /* selectionPath= */ null,
+ /* selectionPaint= */ null,
+ /* cursorOffsetVertical= */ 0
+ );
+
+ final float expectedRoundedRectSize =
+ mTextPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_MIN_DP;
+ List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
+ for (int i = 0; i < drawCommands.size(); i++) {
+ MockCanvas.DrawCommand drawCommand = drawCommands.get(i);
+ if (drawCommand.rect != null) {
+ expect.that(drawCommand.rX).isEqualTo(expectedRoundedRectSize);
+ expect.that(drawCommand.rY).isEqualTo(expectedRoundedRectSize);
+ }
+ }
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_HIGH_CONTRAST_TEXT_SMALL_TEXT_RECT)
+ public void highContrastTextEnabled_testRoundedRectSize_aboveMinimum_usesScaledValue() {
+ mTextPaint.setColor(Color.BLACK);
+ mTextPaint.setTextSize(50); // Value chosen so that N * RADIUS_FACTOR > RADIUS_MIN_DP
+ Layout layout = new StaticLayout("Test text", mTextPaint, mWidth,
+ mAlign, mSpacingMult, mSpacingAdd, /* includePad= */ false);
+
+ MockCanvas c = new MockCanvas(/* width= */ 256, /* height= */ 256);
+ c.setHighContrastTextEnabled(true);
+ layout.draw(
+ c,
+ /* highlightPaths= */ null,
+ /* highlightPaints= */ null,
+ /* selectionPath= */ null,
+ /* selectionPaint= */ null,
+ /* cursorOffsetVertical= */ 0
+ );
+
+ final float expectedRoundedRectSize =
+ mTextPaint.getTextSize() * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_FACTOR;
+ List<MockCanvas.DrawCommand> drawCommands = c.getDrawCommands();
+ for (int i = 0; i < drawCommands.size(); i++) {
+ MockCanvas.DrawCommand drawCommand = drawCommands.get(i);
+ if (drawCommand.rect != null) {
+ expect.that(drawCommand.rX).isEqualTo(expectedRoundedRectSize);
+ expect.that(drawCommand.rY).isEqualTo(expectedRoundedRectSize);
+ }
+ }
+ }
+
private int removeAlpha(int color) {
return Color.rgb(
Color.red(color),
@@ -1087,6 +1152,8 @@ public class LayoutTest {
public final String text;
public final float x;
public final float y;
+ public final float rX;
+ public final float rY;
public final Path path;
public final RectF rect;
public final Paint paint;
@@ -1098,6 +1165,8 @@ public class LayoutTest {
this.paint = new Paint(paint);
path = null;
rect = null;
+ this.rX = 0;
+ this.rY = 0;
}
DrawCommand(Path path, Paint paint) {
@@ -1107,15 +1176,19 @@ public class LayoutTest {
x = 0;
text = null;
rect = null;
+ this.rX = 0;
+ this.rY = 0;
}
- DrawCommand(RectF rect, Paint paint) {
+ DrawCommand(RectF rect, Paint paint, float rX, float rY) {
this.rect = new RectF(rect);
this.paint = new Paint(paint);
path = null;
y = 0;
x = 0;
text = null;
+ this.rX = rX;
+ this.rY = rY;
}
@Override
@@ -1189,12 +1262,12 @@ public class LayoutTest {
@Override
public void drawRect(RectF rect, Paint p) {
- mDrawCommands.add(new DrawCommand(rect, p));
+ mDrawCommands.add(new DrawCommand(rect, p, 0, 0));
}
@Override
public void drawRoundRect(@NonNull RectF rect, float rx, float ry, @NonNull Paint paint) {
- mDrawCommands.add(new DrawCommand(rect, paint));
+ mDrawCommands.add(new DrawCommand(rect, paint, rx, ry));
}
List<DrawCommand> getDrawCommands() {
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java
index db69cf2397fc..02a296892c53 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityWorkProfileTest.java
@@ -72,7 +72,8 @@ public class ChooserActivityWorkProfileTest {
private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry
.getInstrumentation().getTargetContext().getUser();
- private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10);
+ private static final UserHandle WORK_USER_HANDLE =
+ UserHandle.of(PERSONAL_USER_HANDLE.getIdentifier() + 1);
@Rule
public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp
index 03076c0940a4..50666911e313 100644
--- a/libs/WindowManager/Shell/multivalentTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentTests/Android.bp
@@ -51,6 +51,7 @@ android_robolectric_test {
"androidx.test.ext.junit",
"mockito-robolectric-prebuilt",
"mockito-kotlin2",
+ "platform-parametric-runner-lib",
"truth",
"flag-junit-base",
"flag-junit",
@@ -74,6 +75,7 @@ android_test {
"frameworks-base-testutils",
"mockito-kotlin2",
"mockito-target-extended-minus-junit4",
+ "platform-parametric-runner-lib",
"truth",
"platform-test-annotations",
"platform-test-rules",
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt
new file mode 100644
index 000000000000..bdfaef2c6960
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleExpandedViewTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles
+
+import android.content.ComponentName
+import android.content.Context
+import android.platform.test.flag.junit.FlagsParameterization
+import android.platform.test.flag.junit.SetFlagsRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.Flags
+import com.android.wm.shell.taskview.TaskView
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+/** Tests for [BubbleExpandedView] */
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class BubbleExpandedViewTest(flags: FlagsParameterization) {
+
+ @get:Rule
+ val setFlagsRule = SetFlagsRule(flags)
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private val componentName = ComponentName(context, "TestClass")
+
+ @Test
+ fun getTaskId_onTaskCreated_returnsCorrectTaskId() {
+ val bubbleTaskView = BubbleTaskView(mock<TaskView>(), directExecutor())
+ val expandedView = BubbleExpandedView(context).apply {
+ initialize(
+ mock<BubbleExpandedViewManager>(),
+ mock<BubbleStackView>(),
+ mock<BubblePositioner>(),
+ false /* isOverflow */,
+ bubbleTaskView,
+ )
+ setAnimating(true) // Skips setContentVisibility for testing.
+ }
+
+ bubbleTaskView.listener.onTaskCreated(123, componentName)
+
+ assertThat(expandedView.getTaskId()).isEqualTo(123)
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams() = FlagsParameterization.allCombinationsOf(
+ Flags.FLAG_ENABLE_BUBBLE_TASK_VIEW_LISTENER,
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
index 9087da34d259..636ff669d6b4 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt
@@ -266,8 +266,6 @@ class BubbleTaskViewListenerTest {
optionsCaptor.capture(),
any())
- assertThat((intentCaptor.lastValue.flags
- and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
@@ -295,8 +293,6 @@ class BubbleTaskViewListenerTest {
optionsCaptor.capture(),
any())
- assertThat((intentCaptor.lastValue.flags
- and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
@@ -324,8 +320,6 @@ class BubbleTaskViewListenerTest {
optionsCaptor.capture(),
any())
- assertThat((intentCaptor.lastValue.flags
- and Intent.FLAG_ACTIVITY_MULTIPLE_TASK) != 0).isTrue()
assertThat(optionsCaptor.lastValue.launchedFromBubble).isFalse() // chat only
assertThat(optionsCaptor.lastValue.isApplyActivityFlagsForBubbles).isFalse() // chat only
assertThat(optionsCaptor.lastValue.taskAlwaysOnTop).isTrue()
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 2179128df300..5ef83826840b 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -226,7 +226,7 @@
<string name="windowing_app_handle_education_tooltip">The app menu can be found here</string>
<!-- App handle education tooltip text for tooltip pointing to windowing image button -->
- <string name="windowing_desktop_mode_image_button_education_tooltip">Enter desktop view to open multiple apps together</string>
+ <string name="windowing_desktop_mode_image_button_education_tooltip">Enter desktop windowing to open multiple apps together</string>
<!-- App handle education tooltip text for tooltip pointing to app chip -->
<string name="windowing_desktop_mode_exit_education_tooltip">Return to full screen anytime from the app menu</string>
@@ -293,7 +293,7 @@
<!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] -->
<string name="fullscreen_text">Fullscreen</string>
<!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] -->
- <string name="desktop_text">Desktop View</string>
+ <string name="desktop_text">Desktop windowing</string>
<!-- Accessibility text for the handle split screen button [CHAR LIMIT=NONE] -->
<string name="split_screen_text">Split Screen</string>
<!-- Accessibility text for the handle more options button [CHAR LIMIT=NONE] -->
@@ -319,7 +319,7 @@
<!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] -->
<string name="collapse_menu_text">Close Menu</string>
<!-- Accessibility text for the App Header's App Chip [CHAR LIMIT=NONE] -->
- <string name="desktop_mode_app_header_chip_text"><xliff:g id="app_name" example="Chrome">%1$s</xliff:g> (Desktop View)</string>
+ <string name="desktop_mode_app_header_chip_text"><xliff:g id="app_name" example="Chrome">%1$s</xliff:g> (Desktop windowing)</string>
<!-- Maximize menu maximize button string. -->
<string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string>
<!-- Maximize menu snap buttons string. -->
@@ -348,7 +348,7 @@
<!-- Accessibility action replacement for caption handle app chip buttons [CHAR LIMIT=NONE] -->
<string name="app_handle_chip_accessibility_announce">Open Menu</string>
<!-- Accessibility action replacement for caption handle menu buttons [CHAR LIMIT=NONE] -->
- <string name="app_handle_menu_accessibility_announce">Enter <xliff:g id="windowing_mode" example="Desktop View">%1$s</xliff:g></string>
+ <string name="app_handle_menu_accessibility_announce">Enter <xliff:g id="windowing_mode" example="Desktop windowing">%1$s</xliff:g></string>
<!-- Accessibility action replacement for maximize menu enter snap left button [CHAR LIMIT=NONE] -->
<string name="maximize_menu_talkback_action_snap_left_text">Resize window to left</string>
<!-- Accessibility action replacement for maximize menu enter snap right button [CHAR LIMIT=NONE] -->
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java
index 01d2201a5a0c..8bcbd2a3fc9f 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellSharedConstants.java
@@ -22,4 +22,11 @@ package com.android.wm.shell.shared;
public class ShellSharedConstants {
public static final String KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION =
"extra_shell_can_hand_off_animation";
+
+ /**
+ * Defines the max screen width or height in dp for a device to be considered a small tablet.
+ *
+ * @see android.view.WindowManager#LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP
+ */
+ public static final int SMALL_TABLET_MAX_EDGE_DP = 960;
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt
new file mode 100644
index 000000000000..9bf56b075112
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TypefaceUtils.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared
+
+import android.graphics.Typeface
+import android.widget.TextView
+import com.android.wm.shell.Flags
+
+/**
+ * Utility class to apply a specified typeface to a [TextView].
+ *
+ * This class provides a method, [setTypeface],
+ * to easily set a pre-defined font family and style to a given [TextView].
+ */
+class TypefaceUtils {
+
+ enum class FontFamily(val value: String) {
+ GSF_DISPLAY_LARGE("variable-display-large"),
+ GSF_DISPLAY_MEDIUM("variable-display-medium"),
+ GSF_DISPLAY_SMALL("variable-display-small"),
+ GSF_HEADLINE_LARGE("variable-headline-large"),
+ GSF_HEADLINE_MEDIUM("variable-headline-medium"),
+ GSF_HEADLINE_SMALL("variable-headline-small"),
+ GSF_TITLE_LARGE("variable-title-large"),
+ GSF_TITLE_MEDIUM("variable-title-medium"),
+ GSF_TITLE_SMALL("variable-title-small"),
+ GSF_LABEL_LARGE("variable-label-large"),
+ GSF_LABEL_MEDIUM("variable-label-medium"),
+ GSF_LABEL_SMALL("variable-label-small"),
+ GSF_BODY_LARGE("variable-body-large"),
+ GSF_BODY_MEDIUM("variable-body-medium"),
+ GSF_BODY_SMALL("variable-body-small"),
+ GSF_DISPLAY_LARGE_EMPHASIZED("variable-display-large-emphasized"),
+ GSF_DISPLAY_MEDIUM_EMPHASIZED("variable-display-medium-emphasized"),
+ GSF_DISPLAY_SMALL_EMPHASIZED("variable-display-small-emphasized"),
+ GSF_HEADLINE_LARGE_EMPHASIZED("variable-headline-large-emphasized"),
+ GSF_HEADLINE_MEDIUM_EMPHASIZED("variable-headline-medium-emphasized"),
+ GSF_HEADLINE_SMALL_EMPHASIZED("variable-headline-small-emphasized"),
+ GSF_TITLE_LARGE_EMPHASIZED("variable-title-large-emphasized"),
+ GSF_TITLE_MEDIUM_EMPHASIZED("variable-title-medium-emphasized"),
+ GSF_TITLE_SMALL_EMPHASIZED("variable-title-small-emphasized"),
+ GSF_LABEL_LARGE_EMPHASIZED("variable-label-large-emphasized"),
+ GSF_LABEL_MEDIUM_EMPHASIZED("variable-label-medium-emphasized"),
+ GSF_LABEL_SMALL_EMPHASIZED("variable-label-small-emphasized"),
+ GSF_BODY_LARGE_EMPHASIZED("variable-body-large-emphasized"),
+ GSF_BODY_MEDIUM_EMPHASIZED("variable-body-medium-emphasized"),
+ GSF_BODY_SMALL_EMPHASIZED("variable-body-small-emphasized"),
+ }
+
+ companion object {
+ /**
+ * Sets the typeface of the provided [textView] to the specified [fontFamily] and [fontStyle].
+ *
+ * The typeface is only applied to the [TextView] when [Flags.enableGsf] is `true`.
+ * If [Flags.enableGsf] is `false`, this method has no effect.
+ *
+ * @param textView The [TextView] to which the typeface should be applied. If `null`, this method does nothing.
+ * @param fontFamily The desired [FontFamily] for the [TextView].
+ * @param fontStyle The desired font style (e.g., [Typeface.NORMAL], [Typeface.BOLD], [Typeface.ITALIC]). Defaults to [Typeface.NORMAL].
+ */
+ @JvmStatic
+ @JvmOverloads
+ fun setTypeface(
+ textView: TextView?,
+ fontFamily: FontFamily,
+ fontStyle: Int = Typeface.NORMAL,
+ ) {
+ if (!Flags.enableGsf()) return
+ textView?.typeface = Typeface.create(fontFamily.name, fontStyle)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt
index 1b7c9c282304..ad2671b8135d 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt
@@ -26,6 +26,8 @@ import android.view.WindowInsets
import android.view.WindowManager
import kotlin.math.max
+import com.android.wm.shell.shared.ShellSharedConstants.SMALL_TABLET_MAX_EDGE_DP
+
/** Contains device configuration used for positioning bubbles on the screen. */
data class DeviceConfig(
val isLargeScreen: Boolean,
@@ -38,7 +40,6 @@ data class DeviceConfig(
companion object {
private const val LARGE_SCREEN_MIN_EDGE_DP = 600
- private const val SMALL_TABLET_MAX_EDGE_DP = 960
@JvmStatic
fun create(context: Context, windowManager: WindowManager): DeviceConfig {
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 2e33253b5e09..ed5e0c608675 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
@@ -17,7 +17,9 @@
package com.android.wm.shell.shared.desktopmode;
import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED;
+import static android.window.DesktopExperienceFlags.ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE;
+import static com.android.server.display.feature.flags.Flags.enableDisplayContentModeManagement;
import static com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper.enableBubbleToFullscreen;
import android.annotation.NonNull;
@@ -224,7 +226,7 @@ public class DesktopModeStatus {
/**
* Return {@code true} if the current device can host desktop sessions on its internal display.
*/
- public static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
+ private static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops);
}
@@ -269,6 +271,29 @@ public class DesktopModeStatus {
}
/**
+ * Check to see if a display should have desktop mode enabled or not. Internal
+ * and external displays have separate logic.
+ */
+ public static boolean isDesktopModeSupportedOnDisplay(Context context, Display display) {
+ if (!canEnterDesktopMode(context)) {
+ return false;
+ }
+ if (display.getType() == Display.TYPE_INTERNAL) {
+ return canInternalDisplayHostDesktops(context);
+ }
+
+ // TODO (b/395014779): Change this to use WM API
+ if ((display.getType() == Display.TYPE_EXTERNAL
+ || display.getType() == Display.TYPE_OVERLAY)
+ && enableDisplayContentModeManagement()) {
+ final WindowManager wm = context.getSystemService(WindowManager.class);
+ return wm != null && wm.shouldShowSystemDecors(display.getDisplayId());
+ }
+
+ return false;
+ }
+
+ /**
* Returns whether the multiple desktops feature is enabled for this device (both backend and
* frontend implementations).
*/
@@ -341,8 +366,11 @@ public class DesktopModeStatus {
if (!enforceDeviceRestrictions()) {
return true;
}
- final boolean desktopModeSupported = isDesktopModeSupported(context)
- && canInternalDisplayHostDesktops(context);
+ // If projected display is enabled, #canInternalDisplayHostDesktops is no longer a
+ // requirement.
+ final boolean desktopModeSupported = ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE.isTrue()
+ ? isDesktopModeSupported(context) : (isDesktopModeSupported(context)
+ && canInternalDisplayHostDesktops(context));
final boolean desktopModeSupportedByDevOptions =
Flags.enableDesktopModeThroughDevOption()
&& isDesktopModeDevOptionSupported(context);
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 50e2f4d52bf2..3e95a0b1100f 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
@@ -1604,7 +1604,7 @@ public class BubbleController implements ConfigurationChangeListener,
@Nullable BubbleTransitions.DragData dragData) {
if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) return;
Bubble b = mBubbleData.getOrCreateBubble(taskInfo); // Removes from overflow
- ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", taskInfo.taskId);
+ ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - taskId=%s", taskInfo.taskId);
BubbleBarLocation location = null;
if (dragData != null) {
location =
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 2c2451cab999..8ac9230c36c3 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
@@ -238,7 +238,6 @@ public class BubbleExpandedView extends LinearLayout {
mContext.createContextAsUser(
mBubble.getUser(), Context.CONTEXT_RESTRICTED);
Intent fillInIntent = new Intent();
- fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
PendingIntent pi = PendingIntent.getActivity(
context,
/* requestCode= */ 0,
@@ -467,6 +466,11 @@ public class BubbleExpandedView extends LinearLayout {
new BubbleTaskViewListener.Callback() {
@Override
public void onTaskCreated() {
+ // The taskId is saved to use for removeTask,
+ // preventing appearance in recent tasks.
+ mTaskId = ((BubbleTaskViewListener) mCurrentTaskViewListener)
+ .getTaskId();
+
setContentVisibility(true);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
index 63d713495177..9c20e3af9ab4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java
@@ -130,7 +130,6 @@ public class BubbleTaskViewListener implements TaskView.Listener {
mContext.createContextAsUser(
mBubble.getUser(), Context.CONTEXT_RESTRICTED);
Intent fillInIntent = new Intent();
- fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
// First try get pending intent from the bubble
PendingIntent pi = mBubble.getPendingIntent();
if (pi == null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
index d5f2dbdbf5f5..51a5b12edb84 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java
@@ -639,7 +639,7 @@ public class BubbleTransitions {
@Override
public void continueCollapse() {
mBubble.cleanupTaskView();
- if (mTaskLeash == null) return;
+ if (mTaskLeash == null || !mTaskLeash.isValid()) return;
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
t.reparent(mTaskLeash, mRootLeash);
t.apply();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 6c840f020f90..bdb21f246359 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
@@ -447,9 +447,9 @@ public class BubbleBarLayerView extends FrameLayout
bubble.cleanupViews(!inTransition);
endAction.run();
};
- if (mBubbleData.getBubbles().isEmpty()) {
- // we're removing the last bubble. collapse the expanded view and cleanup bubble views
- // at the end.
+ if (mBubbleData.getBubbles().isEmpty() || inTransition) {
+ // If we are removing the last bubble or removing the current bubble via transition,
+ // collapse the expanded view and clean up bubbles at the end.
collapse(cleanUp);
} else {
cleanUp.run();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 9b11e4ab16fa..4413c8715c0d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -18,6 +18,8 @@ package com.android.wm.shell.compatui;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static com.android.wm.shell.compatui.impl.CompatUIRequestsKt.DISPLAY_COMPAT_SHOW_RESTART_DIALOG;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.TaskInfo;
@@ -53,7 +55,9 @@ import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.api.CompatUIEvent;
import com.android.wm.shell.compatui.api.CompatUIHandler;
import com.android.wm.shell.compatui.api.CompatUIInfo;
+import com.android.wm.shell.compatui.api.CompatUIRequest;
import com.android.wm.shell.compatui.impl.CompatUIEvents.SizeCompatRestartButtonClicked;
+import com.android.wm.shell.compatui.impl.CompatUIRequests;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.sysui.KeyguardChangeListener;
@@ -245,6 +249,21 @@ public class CompatUIController implements OnDisplaysChangedListener,
mCallback = callback;
}
+ @Override
+ public void sendCompatUIRequest(CompatUIRequest compatUIRequest) {
+ switch(compatUIRequest.getRequestId()) {
+ case DISPLAY_COMPAT_SHOW_RESTART_DIALOG:
+ handleDisplayCompatShowRestartDialog(compatUIRequest.asType());
+ break;
+ default:
+ }
+ }
+
+ private void handleDisplayCompatShowRestartDialog(
+ CompatUIRequests.DisplayCompatShowRestartDialog request) {
+ onRestartButtonClicked(new Pair<>(request.getTaskInfo(), request.getTaskListener()));
+ }
+
/**
* Called when the Task info changed. Creates and updates the compat UI if there is an
* activity in size compat, or removes the UI if there is no size compat activity.
@@ -254,13 +273,17 @@ public class CompatUIController implements OnDisplaysChangedListener,
public void onCompatInfoChanged(@NonNull CompatUIInfo compatUIInfo) {
final TaskInfo taskInfo = compatUIInfo.getTaskInfo();
final ShellTaskOrganizer.TaskListener taskListener = compatUIInfo.getListener();
- if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()) {
+ final boolean isInDisplayCompatMode =
+ taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove();
+ if (taskInfo != null && !taskInfo.appCompatTaskInfo.isTopActivityInSizeCompat()
+ && !isInDisplayCompatMode) {
mSetOfTaskIdsShowingRestartDialog.remove(taskInfo.taskId);
}
mIsInDesktopMode = isInDesktopMode(taskInfo);
// We close all the Compat UI educations in case TaskInfo has no configuration or
// TaskListener or in desktop mode.
- if (taskInfo.configuration == null || taskListener == null || mIsInDesktopMode) {
+ if (taskInfo.configuration == null || taskListener == null
+ || (mIsInDesktopMode && !isInDisplayCompatMode)) {
// Null token means the current foreground activity is not in compatibility mode.
removeLayouts(taskInfo.taskId);
return;
@@ -552,8 +575,11 @@ public class CompatUIController implements OnDisplaysChangedListener,
@Nullable ShellTaskOrganizer.TaskListener taskListener) {
RestartDialogWindowManager layout =
mTaskIdToRestartDialogWindowManagerMap.get(taskInfo.taskId);
+ final boolean isInNonDisplayCompatDesktopMode = mIsInDesktopMode
+ && !taskInfo.appCompatTaskInfo.isRestartMenuEnabledForDisplayMove();
if (layout != null) {
- if (layout.needsToBeRecreated(taskInfo, taskListener) || mIsInDesktopMode) {
+ if (layout.needsToBeRecreated(taskInfo, taskListener)
+ || isInNonDisplayCompatDesktopMode) {
mTaskIdToRestartDialogWindowManagerMap.remove(taskInfo.taskId);
layout.release();
} else {
@@ -568,8 +594,9 @@ public class CompatUIController implements OnDisplaysChangedListener,
return;
}
}
- if (mIsInDesktopMode) {
- // Return if in desktop mode.
+ if (isInNonDisplayCompatDesktopMode) {
+ // No restart dialog can be shown in desktop mode unless the task is in display compat
+ // mode.
return;
}
// Create a new UI layout.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt
index 817e554b550e..f71f8099f29f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIHandler.kt
@@ -28,6 +28,11 @@ interface CompatUIHandler {
fun onCompatInfoChanged(compatUIInfo: CompatUIInfo)
/**
+ * Invoked when another component in Shell requests a CompatUI state change.
+ */
+ fun sendCompatUIRequest(compatUIRequest: CompatUIRequest)
+
+ /**
* Optional reference to the object responsible to send {@link CompatUIEvent}
*/
fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.kt
new file mode 100644
index 000000000000..069fd9b062a6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUIRequest.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.compatui.api
+
+/**
+ * Abstraction for all the possible Compat UI Component requests.
+ */
+interface CompatUIRequest {
+ /**
+ * Unique request identifier
+ */
+ val requestId: Int
+
+ @Suppress("UNCHECKED_CAST")
+ fun <T : CompatUIRequest> asType(): T? = this as? T
+
+ fun <T : CompatUIRequest> asType(clazz: Class<T>): T? {
+ return if (clazz.isInstance(this)) clazz.cast(this) else null
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt
new file mode 100644
index 000000000000..da4fc99491dc
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/CompatUIRequests.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.compatui.impl
+
+import android.app.TaskInfo
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.compatui.api.CompatUIRequest
+
+internal const val DISPLAY_COMPAT_SHOW_RESTART_DIALOG = 0
+
+/**
+ * All the {@link CompatUIRequest} the Compat UI Framework can handle
+ */
+sealed class CompatUIRequests(override val requestId: Int) : CompatUIRequest {
+ /** Sent when the restart handle menu is clicked, and a restart dialog is requested. */
+ data class DisplayCompatShowRestartDialog(val taskInfo: TaskInfo,
+ val taskListener: ShellTaskOrganizer.TaskListener) :
+ CompatUIRequests(DISPLAY_COMPAT_SHOW_RESTART_DIALOG)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
index 02db85a4f99d..7dcb16c10097 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/impl/DefaultCompatUIHandler.kt
@@ -23,6 +23,7 @@ import com.android.wm.shell.compatui.api.CompatUIEvent
import com.android.wm.shell.compatui.api.CompatUIHandler
import com.android.wm.shell.compatui.api.CompatUIInfo
import com.android.wm.shell.compatui.api.CompatUIRepository
+import com.android.wm.shell.compatui.api.CompatUIRequest
import com.android.wm.shell.compatui.api.CompatUIState
import java.util.function.Consumer
@@ -102,4 +103,6 @@ class DefaultCompatUIHandler(
override fun setCallback(compatUIEventSender: Consumer<CompatUIEvent>?) {
this.compatUIEventSender = compatUIEventSender
}
+
+ override fun sendCompatUIRequest(compatUIRequest: CompatUIRequest) {}
}
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 d230425680ce..bc2ed3f35b45 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
@@ -171,6 +171,7 @@ import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel;
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer;
+import com.android.wm.shell.windowdecor.common.AppHandleAndHeaderVisibilityHelper;
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader;
import com.android.wm.shell.windowdecor.common.viewhost.DefaultWindowDecorViewHostSupplier;
import com.android.wm.shell.windowdecor.common.viewhost.PooledWindowDecorViewHostSupplier;
@@ -758,6 +759,7 @@ public abstract class WMShellModule {
ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
DragToDesktopTransitionHandler dragToDesktopTransitionHandler,
@DynamicOverride DesktopUserRepositories desktopUserRepositories,
+ DesktopRepositoryInitializer desktopRepositoryInitializer,
Optional<DesktopImmersiveController> desktopImmersiveController,
DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
LaunchAdjacentController launchAdjacentController,
@@ -804,6 +806,7 @@ public abstract class WMShellModule {
dragToDesktopTransitionHandler,
desktopImmersiveController.get(),
desktopUserRepositories,
+ desktopRepositoryInitializer,
recentsTransitionHandler,
multiInstanceHelper,
mainExecutor,
@@ -1021,6 +1024,7 @@ public abstract class WMShellModule {
Optional<DesktopTasksLimiter> desktopTasksLimiter,
AppHandleEducationController appHandleEducationController,
AppToWebEducationController appToWebEducationController,
+ AppHandleAndHeaderVisibilityHelper appHandleAndHeaderVisibilityHelper,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
FocusTransitionObserver focusTransitionObserver,
@@ -1044,10 +1048,10 @@ public abstract class WMShellModule {
rootTaskDisplayAreaOrganizer, interactionJankMonitor, genericLinksParser,
assistContentRequester, windowDecorViewHostSupplier, multiInstanceHelper,
desktopTasksLimiter, appHandleEducationController, appToWebEducationController,
- windowDecorCaptionHandleRepository, activityOrientationChangeHandler,
- focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger,
- taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy,
- desktopTilingDecorViewModel,
+ appHandleAndHeaderVisibilityHelper, windowDecorCaptionHandleRepository,
+ activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger,
+ desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler,
+ desktopModeCompatPolicy, desktopTilingDecorViewModel,
multiDisplayDragMoveIndicatorController));
}
@@ -1075,6 +1079,16 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
+ static AppHandleAndHeaderVisibilityHelper provideAppHandleAndHeaderVisibilityHelper(
+ @NonNull Context context,
+ @NonNull DisplayController displayController,
+ @NonNull DesktopModeCompatPolicy desktopModeCompatPolicy) {
+ return new AppHandleAndHeaderVisibilityHelper(context, displayController,
+ desktopModeCompatPolicy);
+ }
+
+ @WMSingleton
+ @Provides
static WindowDecorTaskResourceLoader provideWindowDecorTaskResourceLoader(
@NonNull Context context, @NonNull ShellInit shellInit,
@NonNull ShellController shellController,
@@ -1235,7 +1249,7 @@ public abstract class WMShellModule {
@DynamicOverride DesktopUserRepositories desktopUserRepositories,
@NonNull DesksOrganizer desksOrganizer
) {
- if (DesktopModeStatus.canEnterDesktopMode(context)) {
+ if (DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) {
return Optional.of(
new DesksTransitionObserver(desktopUserRepositories, desksOrganizer));
}
@@ -1301,10 +1315,12 @@ public abstract class WMShellModule {
static Optional<DesktopDisplayEventHandler> provideDesktopDisplayEventHandler(
Context context,
ShellInit shellInit,
+ @ShellMainThread CoroutineScope mainScope,
DisplayController displayController,
Optional<DesktopUserRepositories> desktopUserRepositories,
Optional<DesktopTasksController> desktopTasksController,
- Optional<DesktopDisplayModeController> desktopDisplayModeController
+ Optional<DesktopDisplayModeController> desktopDisplayModeController,
+ DesktopRepositoryInitializer desktopRepositoryInitializer
) {
if (!DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.empty();
@@ -1313,7 +1329,9 @@ public abstract class WMShellModule {
new DesktopDisplayEventHandler(
context,
shellInit,
+ mainScope,
displayController,
+ desktopRepositoryInitializer,
desktopUserRepositories.get(),
desktopTasksController.get(),
desktopDisplayModeController.get()));
@@ -1454,7 +1472,9 @@ public abstract class WMShellModule {
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
IWindowManager windowManager,
ShellTaskOrganizer shellTaskOrganizer,
- DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider
+ DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider,
+ InputManager inputManager,
+ @ShellMainThread Handler mainHandler
) {
if (!DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.empty();
@@ -1466,7 +1486,9 @@ public abstract class WMShellModule {
rootTaskDisplayAreaOrganizer,
windowManager,
shellTaskOrganizer,
- desktopWallpaperActivityTokenProvider));
+ desktopWallpaperActivityTokenProvider,
+ inputManager,
+ mainHandler));
}
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
index afc48acad4f5..683b74392fa6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt
@@ -23,15 +23,21 @@ import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener
+import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
/** Handles display events in desktop mode */
class DesktopDisplayEventHandler(
private val context: Context,
shellInit: ShellInit,
+ private val mainScope: CoroutineScope,
private val displayController: DisplayController,
+ private val desktopRepositoryInitializer: DesktopRepositoryInitializer,
private val desktopUserRepositories: DesktopUserRepositories,
private val desktopTasksController: DesktopTasksController,
private val desktopDisplayModeController: DesktopDisplayModeController,
@@ -61,15 +67,19 @@ class DesktopDisplayEventHandler(
logV("Display #$displayId does not support desks")
return
}
- logV("Creating new desk in new display#$displayId")
- // TODO: b/362720497 - when SystemUI crashes with a freeform task open for any reason, the
- // task is recreated and received in [FreeformTaskListener] before this display callback
- // is invoked, which results in the repository trying to add the task to a desk before the
- // desk has been recreated here, which may result in a crash-loop if the repository is
- // checking that the desk exists before adding a task to it. See b/391984373.
- desktopTasksController.createDesk(displayId)
- // TODO: b/393978539 - consider activating the desk on creation when applicable, such as
- // for connected displays.
+
+ mainScope.launch {
+ desktopRepositoryInitializer.isInitialized.collect { initialized ->
+ if (!initialized) return@collect
+ if (desktopRepository.getNumberOfDesks(displayId) == 0) {
+ logV("Creating new desk in new display#$displayId")
+ // TODO: b/393978539 - consider activating the desk on creation when
+ // applicable, such as for connected displays.
+ desktopTasksController.createDesk(displayId)
+ }
+ cancel()
+ }
+ }
}
override fun onDisplayRemoved(displayId: Int) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
index e89aafe267ed..904d86282c39 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayModeController.kt
@@ -22,6 +22,8 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.app.WindowConfiguration.windowingModeToString
import android.content.Context
+import android.hardware.input.InputManager
+import android.os.Handler
import android.provider.Settings
import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
import android.view.Display.DEFAULT_DISPLAY
@@ -29,11 +31,13 @@ import android.view.IWindowManager
import android.view.WindowManager.TRANSIT_CHANGE
import android.window.DesktopExperienceFlags
import android.window.WindowContainerTransaction
+import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.transition.Transitions
/** Controls the display windowing mode in desktop mode */
@@ -44,8 +48,26 @@ class DesktopDisplayModeController(
private val windowManager: IWindowManager,
private val shellTaskOrganizer: ShellTaskOrganizer,
private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider,
+ private val inputManager: InputManager,
+ @ShellMainThread private val mainHandler: Handler,
) {
+ private val onTabletModeChangedListener =
+ object : InputManager.OnTabletModeChangedListener {
+ override fun onTabletModeChanged(whenNanos: Long, inTabletMode: Boolean) {
+ refreshDisplayWindowingMode()
+ }
+ }
+
+ init {
+ if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) {
+ inputManager.registerOnTabletModeChangedListener(
+ onTabletModeChangedListener,
+ mainHandler,
+ )
+ }
+ }
+
fun refreshDisplayWindowingMode() {
if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return
@@ -89,10 +111,20 @@ class DesktopDisplayModeController(
transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
}
- private fun getTargetWindowingModeForDefaultDisplay(): Int {
+ @VisibleForTesting
+ fun getTargetWindowingModeForDefaultDisplay(): Int {
if (isExtendedDisplayEnabled() && hasExternalDisplay()) {
return WINDOWING_MODE_FREEFORM
}
+ if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) {
+ if (isInClamshellMode()) {
+ return WINDOWING_MODE_FREEFORM
+ }
+ return WINDOWING_MODE_FULLSCREEN
+ }
+
+ // If form factor-based desktop first switch is disabled, use the default display windowing
+ // mode here to keep the freeform mode for some form factors (e.g., FEATURE_PC).
return windowManager.getWindowingMode(DEFAULT_DISPLAY)
}
@@ -108,6 +140,8 @@ class DesktopDisplayModeController(
private fun hasExternalDisplay() =
rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY }
+ private fun isInClamshellMode() = inputManager.isInTabletMode() == InputManager.SWITCH_STATE_OFF
+
private fun logV(msg: String, vararg arguments: Any?) {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
index 1f7edb413908..4646662073e6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -452,6 +452,11 @@ class DesktopMixedTransitionHandler(
private fun findTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? =
info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId }
+ private fun findLaunchChange(info: TransitionInfo): TransitionInfo.Change? =
+ info.changes.firstOrNull { change ->
+ change.mode == TRANSIT_OPEN && change.taskInfo != null && change.taskInfo!!.isFreeform
+ }
+
private fun findDesktopTaskLaunchChange(
info: TransitionInfo,
launchTaskId: Int?,
@@ -459,14 +464,18 @@ class DesktopMixedTransitionHandler(
return if (launchTaskId != null) {
// Launching a known task (probably from background or moving to front), so
// specifically look for it.
- findTaskChange(info, launchTaskId)
+ val launchChange = findTaskChange(info, launchTaskId)
+ if (
+ DesktopModeFlags.ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX.isTrue &&
+ launchChange == null
+ ) {
+ findLaunchChange(info)
+ } else {
+ launchChange
+ }
} else {
// Launching a new task, so the first opening freeform task.
- info.changes.firstOrNull { change ->
- change.mode == TRANSIT_OPEN &&
- change.taskInfo != null &&
- change.taskInfo!!.isFreeform
- }
+ findLaunchChange(info)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
index 5269318943d9..1ea545f3ab67 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandler.kt
@@ -56,7 +56,6 @@ class DesktopModeKeyGestureHandler(
override fun handleKeyGestureEvent(event: KeyGestureEvent, focusedToken: IBinder?): Boolean {
if (
- !isKeyGestureSupported(event.keyGestureType) ||
!desktopTasksController.isPresent ||
!desktopModeWindowDecorViewModel.isPresent
) {
@@ -136,19 +135,6 @@ class DesktopModeKeyGestureHandler(
}
}
- override fun isKeyGestureSupported(gestureType: Int): Boolean =
- when (gestureType) {
- KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY ->
- enableMoveToNextDisplayShortcut()
- KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW,
- KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW,
- KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW,
- KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW ->
- DesktopModeFlags.ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS.isTrue &&
- manageKeyGestures()
- else -> false
- }
-
// TODO: b/364154795 - wait for the completion of moveToNextDisplay transition, otherwise it
// will pick a wrong task when a user quickly perform other actions with keyboard shortcuts
// after moveToNextDisplay, and move this to FocusTransitionObserver class.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
index c5ee3137e5ba..fa98d0339a65 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -22,6 +22,13 @@ import android.annotation.DimenRes
import android.app.ActivityManager.RunningTaskInfo
import android.app.TaskInfo
import android.content.Context
+import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
+import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+import android.content.pm.ActivityInfo.LAUNCH_MULTIPLE
+import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE
+import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK
+import android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK
import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
import android.content.pm.ActivityInfo.isFixedOrientationLandscape
import android.content.pm.ActivityInfo.isFixedOrientationPortrait
@@ -30,7 +37,9 @@ import android.content.res.Configuration.ORIENTATION_PORTRAIT
import android.graphics.Rect
import android.os.SystemProperties
import android.util.Size
+import android.window.DesktopModeFlags
import com.android.wm.shell.R
+import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
import kotlin.math.ceil
@@ -264,6 +273,58 @@ fun getAppHeaderHeight(context: Context): Int =
@DimenRes fun getAppHeaderHeightId(): Int = R.dimen.desktop_mode_freeform_decor_caption_height
/**
+ * Returns the task bounds a launching task should inherit from an existing running instance.
+ * Returns null if there are no bounds to inherit.
+ */
+fun getInheritedExistingTaskBounds(
+ taskRepository: DesktopRepository,
+ shellTaskOrganizer: ShellTaskOrganizer,
+ task: RunningTaskInfo,
+ deskId: Int,
+): Rect? {
+ if (!DesktopModeFlags.INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES.isTrue) return null
+ val activeTask = taskRepository.getExpandedTasksIdsInDeskOrdered(deskId).firstOrNull()
+ if (activeTask == null) return null
+ val lastTask = shellTaskOrganizer.getRunningTaskInfo(activeTask)
+ val lastTaskTopActivity = lastTask?.topActivity
+ val currentTaskTopActivity = task.topActivity
+ val intentFlags = task.baseIntent.flags
+ val launchMode = task.topActivityInfo?.launchMode ?: LAUNCH_MULTIPLE
+ return when {
+ // No running task activity to inherit bounds from.
+ lastTaskTopActivity == null -> null
+ // No current top activity to set bounds for.
+ currentTaskTopActivity == null -> null
+ // Top task is not an instance of the launching activity, do not inherit its bounds.
+ lastTaskTopActivity.packageName != currentTaskTopActivity.packageName -> null
+ // Top task is an instance of launching activity. Activity will be launching in a new
+ // task with the existing task also being closed. Inherit existing task bounds to
+ // prevent new task jumping.
+ (isLaunchingNewTask(launchMode, intentFlags) && isClosingExitingInstance(intentFlags)) ->
+ lastTask.configuration.windowConfiguration.bounds
+ else -> null
+ }
+}
+
+/**
+ * Returns true if the launch mode or intent will result in a new task being created for the
+ * activity.
+ */
+private fun isLaunchingNewTask(launchMode: Int, intentFlags: Int) =
+ launchMode == LAUNCH_SINGLE_TASK ||
+ launchMode == LAUNCH_SINGLE_INSTANCE ||
+ launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK ||
+ (intentFlags and FLAG_ACTIVITY_NEW_TASK) != 0
+
+/**
+ * Returns true if the intent will result in an existing task instance being closed if a new one
+ * appears.
+ */
+private fun isClosingExitingInstance(intentFlags: Int) =
+ (intentFlags and FLAG_ACTIVITY_CLEAR_TASK) != 0 ||
+ (intentFlags and FLAG_ACTIVITY_MULTIPLE_TASK) == 0
+
+/**
* Calculates the desired initial bounds for applications in desktop windowing. This is done as a
* scale of the screen bounds.
*/
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 70539902f651..b3b4d59090e8 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
@@ -32,6 +32,7 @@ import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
+import android.view.Display;
import android.view.SurfaceControl;
import android.window.DesktopModeFlags;
@@ -114,6 +115,7 @@ public class DesktopModeVisualIndicator {
private final Context mContext;
private final DisplayController mDisplayController;
private final ActivityManager.RunningTaskInfo mTaskInfo;
+ private final Display mDisplay;
private IndicatorType mCurrentType;
private final DragStartState mDragStartState;
@@ -145,9 +147,10 @@ public class DesktopModeVisualIndicator {
mCurrentType = NO_INDICATOR;
mDragStartState = dragStartState;
mSnapEventHandler = snapEventHandler;
+ mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId);
mVisualIndicatorViewContainer.createView(
mContext,
- mDisplayController.getDisplay(mTaskInfo.displayId),
+ mDisplay,
mDisplayController.getDisplayLayout(mTaskInfo.displayId),
mTaskInfo,
taskSurface
@@ -175,6 +178,7 @@ public class DesktopModeVisualIndicator {
/** Start the fade-in animation. */
void fadeInIndicator() {
+ if (mCurrentType == NO_INDICATOR) return;
mVisualIndicatorViewContainer.fadeInIndicator(
mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType,
mTaskInfo.displayId);
@@ -193,7 +197,7 @@ public class DesktopModeVisualIndicator {
if (inputCoordinates.x > layout.width()) return TO_SPLIT_RIGHT_INDICATOR;
IndicatorType result;
if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()
- && !DesktopModeStatus.canEnterDesktopMode(mContext)) {
+ && !DesktopModeStatus.isDesktopModeSupportedOnDisplay(mContext, mDisplay)) {
// If desktop is not available, default to "no indicator"
result = NO_INDICATOR;
} else {
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 f64bd757de3b..8636bc1f56c2 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
@@ -716,12 +716,15 @@ class DesktopRepository(
}
/**
- * Returns the top transparent fullscreen task id for a given display's active desk, or null.
+ * Returns the top transparent fullscreen task id for a given display, or null.
*
* TODO: b/389960283 - add explicit [deskId] argument.
*/
fun getTopTransparentFullscreenTaskId(displayId: Int): Int? =
- desktopData.getActiveDesk(displayId)?.topTransparentFullscreenTaskId
+ desktopData
+ .desksSequence(displayId)
+ .mapNotNull { it.topTransparentFullscreenTaskId }
+ .firstOrNull()
/**
* Clears the top transparent fullscreen task id info for a given display's active desk.
@@ -818,7 +821,6 @@ class DesktopRepository(
}
/** Minimizes the task in its desk. */
- @VisibleForTesting
fun minimizeTaskInDesk(displayId: Int, deskId: Int, taskId: Int) {
logD("MinimizeTaskInDesk: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId)
desktopData.getDesk(deskId)?.minimizedTasks?.add(taskId)
@@ -933,6 +935,12 @@ class DesktopRepository(
listener.onDeskRemoved(displayId = desk.displayId, deskId = desk.deskId)
}
}
+ if (
+ DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue &&
+ DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue
+ ) {
+ removeDeskFromPersistentRepository(desk)
+ }
return activeTasks
}
@@ -1031,6 +1039,24 @@ class DesktopRepository(
}
}
+ private fun removeDeskFromPersistentRepository(desk: Desk) {
+ mainCoroutineScope.launch {
+ try {
+ logD(
+ "updatePersistentRepositoryForRemovedDesk user=%d desk=%d",
+ userId,
+ desk.deskId,
+ )
+ persistentRepository.removeDesktop(userId = userId, desktopId = desk.deskId)
+ } catch (throwable: Throwable) {
+ logE(
+ "An exception occurred while updating the persistent repository \n%s",
+ throwable.stackTrace,
+ )
+ }
+ }
+ }
+
internal fun dump(pw: PrintWriter, prefix: String) {
val innerPrefix = "$prefix "
pw.println("${prefix}DesktopRepository")
@@ -1049,6 +1075,7 @@ class DesktopRepository(
}
.forEach { (displayId, activeDeskId, desks) ->
pw.println("${prefix}Display #$displayId:")
+ pw.println("${innerPrefix}numOfDesks=${desks.size}")
pw.println("${innerPrefix}activeDesk=$activeDeskId")
pw.println("${innerPrefix}desks:")
val desksPrefix = "$innerPrefix "
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 99606d04ff3b..d0356d55035d 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
@@ -32,6 +32,8 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.app.WindowConfiguration.WindowingMode
import android.content.Context
import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
@@ -113,6 +115,9 @@ import com.android.wm.shell.desktopmode.multidesks.DeskTransition
import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer
import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver
import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener
+import com.android.wm.shell.desktopmode.multidesks.createDesk
+import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
+import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory
import com.android.wm.shell.draganddrop.DragAndDropController
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
@@ -192,6 +197,7 @@ class DesktopTasksController(
private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
private val desktopImmersiveController: DesktopImmersiveController,
private val userRepositories: DesktopUserRepositories,
+ desktopRepositoryInitializer: DesktopRepositoryInitializer,
private val recentsTransitionHandler: RecentsTransitionHandler,
private val multiInstanceHelper: MultiInstanceHelper,
@ShellMainThread private val mainExecutor: ShellExecutor,
@@ -238,6 +244,10 @@ class DesktopTasksController(
removeVisualIndicator()
}
+ override fun onTransitionInterrupted() {
+ removeVisualIndicator()
+ }
+
private fun removeVisualIndicator() {
visualIndicator?.fadeOutIndicator { releaseVisualIndicator() }
}
@@ -270,6 +280,19 @@ class DesktopTasksController(
}
userId = ActivityManager.getCurrentUser()
taskRepository = userRepositories.getProfile(userId)
+
+ if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
+ desktopRepositoryInitializer.deskRecreationFactory =
+ DeskRecreationFactory { deskUserId, destinationDisplayId, deskId ->
+ if (deskUserId != userId) {
+ // TODO: b/400984250 - add multi-user support for multi-desk restoration.
+ logW("Tried to recreated desk of another user.")
+ deskId
+ } else {
+ desksOrganizer.createDesk(destinationDisplayId)
+ }
+ }
+ }
}
private fun onInit() {
@@ -1712,15 +1735,7 @@ class DesktopTasksController(
wct.reorder(runningTaskInfo.token, /* onTop= */ true)
} else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
// Task is not running, start it
- wct.startTask(
- taskId,
- ActivityOptions.makeBasic()
- .apply {
- launchWindowingMode = WINDOWING_MODE_FREEFORM
- splashScreenStyle = SPLASH_SCREEN_STYLE_ICON
- }
- .toBundle(),
- )
+ wct.startTask(taskId, createActivityOptionsForStartTask().toBundle())
}
}
@@ -2280,11 +2295,19 @@ class DesktopTasksController(
wct.reorder(task.token, true)
return wct
}
+ val inheritedTaskBounds =
+ getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId)
+ if (!taskRepository.isActiveTask(task.taskId) && inheritedTaskBounds != null) {
+ // Inherit bounds from closing task instance to prevent application jumping different
+ // cascading positions.
+ wct.setBounds(task.token, inheritedTaskBounds)
+ }
// TODO(b/365723620): Handle non running tasks that were launched after reboot.
// If task is already visible, it must have been handled already and added to desktop mode.
- // Cascade task only if it's not visible yet.
+ // Cascade task only if it's not visible yet and has no inherited bounds.
if (
- DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() &&
+ inheritedTaskBounds == null &&
+ DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() &&
!taskRepository.isVisibleTask(task.taskId)
) {
val displayLayout = displayController.getDisplayLayout(task.displayId)
@@ -2520,9 +2543,17 @@ class DesktopTasksController(
) {
val targetDisplayId = taskRepository.getDisplayForDesk(deskId)
val displayLayout = displayController.getDisplayLayout(targetDisplayId) ?: return
- val initialBounds = getInitialBounds(displayLayout, task, targetDisplayId)
- if (canChangeTaskPosition(task)) {
- wct.setBounds(task.token, initialBounds)
+ val inheritedTaskBounds =
+ getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId)
+ if (inheritedTaskBounds != null) {
+ // Inherit bounds from closing task instance to prevent application jumping different
+ // cascading positions.
+ wct.setBounds(task.token, inheritedTaskBounds)
+ } else {
+ val initialBounds = getInitialBounds(displayLayout, task, targetDisplayId)
+ if (canChangeTaskPosition(task)) {
+ wct.setBounds(task.token, initialBounds)
+ }
}
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksOrganizer.moveTaskToDesk(wct = wct, deskId = deskId, task = task)
@@ -2804,9 +2835,6 @@ class DesktopTasksController(
}
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)
)
@@ -2824,6 +2852,19 @@ class DesktopTasksController(
desksOrganizer.minimizeTask(wct, deskId, taskToMinimize)
}
}
+ if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) {
+ expandedTasksOrderedFrontToBack
+ .filter { taskId -> taskId != taskIdToMinimize }
+ .reversed()
+ .forEach { taskId ->
+ val runningTaskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId)
+ if (runningTaskInfo == null) {
+ wct.startTask(taskId, createActivityOptionsForStartTask().toBundle())
+ } else {
+ desksOrganizer.reorderTaskToFront(wct, deskId, runningTaskInfo)
+ }
+ }
+ }
return { transition ->
val activateDeskTransition =
if (newTaskIdInFront != null) {
@@ -3475,6 +3516,13 @@ class DesktopTasksController(
}
}
+ private fun createActivityOptionsForStartTask(): ActivityOptions {
+ return ActivityOptions.makeBasic().apply {
+ launchWindowingMode = WINDOWING_MODE_FREEFORM
+ splashScreenStyle = SPLASH_SCREEN_STYLE_ICON
+ }
+ }
+
private fun dump(pw: PrintWriter, prefix: String) {
val innerPrefix = "$prefix "
pw.println("${prefix}DesktopTasksController")
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 4f511a901756..24b2e4879546 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
@@ -24,8 +24,10 @@ import android.os.SystemClock
import android.os.SystemProperties
import android.os.UserHandle
import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_CLOSE
import android.window.DesktopModeFlags
+import android.window.DesktopModeFlags.ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX
import android.window.TransitionInfo
import android.window.TransitionInfo.Change
import android.window.TransitionRequestInfo
@@ -185,18 +187,29 @@ sealed class DragToDesktopTransitionHandler(
*/
fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? {
if (!inProgress) {
+ logV("finishDragToDesktop: not in progress, returning")
// Don't attempt to finish a drag to desktop transition since there is no transition in
// progress which means that the drag to desktop transition was never successfully
// started.
return null
}
- if (requireTransitionState().startAborted) {
+ val state = requireTransitionState()
+ if (state.startAborted) {
+ logV("finishDragToDesktop: start was aborted, clearing state")
// Don't attempt to complete the drag-to-desktop since the start transition didn't
// succeed as expected. Just reset the state as if nothing happened.
clearState()
return null
}
- return transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this)
+ if (state.startInterrupted) {
+ logV("finishDragToDesktop: start was interrupted, returning")
+ // We should only have interrupted the start transition after receiving a cancel/end
+ // request, let that existing request play out and just return here.
+ return null
+ }
+ state.endTransitionToken =
+ transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this)
+ return state.endTransitionToken
}
/**
@@ -220,6 +233,11 @@ sealed class DragToDesktopTransitionHandler(
clearState()
return
}
+ if (state.startInterrupted) {
+ // We should only have interrupted the start transition after receiving a cancel/end
+ // request, let that existing request play out and just return here.
+ return
+ }
state.cancelState = cancelState
if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) {
@@ -227,7 +245,7 @@ sealed class DragToDesktopTransitionHandler(
// transient to start and merge. Animate the cancellation (scale back to original
// bounds) first before actually starting the cancel transition so that the wallpaper
// is visible behind the animating task.
- startCancelAnimation()
+ state.activeCancelAnimation = startCancelAnimation()
} else if (
state.draggedTaskChange != null &&
(cancelState == CancelState.CANCEL_SPLIT_LEFT ||
@@ -255,7 +273,7 @@ sealed class DragToDesktopTransitionHandler(
) {
if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) {
// TODO(b/388853233): add support for dragging split task to bubble
- startCancelAnimation()
+ state.activeCancelAnimation = startCancelAnimation()
} else {
// Animation is handled by BubbleController
val wct = WindowContainerTransaction()
@@ -357,6 +375,19 @@ sealed class DragToDesktopTransitionHandler(
): Boolean {
val state = requireTransitionState()
+ if (
+ handleCancelOrExitAfterInterrupt(
+ transition,
+ info,
+ startTransaction,
+ finishTransaction,
+ finishCallback,
+ state,
+ )
+ ) {
+ return true
+ }
+
val isStartDragToDesktop =
info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP &&
transition == state.startTransitionToken
@@ -539,6 +570,58 @@ sealed class DragToDesktopTransitionHandler(
}
}
+ private fun handleCancelOrExitAfterInterrupt(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: Transaction,
+ finishTransaction: Transaction,
+ finishCallback: Transitions.TransitionFinishCallback,
+ state: TransitionState,
+ ): Boolean {
+ if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) {
+ return false
+ }
+ val isCancelDragToDesktop =
+ info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP &&
+ transition == state.cancelTransitionToken
+ val isEndDragToDesktop =
+ info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP &&
+ transition == state.endTransitionToken
+ // We should only receive cancel or end transitions through startAnimation() if the
+ // start transition was interrupted while a cancel- or end-transition had already
+ // been requested. Finish the cancel/end transition to avoid having to deal with more
+ // incoming transitions, and clear the state for the next start-drag transition.
+ if (!isCancelDragToDesktop && !isEndDragToDesktop) {
+ return false
+ }
+ if (!state.startInterrupted) {
+ logW(
+ "Not interrupted, but received startAnimation for cancel/end drag." +
+ "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop"
+ )
+ return false
+ }
+ logV(
+ "startAnimation: interrupted -> " +
+ "isCancel=$isCancelDragToDesktop, isEnd=$isEndDragToDesktop"
+ )
+ if (isEndDragToDesktop) {
+ setupEndDragToDesktop(info, startTransaction, finishTransaction)
+ animateEndDragToDesktop(startTransaction = startTransaction, finishCallback)
+ } else { // isCancelDragToDesktop
+ // Similar to when we merge the cancel transition: ensure all tasks involved in the
+ // cancel transition are shown, and finish the transition immediately.
+ info.changes.forEach { change ->
+ startTransaction.show(change.leash)
+ finishTransaction.show(change.leash)
+ }
+ }
+ startTransaction.apply()
+ finishCallback.onTransitionFinished(/* wct= */ null)
+ clearState()
+ return true
+ }
+
/**
* Calculates start drag to desktop layers for transition [info]. The leash layer is calculated
* based on its change position in the transition, e.g. `appLayer = appLayers - i`, where i is
@@ -590,6 +673,7 @@ sealed class DragToDesktopTransitionHandler(
?: error("Start transition expected to be waiting for merge but wasn't")
if (isEndTransition) {
logV("mergeAnimation: end-transition, target=$mergeTarget")
+ state.mergedEndTransition = true
setupEndDragToDesktop(
info,
startTransaction = startT,
@@ -617,6 +701,41 @@ sealed class DragToDesktopTransitionHandler(
return
}
logW("unhandled merge transition: transitionInfo=$info")
+ // Handle unknown incoming transitions by finishing the start transition. For now, only do
+ // this if we've already requested a cancel- or end transition. If we've already merged the
+ // end-transition, or if the end-transition is running on its own, then just wait until that
+ // finishes instead. If we've merged the cancel-transition we've finished the
+ // start-transition and won't reach this code.
+ if (
+ mergeTarget == state.startTransitionToken &&
+ isCancelOrEndTransitionRequested(state) &&
+ !state.mergedEndTransition
+ ) {
+ interruptStartTransition(state)
+ }
+ }
+
+ private fun isCancelOrEndTransitionRequested(state: TransitionState): Boolean =
+ state.cancelTransitionToken != null || state.endTransitionToken != null
+
+ private fun interruptStartTransition(state: TransitionState) {
+ if (!ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX.isTrue) {
+ return
+ }
+ logV("interruptStartTransition")
+ state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null)
+ state.dragAnimator.cancelAnimator()
+ state.activeCancelAnimation?.removeAllListeners()
+ state.activeCancelAnimation?.cancel()
+ state.activeCancelAnimation = null
+ // Keep the transition state so we can deal with Cancel/End properly in #startAnimation.
+ state.startInterrupted = true
+ dragToDesktopStateListener?.onTransitionInterrupted()
+ // Cancel CUJs here as they won't be accurate now that an incoming transition is playing.
+ interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
+ interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)
+ LatencyTracker.getInstance(context)
+ .onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG)
}
protected open fun setupEndDragToDesktop(
@@ -783,7 +902,7 @@ sealed class DragToDesktopTransitionHandler(
} ?: false
}
- private fun startCancelAnimation() {
+ private fun startCancelAnimation(): Animator {
val state = requireTransitionState()
val dragToDesktopAnimator = state.dragAnimator
@@ -800,7 +919,7 @@ sealed class DragToDesktopTransitionHandler(
val dx = targetX - x
val dy = targetY - y
val tx: SurfaceControl.Transaction = transactionSupplier.get()
- ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f)
+ return ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f)
.setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
.apply {
addUpdateListener { animator ->
@@ -818,6 +937,7 @@ sealed class DragToDesktopTransitionHandler(
addListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
+ state.activeCancelAnimation = null
dragToDesktopStateListener?.onCancelToDesktopAnimationEnd()
// Start the cancel transition to restore order.
startCancelDragToDesktopTransition()
@@ -910,10 +1030,16 @@ sealed class DragToDesktopTransitionHandler(
val dragLayer: Int,
)
+ /** Listener for various events happening during the DragToDesktop transition. */
interface DragToDesktopStateListener {
+ /** Indicates that the animation into Desktop has started. */
fun onCommitToDesktopAnimationStart()
+ /** Called when the animation to cancel the desktop-drag has finished. */
fun onCancelToDesktopAnimationEnd()
+
+ /** Indicates that the drag-to-desktop transition has been interrupted. */
+ fun onTransitionInterrupted()
}
sealed class TransitionState {
@@ -930,6 +1056,10 @@ sealed class DragToDesktopTransitionHandler(
abstract var cancelState: CancelState
abstract var startAborted: Boolean
abstract val visualIndicator: DesktopModeVisualIndicator?
+ abstract var startInterrupted: Boolean
+ abstract var endTransitionToken: IBinder?
+ abstract var mergedEndTransition: Boolean
+ abstract var activeCancelAnimation: Animator?
data class FromFullscreen(
override val draggedTaskId: Int,
@@ -945,6 +1075,10 @@ sealed class DragToDesktopTransitionHandler(
override var cancelState: CancelState = CancelState.NO_CANCEL,
override var startAborted: Boolean = false,
override val visualIndicator: DesktopModeVisualIndicator?,
+ override var startInterrupted: Boolean = false,
+ override var endTransitionToken: IBinder? = null,
+ override var mergedEndTransition: Boolean = false,
+ override var activeCancelAnimation: Animator? = null,
var otherRootChanges: MutableList<Change> = mutableListOf(),
) : TransitionState()
@@ -962,6 +1096,10 @@ sealed class DragToDesktopTransitionHandler(
override var cancelState: CancelState = CancelState.NO_CANCEL,
override var startAborted: Boolean = false,
override val visualIndicator: DesktopModeVisualIndicator?,
+ override var startInterrupted: Boolean = false,
+ override var endTransitionToken: IBinder? = null,
+ override var mergedEndTransition: Boolean = false,
+ override var activeCancelAnimation: Animator? = null,
var splitRootChange: Change? = null,
var otherSplitTask: Int,
) : TransitionState()
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 8c4bc2598dff..5a988fcd1b77 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt
@@ -18,6 +18,8 @@ package com.android.wm.shell.desktopmode.multidesks
import android.app.ActivityManager
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
+import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback
+import kotlin.coroutines.suspendCoroutine
/** An organizer of desk containers in which to host child desktop windows. */
interface DesksOrganizer {
@@ -82,3 +84,9 @@ interface DesksOrganizer {
fun onCreated(deskId: Int)
}
}
+
+/** Creates a new desk container in the given display. */
+suspend fun DesksOrganizer.createDesk(displayId: Int): Int = suspendCoroutine { cont ->
+ val onCreateCallback = OnCreateCallback { deskId -> cont.resumeWith(Result.success(deskId)) }
+ createDesk(displayId, onCreateCallback)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
index 1566544f5303..f71eacab518d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt
@@ -132,10 +132,7 @@ class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersis
.toBuilder()
.putDesktopRepoByUser(
userId,
- currentRepository
- .toBuilder()
- .putDesktop(desktopId, desktop.build())
- .build(),
+ currentRepository.toBuilder().putDesktop(desktopId, desktop.build()).build(),
)
.build()
}
@@ -149,6 +146,33 @@ class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersis
}
}
+ /** Removes the desktop from the persistent repository. */
+ suspend fun removeDesktop(userId: Int, desktopId: Int) {
+ try {
+ dataStore.updateData { persistentRepositories: DesktopPersistentRepositories ->
+ val currentRepository =
+ persistentRepositories.getDesktopRepoByUserOrDefault(
+ userId,
+ DesktopRepositoryState.getDefaultInstance(),
+ )
+ persistentRepositories
+ .toBuilder()
+ .putDesktopRepoByUser(
+ userId,
+ currentRepository.toBuilder().removeDesktop(desktopId).build(),
+ )
+ .build()
+ }
+ } catch (throwable: Throwable) {
+ Log.e(
+ TAG,
+ "Error in removing desktop related data, data is " +
+ "stored in a file named $DESKTOP_REPOSITORIES_DATASTORE_FILE",
+ throwable,
+ )
+ }
+ }
+
suspend fun removeUsers(uids: List<Int>) {
try {
dataStore.updateData { persistentRepositories: DesktopPersistentRepositories ->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt
index a26ebbf4c99a..8191181cac11 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt
@@ -17,8 +17,22 @@
package com.android.wm.shell.desktopmode.persistence
import com.android.wm.shell.desktopmode.DesktopUserRepositories
+import kotlinx.coroutines.flow.StateFlow
/** Interface for initializing the [DesktopUserRepositories]. */
-fun interface DesktopRepositoryInitializer {
+interface DesktopRepositoryInitializer {
+ /** A factory used to recreate a desk from persistence. */
+ var deskRecreationFactory: DeskRecreationFactory
+
+ /** A flow that emits true when the repository has been initialized. */
+ val isInitialized: StateFlow<Boolean>
+
+ /** Initialize the user repositories from a persistent data store. */
fun initialize(userRepositories: DesktopUserRepositories)
+
+ /** A factory for recreating desks. */
+ fun interface DeskRecreationFactory {
+ /** Recreates a restored desk and returns the new desk id. */
+ suspend fun recreateDesk(userId: Int, destinationDisplayId: Int, deskId: Int): Int
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
index 0507e59c06e1..49cb7391fe97 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt
@@ -17,13 +17,19 @@
package com.android.wm.shell.desktopmode.persistence
import android.content.Context
+import android.view.Display
import android.window.DesktopExperienceFlags
import android.window.DesktopModeFlags
+import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.desktopmode.DesktopRepository
import com.android.wm.shell.desktopmode.DesktopUserRepositories
+import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
/**
@@ -37,62 +43,136 @@ class DesktopRepositoryInitializerImpl(
private val persistentRepository: DesktopPersistentRepository,
@ShellMainThread private val mainCoroutineScope: CoroutineScope,
) : DesktopRepositoryInitializer {
+
+ override var deskRecreationFactory: DeskRecreationFactory = DefaultDeskRecreationFactory()
+
+ private val _isInitialized = MutableStateFlow(false)
+ override val isInitialized: StateFlow<Boolean> = _isInitialized
+
override fun initialize(userRepositories: DesktopUserRepositories) {
- if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) return
+ if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) {
+ _isInitialized.value = true
+ return
+ }
// TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized
mainCoroutineScope.launch {
- val desktopUserPersistentRepositoryMap =
- persistentRepository.getUserDesktopRepositoryMap() ?: return@launch
- for (userId in desktopUserPersistentRepositoryMap.keys) {
- val repository = userRepositories.getProfile(userId)
- val desktopRepositoryState =
- persistentRepository.getDesktopRepositoryState(userId) ?: continue
- val desktopByDesktopIdMap = desktopRepositoryState.desktopMap
- for (desktopId in desktopByDesktopIdMap.keys) {
- val persistentDesktop =
- persistentRepository.readDesktop(userId, desktopId) ?: continue
- val maxTasks =
- DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 }
- ?: persistentDesktop.zOrderedTasksCount
- var visibleTasksCount = 0
- repository.addDesk(
- displayId = persistentDesktop.displayId,
- deskId =
- if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
- persistentDesktop.desktopId
- } else {
- // When disabled, desk ids are always the display id.
- persistentDesktop.displayId
- },
+ try {
+ val desktopUserPersistentRepositoryMap =
+ persistentRepository.getUserDesktopRepositoryMap() ?: return@launch
+ for (userId in desktopUserPersistentRepositoryMap.keys) {
+ val repository = userRepositories.getProfile(userId)
+ val desktopRepositoryState =
+ persistentRepository.getDesktopRepositoryState(userId) ?: continue
+ val desksToRestore = getDesksToRestore(desktopRepositoryState, userId)
+ logV(
+ "initialize() will restore desks=%s user=%d",
+ desksToRestore.map { it.desktopId },
+ userId,
)
- persistentDesktop.zOrderedTasksList
- // Reverse it so we initialize the repo from bottom to top.
- .reversed()
- .mapNotNull { taskId -> persistentDesktop.tasksByTaskIdMap[taskId] }
- // TODO: b/362720497 - add tasks to their respective desk when multi-desk
- // persistence is implemented.
- .forEach { task ->
- if (
- task.desktopTaskState == DesktopTaskState.VISIBLE &&
- visibleTasksCount < maxTasks
- ) {
- visibleTasksCount++
- repository.addTask(
- persistentDesktop.displayId,
- task.taskId,
- isVisible = false,
- )
- } else {
- repository.addTask(
- persistentDesktop.displayId,
- task.taskId,
+ desksToRestore.forEach { persistentDesktop ->
+ val maxTasks = getTaskLimit(persistentDesktop)
+ val displayId = persistentDesktop.displayId
+ val deskId = persistentDesktop.desktopId
+ // TODO: b/401107440 - Implement desk restoration to other displays.
+ val newDisplayId = Display.DEFAULT_DISPLAY
+ val newDeskId =
+ deskRecreationFactory.recreateDesk(
+ userId = userId,
+ destinationDisplayId = newDisplayId,
+ deskId = deskId,
+ )
+ logV(
+ "Recreated desk=%d in display=%d using new deskId=%d and displayId=%d",
+ deskId,
+ displayId,
+ newDeskId,
+ newDisplayId,
+ )
+ if (newDeskId != deskId || newDisplayId != displayId) {
+ logV("Removing obsolete desk from persistence under deskId=%d", deskId)
+ persistentRepository.removeDesktop(userId, deskId)
+ }
+
+ // TODO: b/393961770 - [DesktopRepository] doesn't save desks to the
+ // persistent repository until a task is added to them. Update it so that
+ // empty desks can be restored too.
+ repository.addDesk(displayId = displayId, deskId = newDeskId)
+ var visibleTasksCount = 0
+ persistentDesktop.zOrderedTasksList
+ // Reverse it so we initialize the repo from bottom to top.
+ .reversed()
+ .mapNotNull { taskId -> persistentDesktop.tasksByTaskIdMap[taskId] }
+ .forEach { task ->
+ // Visible here means non-minimized a.k.a. expanded, it does not
+ // mean
+ // it is visible in WM (and |DesktopRepository|) terms.
+ val isVisible =
+ task.desktopTaskState == DesktopTaskState.VISIBLE &&
+ visibleTasksCount < maxTasks
+
+ repository.addTaskToDesk(
+ displayId = displayId,
+ deskId = newDeskId,
+ taskId = task.taskId,
isVisible = false,
)
- repository.minimizeTask(persistentDesktop.displayId, task.taskId)
+
+ if (isVisible) {
+ visibleTasksCount++
+ } else {
+ repository.minimizeTaskInDesk(
+ displayId = displayId,
+ deskId = newDeskId,
+ taskId = task.taskId,
+ )
+ }
}
- }
+ }
}
+ } finally {
+ _isInitialized.value = true
}
}
}
+
+ private suspend fun getDesksToRestore(
+ state: DesktopRepositoryState,
+ userId: Int,
+ ): Set<Desktop> {
+ // TODO: b/365873835 - what about desks that won't be restored?
+ // - invalid desk ids from multi-desk -> single-desk switching can be ignored / deleted.
+ val limitToSingleDeskPerDisplay =
+ !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue
+ return state.desktopMap.keys
+ .mapNotNull { deskId ->
+ persistentRepository.readDesktop(userId, deskId)?.takeIf { desk ->
+ // Do not restore invalid desks when multi-desks is disabled. This is
+ // possible if the feature is disabled after having created multiple desks.
+ val isValidSingleDesk = desk.desktopId == desk.displayId
+ (!limitToSingleDeskPerDisplay || isValidSingleDesk)
+ }
+ }
+ .toSet()
+ }
+
+ private fun getTaskLimit(persistedDesk: Desktop): Int =
+ DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 }
+ ?: persistedDesk.zOrderedTasksCount
+
+ private fun logV(msg: String, vararg arguments: Any?) {
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+ }
+
+ /** A default implementation of [DeskRecreationFactory] that reuses the desk id. */
+ private class DefaultDeskRecreationFactory : DeskRecreationFactory {
+ override suspend fun recreateDesk(
+ userId: Int,
+ destinationDisplayId: Int,
+ deskId: Int,
+ ): Int = deskId
+ }
+
+ companion object {
+ private const val TAG = "DesktopRepositoryInitializerImpl"
+ }
}
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 383afcf6f821..f81f330e50c4 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
@@ -20,6 +20,7 @@ import android.app.PictureInPictureParams;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Rect;
+import android.os.Bundle;
import android.os.SystemProperties;
import android.view.SurfaceControl;
import android.window.WindowContainerToken;
@@ -47,7 +48,7 @@ import java.util.function.Supplier;
/**
* Scheduler for Shell initiated PiP transitions and animations.
*/
-public class PipScheduler {
+public class PipScheduler implements PipTransitionState.PipTransitionStateChangedListener {
private static final String TAG = PipScheduler.class.getSimpleName();
/**
@@ -71,6 +72,7 @@ public class PipScheduler {
private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper;
@Nullable private Runnable mUpdateMovementBoundsRunnable;
+ @Nullable private PipAlphaAnimator mOverlayFadeoutAnimator;
private PipAlphaAnimatorSupplier mPipAlphaAnimatorSupplier;
private Supplier<PictureInPictureParams> mPipParamsSupplier;
@@ -85,6 +87,7 @@ public class PipScheduler {
mPipBoundsState = pipBoundsState;
mMainExecutor = mainExecutor;
mPipTransitionState = pipTransitionState;
+ mPipTransitionState.addPipTransitionStateChangedListener(this);
mPipDesktopState = pipDesktopState;
mSplitScreenControllerOptional = splitScreenControllerOptional;
@@ -238,12 +241,16 @@ public class PipScheduler {
void startOverlayFadeoutAnimation(@NonNull SurfaceControl overlayLeash,
boolean withStartDelay, @NonNull Runnable onAnimationEnd) {
- PipAlphaAnimator animator = mPipAlphaAnimatorSupplier.get(mContext, overlayLeash,
+ mOverlayFadeoutAnimator = mPipAlphaAnimatorSupplier.get(mContext, overlayLeash,
null /* startTx */, null /* finishTx */, PipAlphaAnimator.FADE_OUT);
- animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DURATION_MS);
- animator.setStartDelay(withStartDelay ? EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS : 0);
- animator.setAnimationEndCallback(onAnimationEnd);
- animator.start();
+ mOverlayFadeoutAnimator.setDuration(CONTENT_OVERLAY_FADE_OUT_DURATION_MS);
+ mOverlayFadeoutAnimator.setStartDelay(withStartDelay
+ ? EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS : 0);
+ mOverlayFadeoutAnimator.setAnimationEndCallback(() -> {
+ onAnimationEnd.run();
+ mOverlayFadeoutAnimator = null;
+ });
+ mOverlayFadeoutAnimator.start();
}
void setUpdateMovementBoundsRunnable(@Nullable Runnable updateMovementBoundsRunnable) {
@@ -289,6 +296,21 @@ public class PipScheduler {
mSurfaceControlTransactionFactory = factory;
}
+ @Override
+ public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState,
+ @PipTransitionState.TransitionState int newState,
+ @android.annotation.Nullable Bundle extra) {
+ switch (newState) {
+ case PipTransitionState.EXITING_PIP:
+ case PipTransitionState.SCHEDULED_BOUNDS_CHANGE:
+ if (mOverlayFadeoutAnimator != null && mOverlayFadeoutAnimator.isStarted()) {
+ mOverlayFadeoutAnimator.end();
+ mOverlayFadeoutAnimator = null;
+ }
+ break;
+ }
+ }
+
@VisibleForTesting
interface PipAlphaAnimatorSupplier {
PipAlphaAnimator get(@NonNull Context context,
@@ -303,6 +325,17 @@ public class PipScheduler {
mPipAlphaAnimatorSupplier = supplier;
}
+ @VisibleForTesting
+ void setOverlayFadeoutAnimator(@NonNull PipAlphaAnimator animator) {
+ mOverlayFadeoutAnimator = animator;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ PipAlphaAnimator getOverlayFadeoutAnimator() {
+ return mOverlayFadeoutAnimator;
+ }
+
void setPipParamsSupplier(@NonNull Supplier<PictureInPictureParams> pipParamsSupplier) {
mPipParamsSupplier = pipParamsSupplier;
}
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 c370c0cb0930..75c09829e551 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
@@ -93,7 +93,6 @@ import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.IActivityTaskManager;
import android.app.PendingIntent;
-import android.app.PictureInPictureParams;
import android.app.TaskInfo;
import android.content.ActivityNotFoundException;
import android.content.Context;
@@ -2249,10 +2248,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
setRootForceTranslucent(true, wct);
if (!enableFlexibleSplit()) {
- //TODO(b/373709676) Need to figure out how adjacentRoots work for flex split
+ // TODO: consider support 3 splits
// Make the stages adjacent to each other so they occlude what's behind them.
- wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
+ wct.setAdjacentRootSet(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
mSplitLayout.getInvisibleBounds(mTempRect1);
wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
}
@@ -2263,7 +2262,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
});
mLaunchAdjacentController.setLaunchAdjacentRoot(mSideStage.mRootTaskInfo.token);
} else {
- // TODO(b/373709676) Need to figure out how adjacentRoots work for flex split
+ // TODO: consider support 3 splits
}
}
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 05185cc5cdf0..a1d2774ee428 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
@@ -17,11 +17,9 @@
package com.android.wm.shell.windowdecor;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-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_MULTI_WINDOW;
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_HOVER_ENTER;
@@ -79,7 +77,6 @@ import android.view.SurfaceControl.Transaction;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewRootImpl;
-import android.view.WindowManager;
import android.window.DesktopModeFlags;
import android.window.TaskSnapshot;
import android.window.WindowContainerToken;
@@ -121,7 +118,6 @@ import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
-import com.android.wm.shell.desktopmode.DesktopWallpaperActivity;
import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository;
import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction;
import com.android.wm.shell.desktopmode.common.ToggleTaskSizeUtilsKt;
@@ -146,6 +142,7 @@ import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.FocusTransitionObserver;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener;
+import com.android.wm.shell.windowdecor.common.AppHandleAndHeaderVisibilityHelper;
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier;
@@ -207,6 +204,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter;
private final AppHandleEducationController mAppHandleEducationController;
private final AppToWebEducationController mAppToWebEducationController;
+ private final AppHandleAndHeaderVisibilityHelper mAppHandleAndHeaderVisibilityHelper;
private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory;
private boolean mTransitionDragActive;
@@ -294,6 +292,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
AppHandleEducationController appHandleEducationController,
AppToWebEducationController appToWebEducationController,
+ AppHandleAndHeaderVisibilityHelper appHandleAndHeaderVisibilityHelper,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
FocusTransitionObserver focusTransitionObserver,
@@ -338,6 +337,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
desktopTasksLimiter,
appHandleEducationController,
appToWebEducationController,
+ appHandleAndHeaderVisibilityHelper,
windowDecorCaptionHandleRepository,
activityOrientationChangeHandler,
new TaskPositionerFactory(),
@@ -386,6 +386,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
Optional<DesktopTasksLimiter> desktopTasksLimiter,
AppHandleEducationController appHandleEducationController,
AppToWebEducationController appToWebEducationController,
+ AppHandleAndHeaderVisibilityHelper appHandleAndHeaderVisibilityHelper,
WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository,
Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler,
TaskPositionerFactory taskPositionerFactory,
@@ -431,6 +432,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
mDesktopTasksLimiter = desktopTasksLimiter;
mAppHandleEducationController = appHandleEducationController;
mAppToWebEducationController = appToWebEducationController;
+ mAppHandleAndHeaderVisibilityHelper = appHandleAndHeaderVisibilityHelper;
mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository;
mActivityOrientationChangeHandler = activityOrientationChangeHandler;
mAssistContentRequester = assistContentRequester;
@@ -528,6 +530,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
@Override
public void setSplitScreenController(SplitScreenController splitScreenController) {
mSplitScreenController = splitScreenController;
+ mAppHandleAndHeaderVisibilityHelper.setSplitScreenController(splitScreenController);
}
@Override
@@ -1724,32 +1727,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
}
private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
- if (mDisplayController.getDisplay(taskInfo.displayId) == null) {
- // If DisplayController doesn't have it tracked, it could be a private/managed display.
- return false;
- }
- if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) return true;
- if (mSplitScreenController != null
- && mSplitScreenController.isTaskRootOrStageRoot(taskInfo.taskId)) {
- return false;
- }
- if (mDesktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo)) {
- return false;
- }
- final boolean isOnLargeScreen =
- mDisplayController.getDisplay(taskInfo.displayId).getMinSizeDimensionDp()
- >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
- if (!DesktopModeStatus.canEnterDesktopMode(mContext)
- && DesktopModeStatus.overridesShowAppHandle(mContext) && !isOnLargeScreen) {
- // Devices with multiple screens may enable the app handle but it should not show on
- // small screens
- return false;
- }
- return DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(mContext)
- && !DesktopWallpaperActivity.isWallpaperTask(taskInfo)
- && taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED
- && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
- && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop();
+ return mAppHandleAndHeaderVisibilityHelper.shouldShowAppHandleOrHeader(taskInfo);
}
private void createWindowDecoration(
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 2ebe67b22c18..673c3f66cb6c 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
@@ -29,6 +29,7 @@ import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_
import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode;
import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopModeOrShowAppHandle;
+import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.isDesktopModeSupportedOnDisplay;
import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON;
import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
import static com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener;
@@ -1410,7 +1411,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
supportsMultiInstance,
shouldShowManageWindowsButton,
shouldShowChangeAspectRatioButton,
- canEnterDesktopMode(mContext),
+ isDesktopModeSupportedOnDisplay(mContext, mDisplay),
isBrowserApp,
isBrowserApp ? getAppLink() : getBrowserLink(),
mResult.mCaptionWidth,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 7baef2b2dc97..bde46a1bc375 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -361,6 +361,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
outResult.mRootView = rootView;
final boolean fontScaleChanged = mWindowDecorConfig != null
&& mWindowDecorConfig.fontScale != mTaskInfo.configuration.fontScale;
+ final boolean localeListChanged = mWindowDecorConfig != null
+ && !mWindowDecorConfig.getLocales()
+ .equals(mTaskInfo.getConfiguration().getLocales());
final int oldDensityDpi = mWindowDecorConfig != null
? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED;
final int oldNightMode = mWindowDecorConfig != null
@@ -376,7 +379,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
|| oldLayoutResId != mLayoutResId
|| oldNightMode != newNightMode
|| mDecorWindowContext == null
- || fontScaleChanged) {
+ || fontScaleChanged
+ || localeListChanged) {
releaseViews(wct);
if (!obtainDisplayOrRegisterListener()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt
new file mode 100644
index 000000000000..39ccf5bd03a7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/AppHandleAndHeaderVisibilityHelper.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.windowdecor.common
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration
+import android.content.Context
+import android.view.WindowManager
+import android.window.DesktopExperienceFlags.ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.desktopmode.DesktopWallpaperActivity.Companion.isWallpaperTask
+import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy
+import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
+import com.android.wm.shell.splitscreen.SplitScreenController
+
+/**
+ * Resolves whether, given a task and its associated display that it is currently on, to show the
+ * app handle/header or not.
+ */
+class AppHandleAndHeaderVisibilityHelper (
+ private val context: Context,
+ private val displayController: DisplayController,
+ private val desktopModeCompatPolicy: DesktopModeCompatPolicy
+) {
+ var splitScreenController: SplitScreenController? = null
+
+ /**
+ * Returns, given a task's attribute and its display attribute, whether the app
+ * handle/header should show or not for this task.
+ */
+ fun shouldShowAppHandleOrHeader(taskInfo: ActivityManager.RunningTaskInfo): Boolean {
+ if (!ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY.isTrue) {
+ return allowedForTask(taskInfo)
+ }
+ return allowedForTask(taskInfo) && allowedForDisplay(taskInfo.displayId)
+ }
+
+ private fun allowedForTask(taskInfo: ActivityManager.RunningTaskInfo): Boolean {
+ // TODO (b/382023296): Remove once we no longer rely on
+ // Flags.enableBugFixesForSecondaryDisplay as it is taken care of in #allowedForDisplay
+ if (displayController.getDisplay(taskInfo.displayId) == null) {
+ // If DisplayController doesn't have it tracked, it could be a private/managed display.
+ return false
+ }
+ if (taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) return true
+ if (splitScreenController?.isTaskRootOrStageRoot(taskInfo.taskId) == true) {
+ return false
+ }
+
+ if (desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo)) {
+ return false
+ }
+
+ // TODO (b/382023296): Remove once we no longer rely on
+ // Flags.enableBugFixesForSecondaryDisplay as it is taken care of in #allowedForDisplay
+ val isOnLargeScreen =
+ displayController.getDisplay(taskInfo.displayId).minSizeDimensionDp >=
+ WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP
+ if (!DesktopModeStatus.canEnterDesktopMode(context)
+ && DesktopModeStatus.overridesShowAppHandle(context)
+ && !isOnLargeScreen
+ ) {
+ // Devices with multiple screens may enable the app handle but it should not show on
+ // small screens
+ return false
+ }
+ return DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)
+ && !isWallpaperTask(taskInfo)
+ && taskInfo.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED
+ && taskInfo.activityType == WindowConfiguration.ACTIVITY_TYPE_STANDARD
+ && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop
+ }
+
+ private fun allowedForDisplay(displayId: Int): Boolean {
+ // If DisplayController doesn't have it tracked, it could be a private/managed display.
+ val display = displayController.getDisplay(displayId)
+ if (display == null) return false
+
+ if (DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display)) {
+ return true
+ }
+ // If on default display and on Large Screen (unfolded), show app handle
+ return DesktopModeStatus.overridesShowAppHandle(context)
+ && display.minSizeDimensionDp >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt
index 801048adda4d..957898fd0088 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt
@@ -21,6 +21,7 @@ import android.content.Context
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.graphics.Bitmap
+import android.os.LocaleList
import android.os.UserHandle
import androidx.tracing.Trace
import com.android.internal.annotations.VisibleForTesting
@@ -80,6 +81,13 @@ class WindowDecorTaskResourceLoader(
*/
private val existingTasks = mutableSetOf<Int>()
+ /**
+ * A map of task -> localeList to keep track of the language of app name that's currently
+ * cached in |taskToResourceCache|.
+ */
+ @VisibleForTesting
+ val localeListOnCache = ConcurrentHashMap<Int, LocaleList>()
+
init {
shellInit.addInitCallback(this::onInit, this)
}
@@ -99,11 +107,14 @@ class WindowDecorTaskResourceLoader(
fun getName(taskInfo: RunningTaskInfo): CharSequence {
checkWindowDecorExists(taskInfo)
val cachedResources = taskToResourceCache[taskInfo.taskId]
- if (cachedResources != null) {
+ val localeListActiveOnCacheTime = localeListOnCache[taskInfo.taskId]
+ if (cachedResources != null &&
+ taskInfo.getConfiguration().getLocales().equals(localeListActiveOnCacheTime)) {
return cachedResources.appName
}
val resources = loadAppResources(taskInfo)
taskToResourceCache[taskInfo.taskId] = resources
+ localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales()
return resources.appName
}
@@ -117,6 +128,7 @@ class WindowDecorTaskResourceLoader(
}
val resources = loadAppResources(taskInfo)
taskToResourceCache[taskInfo.taskId] = resources
+ localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales()
return resources.appIcon
}
@@ -130,6 +142,7 @@ class WindowDecorTaskResourceLoader(
}
val resources = loadAppResources(taskInfo)
taskToResourceCache[taskInfo.taskId] = resources
+ localeListOnCache[taskInfo.taskId] = taskInfo.getConfiguration().getLocales()
return resources.veilIcon
}
@@ -142,6 +155,7 @@ class WindowDecorTaskResourceLoader(
fun onWindowDecorClosed(taskInfo: RunningTaskInfo) {
existingTasks.remove(taskInfo.taskId)
taskToResourceCache.remove(taskInfo.taskId)
+ localeListOnCache.remove(taskInfo.taskId)
}
private fun checkWindowDecorExists(taskInfo: RunningTaskInfo) {
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml
index 1de47df78853..e51447f5cfcb 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml
@@ -50,6 +50,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
index 34d001c858f6..7659ec903480 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
@@ -50,6 +50,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml
index 34d001c858f6..7659ec903480 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml
@@ -50,6 +50,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
index 9c1a8f17aeee..a4ecac9dfeb0 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
@@ -50,6 +50,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
index ae73dae99d6f..75ffdc69c73b 100644
--- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
@@ -50,6 +50,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
index a136936c0838..8003cbaada50 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
@@ -50,6 +50,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
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 598a101b8bcd..597e4a55ed0e 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
@@ -59,6 +59,7 @@ import com.android.wm.shell.common.DockStateReader;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.compatui.api.CompatUIInfo;
+import com.android.wm.shell.compatui.impl.CompatUIRequests;
import com.android.wm.shell.desktopmode.DesktopRepository;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.sysui.ShellController;
@@ -738,6 +739,22 @@ public class CompatUIControllerTest extends ShellTestCase {
verify(mController, never()).removeLayouts(taskInfo.taskId);
}
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_APP_COMPAT_UI_FRAMEWORK)
+ public void testSendCompatUIRequest_createRestartDialog() {
+ TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ false);
+ doReturn(true).when(mMockRestartDialogLayout)
+ .needsToBeRecreated(any(TaskInfo.class),
+ any(ShellTaskOrganizer.TaskListener.class));
+ doReturn(true).when(mCompatUIConfiguration).isRestartDialogEnabled();
+ doReturn(true).when(mCompatUIConfiguration).shouldShowRestartDialogAgain(eq(taskInfo));
+
+ mController.sendCompatUIRequest(new CompatUIRequests.DisplayCompatShowRestartDialog(
+ taskInfo, mMockTaskListener));
+ verify(mController).createRestartDialogWindowManager(any(), eq(taskInfo),
+ eq(mMockTaskListener));
+ }
+
private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat) {
return createTaskInfo(displayId, taskId, hasSizeCompat, /* isVisible */ false,
/* isFocused */ false, /* isTopActivityTransparent */ false);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
index 8ad54f5a0bb4..275d7b73a112 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt
@@ -28,14 +28,22 @@ import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.whenever
@@ -46,15 +54,18 @@ import org.mockito.quality.Strictness
*
* Usage: atest WMShellUnitTests:DesktopDisplayEventHandlerTest
*/
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidTestingRunner::class)
class DesktopDisplayEventHandlerTest : ShellTestCase() {
@Mock lateinit var testExecutor: ShellExecutor
@Mock lateinit var displayController: DisplayController
@Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories
+ @Mock private lateinit var mockDesktopRepositoryInitializer: DesktopRepositoryInitializer
@Mock private lateinit var mockDesktopRepository: DesktopRepository
@Mock private lateinit var mockDesktopTasksController: DesktopTasksController
@Mock private lateinit var desktopDisplayModeController: DesktopDisplayModeController
+ private val testScope = TestScope()
private lateinit var mockitoSession: StaticMockitoSession
private lateinit var shellInit: ShellInit
@@ -77,7 +88,9 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
DesktopDisplayEventHandler(
context,
shellInit,
+ testScope.backgroundScope,
displayController,
+ mockDesktopRepositoryInitializer,
mockDesktopUserRepositories,
mockDesktopTasksController,
desktopDisplayModeController,
@@ -89,17 +102,66 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
@After
fun tearDown() {
+ testScope.cancel()
mockitoSession.finishMocking()
}
@Test
- fun testDisplayAdded_supportsDesks_createsDesk() {
- whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+ fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_createsDesk() =
+ testScope.runTest {
+ val stateFlow = MutableStateFlow(false)
+ whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow)
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
- onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+ onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+ stateFlow.emit(true)
+ runCurrent()
- verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY)
- }
+ verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY)
+ }
+
+ @Test
+ fun testDisplayAdded_supportsDesks_desktopRepositoryNotInitialized_doesNotCreateDesk() =
+ testScope.runTest {
+ val stateFlow = MutableStateFlow(false)
+ whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow)
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+
+ onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+ runCurrent()
+
+ verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
+ }
+
+ @Test
+ fun testDisplayAdded_supportsDesks_desktopRepositoryInitializedTwice_createsDeskOnce() =
+ testScope.runTest {
+ val stateFlow = MutableStateFlow(false)
+ whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow)
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+
+ onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+ stateFlow.emit(true)
+ stateFlow.emit(true)
+ runCurrent()
+
+ verify(mockDesktopTasksController, times(1)).createDesk(DEFAULT_DISPLAY)
+ }
+
+ @Test
+ fun testDisplayAdded_supportsDesks_desktopRepositoryInitialized_deskExists_doesNotCreateDesk() =
+ testScope.runTest {
+ val stateFlow = MutableStateFlow(false)
+ whenever(mockDesktopRepositoryInitializer.isInitialized).thenReturn(stateFlow)
+ whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
+ whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1)
+
+ onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY)
+ stateFlow.emit(true)
+ runCurrent()
+
+ verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY)
+ }
@Test
fun testDisplayAdded_cannotEnterDesktopMode_doesNotCreateDesk() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
index cc37c440f650..450989dd334d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayModeControllerTest.kt
@@ -21,9 +21,12 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.content.ContentResolver
+import android.hardware.input.InputManager
import android.os.Binder
+import android.os.Handler
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import android.provider.Settings
import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS
import android.view.Display.DEFAULT_DISPLAY
@@ -44,6 +47,7 @@ import com.android.wm.shell.transition.Transitions
import com.google.common.truth.Truth.assertThat
import com.google.testing.junit.testparameterinjector.TestParameter
import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -64,13 +68,18 @@ import org.mockito.kotlin.whenever
*/
@SmallTest
@RunWith(TestParameterInjector::class)
-class DesktopDisplayModeControllerTest : ShellTestCase() {
+class DesktopDisplayModeControllerTest(
+ @TestParameter(valuesProvider = FlagsParameterizationProvider::class)
+ flags: FlagsParameterization
+) : ShellTestCase() {
private val transitions = mock<Transitions>()
private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>()
private val mockWindowManager = mock<IWindowManager>()
private val shellTaskOrganizer = mock<ShellTaskOrganizer>()
private val desktopWallpaperActivityTokenProvider =
mock<DesktopWallpaperActivityTokenProvider>()
+ private val inputManager = mock<InputManager>()
+ private val mainHandler = mock<Handler>()
private lateinit var controller: DesktopDisplayModeController
@@ -82,6 +91,10 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
private val defaultTDA = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
private val wallpaperToken = MockToken().token()
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
@Before
fun setUp() {
whenever(transitions.startTransition(anyInt(), any(), isNull())).thenReturn(Binder())
@@ -95,27 +108,20 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
mockWindowManager,
shellTaskOrganizer,
desktopWallpaperActivityTokenProvider,
+ inputManager,
+ mainHandler,
)
runningTasks.add(freeformTask)
runningTasks.add(fullscreenTask)
whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenReturn(ArrayList(runningTasks))
whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken)
+ setTabletModeStatus(SwitchState.UNKNOWN)
}
- private fun testDisplayWindowingModeSwitch(
- defaultWindowingMode: Int,
- extendedDisplayEnabled: Boolean,
- expectToSwitch: Boolean,
- ) {
- defaultTDA.configuration.windowConfiguration.windowingMode = defaultWindowingMode
- whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(defaultWindowingMode)
- val settingsSession =
- ExtendedDisplaySettingsSession(
- context.contentResolver,
- if (extendedDisplayEnabled) 1 else 0,
- )
-
- settingsSession.use {
+ private fun testDisplayWindowingModeSwitchOnDisplayConnected(expectToSwitch: Boolean) {
+ defaultTDA.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+ whenever(mockWindowManager.getWindowingMode(anyInt())).thenReturn(WINDOWING_MODE_FULLSCREEN)
+ ExtendedDisplaySettingsSession(context.contentResolver, 1).use {
connectExternalDisplay()
if (expectToSwitch) {
// Assumes [connectExternalDisplay] properly triggered the switching transition.
@@ -133,7 +139,7 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
assertThat(arg.firstValue.changes[wallpaperToken.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FULLSCREEN)
assertThat(arg.secondValue.changes[defaultTDA.token.asBinder()]?.windowingMode)
- .isEqualTo(defaultWindowingMode)
+ .isEqualTo(WINDOWING_MODE_FULLSCREEN)
assertThat(arg.secondValue.changes[wallpaperToken.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FULLSCREEN)
} else {
@@ -144,25 +150,64 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
@Test
@DisableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING)
- fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled(
- @TestParameter param: ModeSwitchTestCase
- ) {
- testDisplayWindowingModeSwitch(
- param.defaultWindowingMode,
- param.extendedDisplayEnabled,
- // When the flag is disabled, never switch.
- expectToSwitch = false,
- )
+ fun displayWindowingModeSwitchOnDisplayConnected_flagDisabled() {
+ // When the flag is disabled, never switch.
+ testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ false)
}
@Test
@EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING)
- fun displayWindowingModeSwitchOnDisplayConnected(@TestParameter param: ModeSwitchTestCase) {
- testDisplayWindowingModeSwitch(
- param.defaultWindowingMode,
- param.extendedDisplayEnabled,
- param.expectToSwitchByDefault,
- )
+ fun displayWindowingModeSwitchOnDisplayConnected() {
+ testDisplayWindowingModeSwitchOnDisplayConnected(/* expectToSwitch= */ true)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING)
+ @DisableFlags(Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH)
+ fun testTargetWindowingMode_formfactorDisabled(
+ @TestParameter param: ExternalDisplayBasedTargetModeTestCase,
+ @TestParameter tabletModeStatus: SwitchState,
+ ) {
+ whenever(mockWindowManager.getWindowingMode(anyInt()))
+ .thenReturn(param.defaultWindowingMode)
+ if (param.hasExternalDisplay) {
+ connectExternalDisplay()
+ } else {
+ disconnectExternalDisplay()
+ }
+ setTabletModeStatus(tabletModeStatus)
+
+ ExtendedDisplaySettingsSession(
+ context.contentResolver,
+ if (param.extendedDisplayEnabled) 1 else 0,
+ )
+ .use {
+ assertThat(controller.getTargetWindowingModeForDefaultDisplay())
+ .isEqualTo(param.expectedWindowingMode)
+ }
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING,
+ Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH,
+ )
+ fun testTargetWindowingMode(@TestParameter param: FormFactorBasedTargetModeTestCase) {
+ if (param.hasExternalDisplay) {
+ connectExternalDisplay()
+ } else {
+ disconnectExternalDisplay()
+ }
+ setTabletModeStatus(param.tabletModeStatus)
+
+ ExtendedDisplaySettingsSession(
+ context.contentResolver,
+ if (param.extendedDisplayEnabled) 1 else 0,
+ )
+ .use {
+ assertThat(controller.getTargetWindowingModeForDefaultDisplay())
+ .isEqualTo(param.expectedWindowingMode)
+ }
}
@Test
@@ -217,6 +262,10 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
controller.refreshDisplayWindowingMode()
}
+ private fun setTabletModeStatus(status: SwitchState) {
+ whenever(inputManager.isInTabletMode()).thenReturn(status.value)
+ }
+
private class ExtendedDisplaySettingsSession(
private val contentResolver: ContentResolver,
private val overrideValue: Int,
@@ -233,33 +282,158 @@ class DesktopDisplayModeControllerTest : ShellTestCase() {
}
}
+ private class FlagsParameterizationProvider : TestParameterValuesProvider() {
+ override fun provideValues(
+ context: TestParameterValuesProvider.Context
+ ): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf(
+ Flags.FLAG_FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH
+ )
+ }
+ }
+
companion object {
const val EXTERNAL_DISPLAY_ID = 100
- enum class ModeSwitchTestCase(
+ enum class SwitchState(val value: Int) {
+ UNKNOWN(InputManager.SWITCH_STATE_UNKNOWN),
+ ON(InputManager.SWITCH_STATE_ON),
+ OFF(InputManager.SWITCH_STATE_OFF),
+ }
+
+ enum class ExternalDisplayBasedTargetModeTestCase(
val defaultWindowingMode: Int,
+ val hasExternalDisplay: Boolean,
val extendedDisplayEnabled: Boolean,
- val expectToSwitchByDefault: Boolean,
+ val expectedWindowingMode: Int,
) {
- FULLSCREEN_DISPLAY(
+ FREEFORM_EXTERNAL_EXTENDED(
+ defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = true,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ FULLSCREEN_EXTERNAL_EXTENDED(
defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = true,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ FREEFORM_NO_EXTERNAL_EXTENDED(
+ defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasExternalDisplay = false,
extendedDisplayEnabled = true,
- expectToSwitchByDefault = true,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ FULLSCREEN_NO_EXTERNAL_EXTENDED(
+ defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = true,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ FREEFORM_EXTERNAL_MIRROR(
+ defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = false,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
),
- FULLSCREEN_DISPLAY_MIRRORING(
+ FULLSCREEN_EXTERNAL_MIRROR(
defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ hasExternalDisplay = true,
extendedDisplayEnabled = false,
- expectToSwitchByDefault = false,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
- FREEFORM_DISPLAY(
+ FREEFORM_NO_EXTERNAL_MIRROR(
defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = false,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ FULLSCREEN_NO_EXTERNAL_MIRROR(
+ defaultWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = false,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ }
+
+ enum class FormFactorBasedTargetModeTestCase(
+ val hasExternalDisplay: Boolean,
+ val extendedDisplayEnabled: Boolean,
+ val tabletModeStatus: SwitchState,
+ val expectedWindowingMode: Int,
+ ) {
+ EXTERNAL_EXTENDED_TABLET(
+ hasExternalDisplay = true,
extendedDisplayEnabled = true,
- expectToSwitchByDefault = false,
+ tabletModeStatus = SwitchState.ON,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
),
- FREEFORM_DISPLAY_MIRRORING(
- defaultWindowingMode = WINDOWING_MODE_FREEFORM,
+ NO_EXTERNAL_EXTENDED_TABLET(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.ON,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ EXTERNAL_MIRROR_TABLET(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.ON,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ NO_EXTERNAL_MIRROR_TABLET(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.ON,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ EXTERNAL_EXTENDED_CLAMSHELL(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.OFF,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ NO_EXTERNAL_EXTENDED_CLAMSHELL(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.OFF,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ EXTERNAL_MIRROR_CLAMSHELL(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.OFF,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ NO_EXTERNAL_MIRROR_CLAMSHELL(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.OFF,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ EXTERNAL_EXTENDED_UNKNOWN(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.UNKNOWN,
+ expectedWindowingMode = WINDOWING_MODE_FREEFORM,
+ ),
+ NO_EXTERNAL_EXTENDED_UNKNOWN(
+ hasExternalDisplay = false,
+ extendedDisplayEnabled = true,
+ tabletModeStatus = SwitchState.UNKNOWN,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ EXTERNAL_MIRROR_UNKNOWN(
+ hasExternalDisplay = true,
+ extendedDisplayEnabled = false,
+ tabletModeStatus = SwitchState.UNKNOWN,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
+ ),
+ NO_EXTERNAL_MIRROR_UNKNOWN(
+ hasExternalDisplay = false,
extendedDisplayEnabled = false,
- expectToSwitchByDefault = false,
+ tabletModeStatus = SwitchState.UNKNOWN,
+ expectedWindowingMode = WINDOWING_MODE_FULLSCREEN,
),
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
index 006c3cae121c..4c18ee1500b7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
@@ -114,6 +114,8 @@ class DesktopImmersiveControllerTest : ShellTestCase() {
transactionSupplier = transactionSupplier,
)
desktopRepository = userRepositories.current
+ desktopRepository.addDesk(DEFAULT_DISPLAY, DEFAULT_DESK_ID)
+ desktopRepository.setActiveDesk(DEFAULT_DISPLAY, DEFAULT_DESK_ID)
}
@Test
@@ -835,5 +837,6 @@ class DesktopImmersiveControllerTest : ShellTestCase() {
companion object {
private val STABLE_BOUNDS = Rect(0, 100, 2000, 1900)
private val DISPLAY_BOUNDS = Rect(0, 0, 2000, 2000)
+ private const val DEFAULT_DESK_ID = 0
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index e9f92cfd7c56..0c585b3e843a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -431,6 +431,38 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX,
+ Flags.FLAG_ENABLE_DESKTOP_OPENING_DEEPLINK_MINIMIZE_ANIMATION_BUGFIX,
+ )
+ fun startAndAnimateLaunchTransition_withMinimizeChange_wrongTaskId_reparentsMinimizeChange() {
+ val wct = WindowContainerTransaction()
+ val launchingTask = createTask(WINDOWING_MODE_FREEFORM)
+ val minimizingTask = createTask(WINDOWING_MODE_FREEFORM)
+ val launchTaskChange = createChange(launchingTask, mode = TRANSIT_OPEN)
+ val minimizeChange = createChange(minimizingTask)
+ val transition = Binder()
+ whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull()))
+ .thenReturn(transition)
+
+ mixedHandler.startLaunchTransition(
+ transitionType = TRANSIT_OPEN,
+ wct = wct,
+ taskId = Int.MAX_VALUE,
+ minimizingTaskId = minimizingTask.taskId,
+ )
+ mixedHandler.startAnimation(
+ transition,
+ createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange, minimizeChange)),
+ SurfaceControl.Transaction(),
+ SurfaceControl.Transaction(),
+ ) {}
+
+ verify(rootTaskDisplayAreaOrganizer)
+ .reparentToDisplayArea(anyInt(), eq(minimizeChange.leash), any())
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX)
fun startAnimation_pendingTransition_noLaunchChange_returnsFalse() {
val wct = WindowContainerTransaction()
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 fe1dc29181b9..b859a00c6df4 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
@@ -24,7 +24,6 @@ import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
-import android.view.Display
import android.view.SurfaceControl
import androidx.test.filters.SmallTest
import com.android.internal.policy.SystemBarUtils
@@ -67,7 +66,6 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
private lateinit var taskInfo: RunningTaskInfo
@Mock private lateinit var syncQueue: SyncTransactionQueue
@Mock private lateinit var displayController: DisplayController
- @Mock private lateinit var display: Display
@Mock private lateinit var taskSurface: SurfaceControl
@Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
@Mock private lateinit var displayLayout: DisplayLayout
@@ -83,12 +81,20 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width())
whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height())
whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS)
- whenever(displayController.getDisplay(anyInt())).thenReturn(display)
whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
whenever(displayController.getDisplay(anyInt())).thenReturn(mContext.display)
whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(any()))
.thenReturn(Rect())
taskInfo = DesktopTestHelpers.createFullscreenTask()
+
+ mContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_isDesktopModeSupported,
+ true,
+ )
+ mContext.orCreateTestableResources.addOverride(
+ com.android.internal.R.bool.config_canInternalDisplayHostDesktops,
+ true,
+ )
}
@Test
@@ -260,14 +266,9 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
)
fun testDefaultIndicatorWithNoDesktop() {
mContext.orCreateTestableResources.addOverride(
- com.android.internal.R.bool.config_isDesktopModeSupported,
+ com.android.internal.R.bool.config_canInternalDisplayHostDesktops,
false,
)
- mContext.orCreateTestableResources.addOverride(
- com.android.internal.R.bool.config_isDesktopModeDevOptionSupported,
- false,
- )
-
// Fullscreen to center, no desktop indicator
createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN)
var result = visualIndicator.updateIndicatorType(PointF(500f, 500f))
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 de92d391645a..f84a1a38bdfc 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
@@ -1203,6 +1203,17 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() {
}
@Test
+ @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
+ fun removeDesk_removesFromPersistence() =
+ runTest(StandardTestDispatcher()) {
+ repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 2)
+
+ repo.removeDesk(deskId = 2)
+
+ verify(persistentRepository).removeDesktop(DEFAULT_USER_ID, 2)
+ }
+
+ @Test
fun getTaskInFullImmersiveState_byDisplay() {
repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
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 d093629000f3..34c5ebd6d94d 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
@@ -22,6 +22,7 @@ import android.app.ActivityOptions
import android.app.KeyguardManager
import android.app.PendingIntent
import android.app.PictureInPictureParams
+import android.app.WindowConfiguration
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -31,6 +32,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.ActivityInfo
import android.content.pm.ActivityInfo.CONFIG_DENSITY
import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
@@ -296,6 +298,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
private val wallpaperToken = MockToken().token()
private val homeComponentName = ComponentName(HOME_LAUNCHER_PACKAGE_NAME, /* class */ "")
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
@Before
fun setUp() {
Dispatchers.setMain(StandardTestDispatcher())
@@ -432,6 +438,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
dragToDesktopTransitionHandler,
mMockDesktopImmersiveController,
userRepositories,
+ repositoryInitializer,
recentsTransitionHandler,
multiInstanceHelper,
shellExecutor,
@@ -626,11 +633,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isDesktopModeShowing_noTasks_returnsFalse() {
assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isDesktopModeShowing_noTasksVisible_returnsFalse() {
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
@@ -641,6 +650,15 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun isDesktopModeShowing_noActiveDesk_returnsFalse() {
+ taskRepository.setDeskInactive(deskId = 0)
+
+ assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() {
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
@@ -655,6 +673,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC,
)
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isDesktopModeShowing_topTransparentFullscreenTask_returnsTrue() {
val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId)
@@ -664,6 +683,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
+ Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun isDesktopModeShowing_deskInactive_topTransparentFullscreenTask_returnsTrue() {
+ val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+ taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId)
+ taskRepository.setDeskInactive(deskId = 0)
+
+ assertThat(controller.isDesktopModeShowing(displayId = DEFAULT_DISPLAY)).isTrue()
+ }
+
+ @Test
+ @EnableFlags(
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
)
@@ -1052,11 +1085,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isAnyDeskActive_noTasks_returnsFalse() {
assertThat(controller.isAnyDeskActive(DEFAULT_DISPLAY)).isFalse()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun isAnyDeskActive_noActiveDesk_returnsFalse() {
+ taskRepository.setDeskInactive(deskId = 0)
+
+ assertThat(controller.isAnyDeskActive(DEFAULT_DISPLAY)).isFalse()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun isAnyDeskActive_withActiveDesk_returnsTrue() {
+ taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
+
+ assertThat(controller.isAnyDeskActive(DEFAULT_DISPLAY)).isTrue()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isAnyDeskActive_twoTasks_bothVisible_returnsTrue() {
setUpHomeTask()
@@ -1067,6 +1118,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isInDesktop_twoTasks_oneVisible_returnsTrue() {
setUpHomeTask()
@@ -1077,6 +1129,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun isAnyDeskActive_twoTasksVisibleOnDifferentDisplays_returnsTrue() {
taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
@@ -1096,7 +1149,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.addMoveToDeskTaskChanges(wct, task, deskId = 0)
val finalBounds = findBoundsChange(wct, task)
- assertThat(finalBounds).isEqualTo(Rect())
+ assertThat(finalBounds).isNull()
}
@Test
@@ -1107,7 +1160,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.addMoveToDeskTaskChanges(wct, task, deskId = 0)
val finalBounds = findBoundsChange(wct, task)
- assertThat(finalBounds).isEqualTo(Rect())
+ assertThat(finalBounds).isNull()
}
@Test
@@ -1118,7 +1171,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.addMoveToDeskTaskChanges(wct, task, deskId = 0)
val finalBounds = findBoundsChange(wct, task)
- assertThat(finalBounds).isEqualTo(Rect())
+ assertThat(finalBounds).isNull()
}
@Test
@@ -1129,7 +1182,55 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
controller.addMoveToDeskTaskChanges(wct, task, deskId = 0)
val finalBounds = findBoundsChange(wct, task)
- assertThat(finalBounds).isEqualTo(Rect())
+ assertThat(finalBounds).isNull()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES)
+ fun addMoveToDeskTaskChanges_newTaskInstance_inheritsClosingInstanceBounds() {
+ // Setup existing task.
+ val existingTask = setUpFreeformTask(active = true)
+ val testComponent = ComponentName(/* package */ "test.package", /* class */ "test.class")
+ existingTask.topActivity = testComponent
+ existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500))
+ // Set up new instance of already existing task.
+ val launchingTask = setUpFullscreenTask()
+ launchingTask.topActivity = testComponent
+ launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
+
+ // Move new instance to desktop. By default multi instance is not supported so first
+ // instance will close.
+ val wct = WindowContainerTransaction()
+ controller.addMoveToDeskTaskChanges(wct, launchingTask, deskId = 0)
+
+ // New instance should inherit task bounds of old instance.
+ assertThat(findBoundsChange(wct, launchingTask))
+ .isEqualTo(existingTask.configuration.windowConfiguration.bounds)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES)
+ fun handleRequest_newTaskInstance_inheritsClosingInstanceBounds() {
+ setUpLandscapeDisplay()
+ // Setup existing task.
+ val existingTask = setUpFreeformTask(active = true)
+ val testComponent = ComponentName(/* package */ "test.package", /* class */ "test.class")
+ existingTask.topActivity = testComponent
+ existingTask.configuration.windowConfiguration.setBounds(Rect(0, 0, 500, 500))
+ // Set up new instance of already existing task.
+ val launchingTask = setUpFreeformTask(active = false)
+ taskRepository.removeTask(launchingTask.displayId, launchingTask.taskId)
+ launchingTask.topActivity = testComponent
+ launchingTask.baseIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
+
+ // Move new instance to desktop. By default multi instance is not supported so first
+ // instance will close.
+ val wct = controller.handleRequest(Binder(), createTransition(launchingTask))
+
+ assertNotNull(wct, "should handle request")
+ val finalBounds = findBoundsChange(wct, launchingTask)
+ // New instance should inherit task bounds of old instance.
+ assertThat(finalBounds).isEqualTo(existingTask.configuration.windowConfiguration.bounds)
}
@Test
@@ -1813,6 +1914,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
val wallpaperToken = MockToken().token()
whenever(desktopWallpaperActivityTokenProvider.getToken(SECOND_DISPLAY))
.thenReturn(wallpaperToken)
+ taskRepository.addDesk(SECOND_DISPLAY, deskId = 2)
val task = setUpFreeformTask(displayId = SECOND_DISPLAY, deskId = 2, background = true)
controller.moveTaskToDefaultDeskAndActivate(
@@ -2617,7 +2719,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun moveToNextDisplay_moveFromFirstToSecondDisplay() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToNextDisplay_moveFromFirstToSecondDisplay_multiDesksDisabled() {
// Set up two display ids
taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
whenever(rootTaskDisplayAreaOrganizer.displayIds)
@@ -2643,7 +2746,27 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun moveToNextDisplay_moveFromSecondToFirstDisplay() {
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToNextDisplay_moveFromFirstToSecondDisplay_multiDesksEnabled() {
+ // Set up two display ids
+ val targetDeskId = 2
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId)
+ whenever(rootTaskDisplayAreaOrganizer.displayIds)
+ .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+ // Create a mock for the target display area: second display
+ val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0)
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY))
+ .thenReturn(secondDisplayArea)
+
+ val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ controller.moveToNextDisplay(task.taskId)
+
+ verify(desksOrganizer).moveTaskToDesk(any(), eq(targetDeskId), eq(task))
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToNextDisplay_moveFromSecondToFirstDisplay_multiDesksDisabled() {
// Set up two display ids
whenever(rootTaskDisplayAreaOrganizer.displayIds)
.thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
@@ -2669,6 +2792,25 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToNextDisplay_moveFromSecondToFirstDisplay_multiDesksEnabled() {
+ // Set up two display ids
+ val targetDeskId = 0
+ whenever(rootTaskDisplayAreaOrganizer.displayIds)
+ .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+ // Create a mock for the target display area: default display
+ val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
+ .thenReturn(defaultDisplayArea)
+
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY)
+ val task = setUpFreeformTask(displayId = SECOND_DISPLAY)
+ controller.moveToNextDisplay(task.taskId)
+
+ verify(desksOrganizer).moveTaskToDesk(any(), eq(targetDeskId), eq(task))
+ }
+
+ @Test
@EnableFlags(
FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
@@ -2732,6 +2874,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT)
fun moveToNextDisplay_sizeInDpPreserved() {
// Set up two display ids
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2)
whenever(rootTaskDisplayAreaOrganizer.displayIds)
.thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
// Create a mock for the target display area: second display
@@ -2773,6 +2916,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT)
fun moveToNextDisplay_shiftWithinDestinationDisplayBounds() {
// Set up two display ids
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2)
whenever(rootTaskDisplayAreaOrganizer.displayIds)
.thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
// Create a mock for the target display area: second display
@@ -2814,6 +2958,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
// Set up two display ids
whenever(rootTaskDisplayAreaOrganizer.displayIds)
.thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2)
// Create a mock for the target display area: second display
val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0)
whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY))
@@ -2922,7 +3067,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun moveToNextDisplay_toDesktopInOtherDisplay_bringsExistingTasksToFront() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToNextDisplay_toDesktopInOtherDisplay_multiDesksDisabled_bringsExistingTasksToFront() {
val transition = Binder()
val sourceDeskId = 0
val targetDeskId = 2
@@ -2948,6 +3094,34 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun moveToNextDisplay_toDesktopInOtherDisplay_multiDesksEnabled_bringsExistingTasksToFront() {
+ val transition = Binder()
+ val sourceDeskId = 0
+ val targetDeskId = 2
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId)
+ taskRepository.setDeskInactive(deskId = targetDeskId)
+ // Set up two display ids
+ whenever(rootTaskDisplayAreaOrganizer.displayIds)
+ .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
+ // Create a mock for the target display area: second display
+ val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0)
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY))
+ .thenReturn(secondDisplayArea)
+ whenever(transitions.startTransition(eq(TRANSIT_CHANGE), any(), anyOrNull()))
+ .thenReturn(transition)
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = sourceDeskId)
+ val task2 = setUpFreeformTask(displayId = SECOND_DISPLAY, deskId = targetDeskId)
+
+ controller.moveToNextDisplay(task1.taskId)
+
+ // Existing desktop task in the target display is moved to front.
+ val wct = getLatestTransition()
+ assertNotNull(wct)
+ verify(desksOrganizer).reorderTaskToFront(wct, targetDeskId, task2)
+ }
+
+ @Test
@EnableFlags(
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
@@ -3061,7 +3235,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
fun moveToNextDisplay_movingToDesktop_sendsTaskbarRoundingUpdate() {
val transition = Binder()
- val sourceDeskId = 1
+ val sourceDeskId = 0
val targetDeskId = 2
taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId)
taskRepository.setDeskInactive(deskId = targetDeskId)
@@ -3578,7 +3752,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTask_freeformVisible_multiDesksDisabled_returnSwitchToFreeformWCT() {
val homeTask = setUpHomeTask()
val freeformTask = setUpFreeformTask()
markTaskVisible(freeformTask)
@@ -3594,7 +3769,22 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_returnSwitchToFreeformWCT() {
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTask_deskActive_multiDesksEnabled_movesToDesk() {
+ val deskId = 0
+ taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = deskId)
+ setUpHomeTask()
+ val fullscreenTask = createFullscreenTask()
+
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ assertNotNull(wct, "should handle request")
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_multiDesksDisabled_returnSwitchToFreeformWCT() {
val homeTask = setUpHomeTask()
val freeformTask = setUpFreeformTask()
markTaskVisible(freeformTask)
@@ -3619,6 +3809,21 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTaskWithTaskOnHome_activeDesk_multiDesksEnabled_movesToDesk() {
+ val deskId = 0
+ setUpHomeTask()
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val fullscreenTask = createFullscreenTask()
+ fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME)
+
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ assertNotNull(wct, "should handle request")
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask)
+ }
+
+ @Test
@DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun handleRequest_fullscreenTaskToDesk_underTaskLimit_multiDesksDisabled_dontMinimize() {
val freeformTask = setUpFreeformTask()
@@ -3850,6 +4055,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() {
whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
@@ -3872,7 +4078,28 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- fun handleRequest_fullscreenTask_noTasks_enforceDesktop_fullscreenDisplay_returnNull() {
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_fullscreenTask_noInDesk_enforceDesktop_freeformDisplay_movesToDesk() {
+ val deskId = 0
+ taskRepository.setDeskInactive(deskId)
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
+ whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+
+ val fullscreenTask = createFullscreenTask()
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ assertNotNull(wct, "should handle request")
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, fullscreenTask)
+ }
+
+ @Test
+ fun handleRequest_fullscreenTask_notInDesk_enforceDesktop_fullscreenDisplay_returnNull() {
+ taskRepository.setDeskInactive(deskId = 0)
whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
@@ -3884,6 +4111,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() {
val freeformTask = setUpFreeformTask()
markTaskHidden(freeformTask)
@@ -3892,12 +4120,23 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun handleRequest_fullscreenTask_noOtherTasks_returnNull() {
val fullscreenTask = createFullscreenTask()
assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTask_notInDesk_returnNull() {
+ taskRepository.setDeskInactive(deskId = 0)
+ val fullscreenTask = createFullscreenTask()
+
+ assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() {
val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY)
createFreeformTask(displayId = SECOND_DISPLAY)
@@ -3908,6 +4147,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_fullscreenTask_deskInOtherDisplayActive_returnNull() {
+ taskRepository.setDeskInactive(deskId = 0)
+ val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY)
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2)
+ taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = 2)
+
+ val result =
+ controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay))
+
+ assertThat(result).isNull()
+ }
+
+ @Test
@DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_multiDesksDisabled_minimize() {
val deskId = 0
@@ -3959,7 +4212,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() {
- val freeformTask = setUpFreeformTask()
+ taskRepository.setDeskInactive(deskId = 0)
+ val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0)
markTaskHidden(freeformTask)
val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
@@ -3989,9 +4243,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
-
- val freeformTask = setUpFreeformTask()
+ taskRepository.setDeskInactive(deskId = 0)
+ val freeformTask = setUpFreeformTask(DEFAULT_DISPLAY, deskId = 0)
markTaskHidden(freeformTask)
+
val wct = controller.handleRequest(Binder(), createTransition(freeformTask))
assertNotNull(wct, "should handle request")
@@ -4000,7 +4255,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 handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() {
val freeformTask1 = setUpFreeformTask()
val freeformTask2 = createFreeformTask()
@@ -4019,7 +4277,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_multiDesksDisabled_reorderedToTop() {
whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val freeformTask1 = setUpFreeformTask()
val freeformTask2 = createFreeformTask()
@@ -4042,34 +4301,47 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() {
- val task = createFreeformTask()
- val result = controller.handleRequest(Binder(), createTransition(task))
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_freeformTask_desktopWallpaperEnabled_notInDesk_reorderedToTop() {
+ whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
+ val deskId = 0
+ taskRepository.setDeskInactive(deskId)
+ val freeformTask1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ val freeformTask2 = createFreeformTask()
- assertNotNull(result, "Should handle request")
- assertThat(result.hierarchyOps?.size).isEqualTo(1)
- result.assertReorderAt(0, task, toTop = true)
+ val wct =
+ controller.handleRequest(
+ Binder(),
+ createTransition(freeformTask2, type = TRANSIT_TO_FRONT),
+ )
+
+ assertNotNull(wct, "Should handle request")
+ verify(desksOrganizer).reorderTaskToFront(wct, deskId, freeformTask1)
+ wct.assertReorder(freeformTask2, toTop = true)
}
@Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() {
- whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() {
val task = createFreeformTask()
-
val result = controller.handleRequest(Binder(), createTransition(task))
assertNotNull(result, "Should handle request")
- assertThat(result.hierarchyOps?.size).isEqualTo(2)
- // Add desktop wallpaper activity
- result.assertPendingIntentAt(0, desktopWallpaperIntent)
- // Bring new task to front
- result.assertReorderAt(1, task, toTop = true)
+ assertThat(result.hierarchyOps?.size).isEqualTo(1)
+ result.assertReorderAt(0, task, toTop = true)
}
@Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ @DisableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() {
val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
// Second display task
@@ -4085,10 +4357,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() {
+ taskRepository.setDeskInactive(deskId = 0)
whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null)
val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
// Second display task
- createFreeformTask(displayId = SECOND_DISPLAY)
+ taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2)
+ taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = 2)
+ setUpFreeformTask(displayId = SECOND_DISPLAY, deskId = 2)
val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay))
@@ -4224,7 +4499,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
- fun handleRequest_topActivityTransparentWithoutDisplay_returnSwitchToFreeformWCT() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_topActivityTransparentWithoutDisplay_multiDesksDisabled_returnSwitchToFreeformWCT() {
val freeformTask = setUpFreeformTask()
markTaskVisible(freeformTask)
@@ -4241,6 +4517,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_topActivityTransparentWithoutDisplay_multiDesksEnabled_returnSwitchToFreeformWCT() {
+ val deskId = 0
+ val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ markTaskVisible(freeformTask)
+
+ val task =
+ setUpFullscreenTask().apply {
+ isActivityStackTransparent = true
+ isTopActivityNoDisplay = true
+ numActivities = 1
+ }
+
+ val wct = controller.handleRequest(Binder(), createTransition(task))
+
+ assertNotNull(wct)
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, task)
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
@DisableFlags(Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION)
fun handleRequest_topActivityTransparentWithDisplay_returnSwitchToFullscreenWCT() {
@@ -4299,7 +4598,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC,
)
- fun handleRequest_onlyTopTransparentFullscreenTask_returnSwitchToFreeformWCT() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_onlyTopTransparentFullscreenTask_multiDesksDisabled_returnSwitchToFreeformWCT() {
val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId)
@@ -4311,8 +4611,28 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
+ Flags.FLAG_INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_onlyTopTransparentFullscreenTask_multiDesksEnabled_movesToDesktop() {
+ val deskId = 0
+ val topTransparentTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+ taskRepository.setTopTransparentFullscreenTaskId(DEFAULT_DISPLAY, topTransparentTask.taskId)
+ taskRepository.setDeskInactive(deskId = deskId)
+
+ val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+
+ val wct = controller.handleRequest(Binder(), createTransition(task))
+ assertNotNull(wct)
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, task)
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun handleRequest_desktopNotShowing_topTransparentFullscreenTask_returnNull() {
+ taskRepository.setDeskInactive(deskId = 0)
val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
@@ -4366,7 +4686,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
- fun handleRequest_systemUIActivityWithoutDisplay_returnSwitchToFreeformWCT() {
+ @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun handleRequest_systemUIActivityWithoutDisplay_multiDesksDisabled_returnSwitchToFreeformWCT() {
val freeformTask = setUpFreeformTask()
markTaskVisible(freeformTask)
@@ -4385,6 +4706,32 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
.isEqualTo(WINDOWING_MODE_FREEFORM)
}
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun handleRequest_systemUIActivityWithoutDisplay_multiDesksEnabled_movesTaskToDesk() {
+ val deskId = 0
+ val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
+ markTaskVisible(freeformTask)
+
+ // 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(displayId = DEFAULT_DISPLAY).apply {
+ baseActivity = baseComponent
+ isTopActivityNoDisplay = true
+ }
+
+ val wct = controller.handleRequest(Binder(), createTransition(task))
+
+ assertNotNull(wct)
+ verify(desksOrganizer).moveTaskToDesk(wct, deskId, task)
+ }
+
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun handleRequest_defaultHomePackageWithDisplay_returnSwitchToFullscreenWCT() {
val freeformTask = setUpFreeformTask()
@@ -4427,6 +4774,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
fun handleRequest_systemUIActivityWithDisplay_returnSwitchToFullscreenWCT_enforcedDesktop() {
+ taskRepository.setDeskInactive(deskId = 0)
whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true)
val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
@@ -5060,6 +5408,55 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
)
+ fun activateDesk_hasNonRunningTask_startsTask() {
+ val deskId = 0
+ val nonRunningTask =
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0, background = true)
+
+ 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()))
+
+ val wct = getLatestWct(TRANSIT_TO_FRONT, OneShotRemoteHandler::class.java)
+ assertNotNull(wct)
+ wct.assertLaunchTask(nonRunningTask.taskId, WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
+ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun activateDesk_hasRunningTask_reordersTask() {
+ val deskId = 0
+ val runningTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, 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(desksOrganizer).reorderTaskToFront(any(), eq(deskId), eq(runningTask))
+ }
+
+ @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)
@@ -6784,6 +7181,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@Test
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() {
+ taskRepository.setDeskInactive(deskId = 0)
val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
taskRepository.setTaskInFullImmersiveState(
displayId = triggerTask.displayId,
@@ -6860,6 +7258,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() {
val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+ taskRepository.setDeskInactive(deskId = 0)
assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
assertThat(
@@ -6895,6 +7294,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() {
val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, active = false)
+ taskRepository.setDeskInactive(deskId = 0)
assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
assertThat(
@@ -6924,6 +7324,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER,
)
fun startLaunchTransition_desktopNotShowing_movesWallpaperToFront() {
+ taskRepository.setDeskInactive(deskId = 0)
val launchingTask = createFreeformTask(displayId = DEFAULT_DISPLAY)
val wct = WindowContainerTransaction()
wct.reorder(launchingTask.token, /* onTop= */ true)
@@ -7393,7 +7794,15 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
}
private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? =
- wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds
+ wct.changes.entries
+ .find { (token, change) ->
+ token == task.token.asBinder() &&
+ (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0
+ }
+ ?.value
+ ?.configuration
+ ?.windowConfiguration
+ ?.bounds
private fun verifyWCTNotExecuted() {
verify(transitions, never()).startTransition(anyInt(), any(), isNull())
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 0871d38ceb46..6e7adf368155 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
@@ -26,6 +26,7 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
import com.android.internal.jank.InteractionJankMonitor
import com.android.window.flags.Flags
+import com.android.window.flags.Flags.FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestRunningTaskInfoBuilder
@@ -34,6 +35,7 @@ import com.android.wm.shell.bubbles.BubbleTransitions
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
+import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.CancelState
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.Companion.DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS
import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
@@ -56,6 +58,7 @@ import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.MockitoSession
+import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argThat
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
@@ -118,6 +121,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
.strictness(Strictness.LENIENT)
.mockStatic(SystemProperties::class.java)
.startMocking()
+ whenever(
+ transitions.startTransition(
+ eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP),
+ /* wct= */ any(),
+ eq(defaultHandler),
+ )
+ )
+ .thenReturn(mock<IBinder>())
}
@After
@@ -679,17 +690,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
val startTransition = startDrag(defaultHandler, task)
val endTransition = mock<IBinder>()
defaultHandler.onTaskResizeAnimationListener = mock()
- defaultHandler.mergeAnimation(
+ mergeAnimation(
transition = endTransition,
- info =
- createTransitionInfo(
- type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
- draggedTask = task,
- ),
- startT = mock<SurfaceControl.Transaction>(),
- finishT = mock<SurfaceControl.Transaction>(),
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ task = task,
mergeTarget = startTransition,
- finishCallback = mock<Transitions.TransitionFinishCallback>(),
)
defaultHandler.onTransitionConsumed(endTransition, aborted = true, mock())
@@ -701,6 +706,123 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
+ fun mergeOtherTransition_cancelAndEndNotYetRequested_doesntInterruptsStartDrag() {
+ val finishCallback = mock<Transitions.TransitionFinishCallback>()
+ val task = createTask()
+ defaultHandler.onTaskResizeAnimationListener = mock()
+ val startTransition = startDrag(defaultHandler, task, finishCallback = finishCallback)
+
+ mergeInterruptingTransition(mergeTarget = startTransition)
+
+ verify(finishCallback, never()).onTransitionFinished(anyOrNull())
+ verify(dragAnimator, never()).cancelAnimator()
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
+ fun mergeOtherTransition_endDragAlreadyMerged_doesNotInterruptStartDrag() {
+ val startDragFinishCallback = mock<Transitions.TransitionFinishCallback>()
+ val task = createTask()
+ val startTransition =
+ startDrag(defaultHandler, task, finishCallback = startDragFinishCallback)
+ defaultHandler.onTaskResizeAnimationListener = mock()
+ mergeAnimation(
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ task = task,
+ mergeTarget = startTransition,
+ )
+
+ mergeInterruptingTransition(mergeTarget = startTransition)
+
+ verify(startDragFinishCallback, never()).onTransitionFinished(anyOrNull())
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
+ fun startEndAnimation_otherTransitionInterruptedStartAfterEndRequest_finishImmediately() {
+ val task1 = createTask()
+ val startTransition = startDrag(defaultHandler, task1)
+ val endTransition =
+ defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction())
+ val startTransaction = mock<SurfaceControl.Transaction>()
+ val endDragFinishCallback = mock<Transitions.TransitionFinishCallback>()
+ defaultHandler.onTaskResizeAnimationListener = mock()
+ mergeInterruptingTransition(mergeTarget = startTransition)
+
+ val didAnimate =
+ defaultHandler.startAnimation(
+ transition = requireNotNull(endTransition),
+ info =
+ createTransitionInfo(
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ draggedTask = task1,
+ ),
+ startTransaction = startTransaction,
+ finishTransaction = mock(),
+ finishCallback = endDragFinishCallback,
+ )
+
+ assertThat(didAnimate).isTrue()
+ verify(startTransaction).apply()
+ verify(endDragFinishCallback).onTransitionFinished(anyOrNull())
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_DRAG_TO_DESKTOP_INCOMING_TRANSITIONS_BUGFIX)
+ fun startDrag_otherTransitionInterruptedStartAfterEndRequested_animatesDragWhenReady() {
+ val task1 = createTask()
+ val startTransition = startDrag(defaultHandler, task1)
+ verify(dragAnimator).startAnimation()
+ val endTransition =
+ defaultHandler.finishDragToDesktopTransition(WindowContainerTransaction())
+ defaultHandler.onTaskResizeAnimationListener = mock()
+ mergeInterruptingTransition(mergeTarget = startTransition)
+ defaultHandler.startAnimation(
+ transition = requireNotNull(endTransition),
+ info =
+ createTransitionInfo(
+ type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP,
+ draggedTask = task1,
+ ),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = mock(),
+ )
+
+ startDrag(defaultHandler, createTask())
+
+ verify(dragAnimator, times(2)).startAnimation()
+ }
+
+ private fun mergeInterruptingTransition(mergeTarget: IBinder) {
+ defaultHandler.mergeAnimation(
+ transition = mock<IBinder>(),
+ info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = createTask()),
+ startT = mock(),
+ finishT = mock(),
+ mergeTarget = mergeTarget,
+ finishCallback = mock(),
+ )
+ }
+
+ private fun mergeAnimation(
+ transition: IBinder = mock(),
+ type: Int,
+ mergeTarget: IBinder,
+ task: RunningTaskInfo,
+ ) {
+ defaultHandler.mergeAnimation(
+ transition = transition,
+ info = createTransitionInfo(type = type, draggedTask = task),
+ startT = mock(),
+ finishT = mock(),
+ mergeTarget = mergeTarget,
+ finishCallback = mock(),
+ )
+ }
+
+ @Test
fun getAnimationFraction_returnsFraction() {
val fraction =
SpringDragToDesktopTransitionHandler.getAnimationFraction(
@@ -785,6 +907,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
finishTransaction: SurfaceControl.Transaction = mock(),
homeChange: TransitionInfo.Change? = createHomeChange(),
transitionRootLeash: SurfaceControl = mock(),
+ finishCallback: Transitions.TransitionFinishCallback = mock(),
): IBinder {
whenever(dragAnimator.position).thenReturn(PointF())
// Simulate transition is started and is ready to animate.
@@ -800,7 +923,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
),
startTransaction = startTransaction,
finishTransaction = finishTransaction,
- finishCallback = {},
+ finishCallback = finishCallback,
)
return transition
}
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 6b2f90fc0a59..9af504797182 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,6 +15,7 @@
*/
package com.android.wm.shell.desktopmode.multidesks
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.testing.AndroidTestingRunner
import android.view.Display
@@ -29,6 +30,7 @@ import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_R
import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTaskOrganizer.TaskListener
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.common.LaunchAdjacentController
@@ -39,14 +41,17 @@ import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellInit
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertNotNull
+import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.kotlin.argThat
import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
/**
* Tests for [RootTaskDesksOrganizer].
@@ -76,48 +81,17 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
)
}
- @Test
- fun testCreateDesk_callsBack() {
- val callback = FakeOnCreateCallback()
- organizer.createDesk(Display.DEFAULT_DISPLAY, callback)
-
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
-
- assertThat(callback.created).isTrue()
- assertEquals(freeformRoot.taskId, callback.deskId)
- }
-
- @Test
- fun testCreateDesk_createsMinimizationRoot() {
- val callback = FakeOnCreateCallback()
- organizer.createDesk(Display.DEFAULT_DISPLAY, callback)
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
-
- val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(minimizationRootTask, SurfaceControl())
-
- val minimizationRoot = organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId]
- assertNotNull(minimizationRoot)
- assertThat(minimizationRoot.deskId).isEqualTo(freeformRoot.taskId)
- assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId)
- }
+ @Test fun testCreateDesk_createsDeskAndMinimizationRoots() = runTest { createDesk() }
@Test
- fun testCreateMinimizationRoot_marksHidden() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
-
- val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(minimizationRootTask, SurfaceControl())
+ fun testCreateMinimizationRoot_marksHidden() = runTest {
+ val desk = createDesk()
verify(mockShellTaskOrganizer)
.applyTransaction(
argThat { wct ->
wct.changes.any { change ->
- change.key == minimizationRootTask.token.asBinder() &&
+ change.key == desk.minimizationRoot.token.asBinder() &&
(change.value.changeMask and Change.CHANGE_HIDDEN != 0) &&
change.value.hidden
}
@@ -126,7 +100,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testOnTaskAppeared_withoutRequest_throws() {
+ fun testOnTaskAppeared_withoutRequest_throws() = runTest {
val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
assertThrows(Exception::class.java) {
@@ -135,41 +109,25 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testOnTaskAppeared_withRequestOnlyInAnotherDisplay_throws() {
- organizer.createDesk(displayId = 2, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask(Display.DEFAULT_DISPLAY).apply { parentTaskId = -1 }
-
- assertThrows(Exception::class.java) {
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
- }
- }
-
- @Test
- fun testOnTaskAppeared_duplicateRoot_throws() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ fun testOnTaskAppeared_duplicateRoot_throws() = runTest {
+ val desk = createDesk()
assertThrows(Exception::class.java) {
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ organizer.onTaskAppeared(desk.deskRoot.taskInfo, SurfaceControl())
}
}
@Test
- fun testOnTaskAppeared_duplicateMinimizedRoot_throws() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
- organizer.onTaskAppeared(minimizationRootTask, SurfaceControl())
+ fun testOnTaskAppeared_duplicateMinimizedRoot_throws() = runTest {
+ val desk = createDesk()
assertThrows(Exception::class.java) {
- organizer.onTaskAppeared(minimizationRootTask, SurfaceControl())
+ organizer.onTaskAppeared(desk.minimizationRoot.taskInfo, SurfaceControl())
}
}
@Test
- fun testOnTaskVanished_removesRoot() {
+ fun testOnTaskVanished_removesRoot() = runTest {
val desk = createDesk()
organizer.onTaskVanished(desk.deskRoot.taskInfo)
@@ -178,7 +136,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testOnTaskVanished_removesMinimizedRoot() {
+ fun testOnTaskVanished_removesMinimizedRoot() = runTest {
val desk = createDesk()
organizer.onTaskVanished(desk.deskRoot.taskInfo)
@@ -188,7 +146,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testDesktopWindowAppearsInDesk() {
+ fun testDesktopWindowAppearsInDesk() = runTest {
val desk = createDesk()
val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
@@ -198,7 +156,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testDesktopWindowAppearsInDeskMinimizationRoot() {
+ fun testDesktopWindowAppearsInDeskMinimizationRoot() = runTest {
val desk = createDesk()
val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
@@ -208,7 +166,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testDesktopWindowMovesToMinimizationRoot() {
+ fun testDesktopWindowMovesToMinimizationRoot() = runTest {
val desk = createDesk()
val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
organizer.onTaskAppeared(child, SurfaceControl())
@@ -221,7 +179,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testDesktopWindowDisappearsFromDesk() {
+ fun testDesktopWindowDisappearsFromDesk() = runTest {
val desk = createDesk()
val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
@@ -232,7 +190,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testDesktopWindowDisappearsFromDeskMinimizationRoot() {
+ fun testDesktopWindowDisappearsFromDeskMinimizationRoot() = runTest {
val desk = createDesk()
val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
@@ -243,7 +201,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testRemoveDesk_removesDeskRoot() {
+ fun testRemoveDesk_removesDeskRoot() = runTest {
val desk = createDesk()
val wct = WindowContainerTransaction()
@@ -259,7 +217,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testRemoveDesk_removesMinimizationRoot() {
+ fun testRemoveDesk_removesMinimizationRoot() = runTest {
val desk = createDesk()
val wct = WindowContainerTransaction()
@@ -275,7 +233,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testActivateDesk() {
+ fun testActivateDesk() = runTest {
val desk = createDesk()
val wct = WindowContainerTransaction()
@@ -299,7 +257,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testActivateDesk_didNotExist_throws() {
+ fun testActivateDesk_didNotExist_throws() = runTest {
val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
val wct = WindowContainerTransaction()
@@ -307,7 +265,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testMoveTaskToDesk() {
+ fun testMoveTaskToDesk() = runTest {
val desk = createDesk()
val desktopTask = createFreeformTask().apply { parentTaskId = -1 }
@@ -333,7 +291,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testMoveTaskToDesk_didNotExist_throws() {
+ fun testMoveTaskToDesk_didNotExist_throws() = runTest {
val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
val desktopTask = createFreeformTask().apply { parentTaskId = -1 }
@@ -344,7 +302,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testGetDeskAtEnd() {
+ fun testGetDeskAtEnd() = runTest {
val desk = createDesk()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
@@ -357,7 +315,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testGetDeskAtEnd_inMinimizationRoot() {
+ fun testGetDeskAtEnd_inMinimizationRoot() = runTest {
val desk = createDesk()
val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
@@ -370,27 +328,24 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun testIsDeskActiveAtEnd() {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot = createFreeformTask().apply { parentTaskId = -1 }
- freeformRoot.isVisibleRequested = true
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
+ fun testIsDeskActiveAtEnd() = runTest {
+ val desk = createDesk()
val isActive =
organizer.isDeskActiveAtEnd(
change =
- TransitionInfo.Change(freeformRoot.token, SurfaceControl()).apply {
- taskInfo = freeformRoot
+ TransitionInfo.Change(desk.deskRoot.token, SurfaceControl()).apply {
+ taskInfo = desk.deskRoot.taskInfo
mode = TRANSIT_TO_FRONT
},
- deskId = freeformRoot.taskId,
+ deskId = desk.deskRoot.deskId,
)
assertThat(isActive).isTrue()
}
@Test
- fun deactivateDesk_clearsLaunchRoot() {
+ fun deactivateDesk_clearsLaunchRoot() = runTest {
val wct = WindowContainerTransaction()
val desk = createDesk()
organizer.activateDesk(wct, desk.deskRoot.deskId)
@@ -409,7 +364,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun isDeskChange_forDeskId() {
+ fun isDeskChange_forDeskId() = runTest {
val desk = createDesk()
assertThat(
@@ -424,7 +379,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun isDeskChange_forDeskId_inMinimizationRoot() {
+ fun isDeskChange_forDeskId_inMinimizationRoot() = runTest {
val desk = createDesk()
assertThat(
@@ -442,7 +397,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun isDeskChange_anyDesk() {
+ fun isDeskChange_anyDesk() = runTest {
val desk = createDesk()
assertThat(
@@ -456,7 +411,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun isDeskChange_anyDesk_inMinimizationRoot() {
+ fun isDeskChange_anyDesk_inMinimizationRoot() = runTest {
val desk = createDesk()
assertThat(
@@ -473,7 +428,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun minimizeTask() {
+ fun minimizeTask() = runTest {
val desk = createDesk()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
val wct = WindowContainerTransaction()
@@ -486,7 +441,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun minimizeTask_alreadyMinimized_noOp() {
+ fun minimizeTask_alreadyMinimized_noOp() = runTest {
val desk = createDesk()
val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId }
val wct = WindowContainerTransaction()
@@ -498,7 +453,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun minimizeTask_inDifferentDesk_noOp() {
+ fun minimizeTask_inDifferentDesk_noOp() = runTest {
val desk = createDesk()
val otherDesk = createDesk()
val task = createFreeformTask().apply { parentTaskId = otherDesk.deskRoot.deskId }
@@ -511,7 +466,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun unminimizeTask() {
+ fun unminimizeTask() = runTest {
val desk = createDesk()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
val wct = WindowContainerTransaction()
@@ -528,7 +483,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun unminimizeTask_alreadyUnminimized_noOp() {
+ fun unminimizeTask_alreadyUnminimized_noOp() = runTest {
val desk = createDesk()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
val wct = WindowContainerTransaction()
@@ -542,7 +497,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun unminimizeTask_notInDesk_noOp() {
+ fun unminimizeTask_notInDesk_noOp() = runTest {
val desk = createDesk()
val task = createFreeformTask()
val wct = WindowContainerTransaction()
@@ -553,7 +508,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun reorderTaskToFront() {
+ fun reorderTaskToFront() = runTest {
val desk = createDesk()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
val wct = WindowContainerTransaction()
@@ -573,7 +528,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun reorderTaskToFront_notInDesk_noOp() {
+ fun reorderTaskToFront_notInDesk_noOp() = runTest {
val desk = createDesk()
val task = createFreeformTask()
val wct = WindowContainerTransaction()
@@ -592,7 +547,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun reorderTaskToFront_minimized_unminimizesAndReorders() {
+ fun reorderTaskToFront_minimized_unminimizesAndReorders() = runTest {
val desk = createDesk()
val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId }
val wct = WindowContainerTransaction()
@@ -615,7 +570,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun onTaskAppeared_visibleDesk_onlyDesk_disablesLaunchAdjacent() {
+ fun onTaskAppeared_visibleDesk_onlyDesk_disablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = true
createDesk(visible = true)
@@ -624,7 +579,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun onTaskAppeared_invisibleDesk_onlyDesk_enablesLaunchAdjacent() {
+ fun onTaskAppeared_invisibleDesk_onlyDesk_enablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = false
createDesk(visible = false)
@@ -633,7 +588,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun onTaskAppeared_invisibleDesk_otherVisibleDesk_disablesLaunchAdjacent() {
+ fun onTaskAppeared_invisibleDesk_otherVisibleDesk_disablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = true
createDesk(visible = true)
@@ -643,7 +598,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun onTaskInfoChanged_deskBecomesVisible_onlyDesk_disablesLaunchAdjacent() {
+ fun onTaskInfoChanged_deskBecomesVisible_onlyDesk_disablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = true
val desk = createDesk(visible = false)
@@ -654,7 +609,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun onTaskInfoChanged_deskBecomesInvisible_onlyDesk_enablesLaunchAdjacent() {
+ fun onTaskInfoChanged_deskBecomesInvisible_onlyDesk_enablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = false
val desk = createDesk(visible = true)
@@ -665,7 +620,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun onTaskInfoChanged_deskBecomesInvisible_otherVisibleDesk_disablesLaunchAdjacent() {
+ fun onTaskInfoChanged_deskBecomesInvisible_otherVisibleDesk_disablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = true
createDesk(visible = true)
@@ -677,7 +632,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun onTaskVanished_visibleDeskDisappears_onlyDesk_enablesLaunchAdjacent() {
+ fun onTaskVanished_visibleDeskDisappears_onlyDesk_enablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = false
val desk = createDesk(visible = true)
@@ -687,7 +642,7 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
}
@Test
- fun onTaskVanished_visibleDeskDisappears_otherDeskVisible_disablesLaunchAdjacent() {
+ fun onTaskVanished_visibleDeskDisappears_otherDeskVisible_disablesLaunchAdjacent() = runTest {
launchAdjacentController.launchAdjacentEnabled = true
createDesk(visible = true)
@@ -702,20 +657,39 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
val minimizationRoot: DeskMinimizationRoot,
)
- private fun createDesk(visible: Boolean = true): DeskRoots {
- organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback())
- val freeformRoot =
+ private suspend fun createDesk(visible: Boolean = true): DeskRoots {
+ val freeformRootTask =
createFreeformTask().apply {
parentTaskId = -1
isVisible = visible
+ isVisibleRequested = visible
}
- organizer.onTaskAppeared(freeformRoot, SurfaceControl())
- val minimizationRoot = createFreeformTask().apply { parentTaskId = -1 }
- organizer.onTaskAppeared(minimizationRoot, SurfaceControl())
- return DeskRoots(
- organizer.deskRootsByDeskId[freeformRoot.taskId],
- checkNotNull(organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId]),
- )
+ val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 }
+ Mockito.reset(mockShellTaskOrganizer)
+ whenever(
+ mockShellTaskOrganizer.createRootTask(
+ Display.DEFAULT_DISPLAY,
+ WINDOWING_MODE_FREEFORM,
+ organizer,
+ true,
+ )
+ )
+ .thenAnswer { invocation ->
+ val listener = (invocation.arguments[2] as TaskListener)
+ listener.onTaskAppeared(freeformRootTask, SurfaceControl())
+ }
+ .thenAnswer { invocation ->
+ val listener = (invocation.arguments[2] as TaskListener)
+ listener.onTaskAppeared(minimizationRootTask, SurfaceControl())
+ }
+ val deskId = organizer.createDesk(Display.DEFAULT_DISPLAY)
+ assertEquals(freeformRootTask.taskId, deskId)
+ val deskRoot = assertNotNull(organizer.deskRootsByDeskId.get(freeformRootTask.taskId))
+ val minimizationRoot =
+ assertNotNull(organizer.deskMinimizationRootsByDeskId[freeformRootTask.taskId])
+ assertThat(minimizationRoot.deskId).isEqualTo(freeformRootTask.taskId)
+ assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId)
+ return DeskRoots(deskRoot, minimizationRoot)
}
private fun WindowContainerTransaction.hasMinimizationHops(
@@ -738,14 +712,4 @@ class RootTaskDesksOrganizerTest : ShellTestCase() {
hop.newParent == desk.deskRoot.token.asBinder() &&
hop.toTop
}
-
- private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback {
- var deskId: Int? = null
- val created: Boolean
- get() = deskId != null
-
- override fun onCreated(deskId: Int) {
- this.deskId = deskId
- }
- }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt
index dd9e6ca0deae..4440d4e801fe 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt
@@ -17,7 +17,6 @@
package com.android.wm.shell.desktopmode.persistence
import android.os.UserManager
-import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
@@ -82,10 +81,27 @@ class DesktopRepositoryInitializerTest : ShellTestCase() {
}
@Test
- @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, FLAG_ENABLE_DESKTOP_WINDOWING_HSUM)
- /** TODO: b/362720497 - add multi-desk version when implemented. */
- @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
- fun initWithPersistence_multipleUsers_addedCorrectly_multiDesksDisabled() =
+ fun init_updatesFlow() =
+ runTest(StandardTestDispatcher()) {
+ whenever(persistentRepository.getUserDesktopRepositoryMap())
+ .thenReturn(mapOf(USER_ID_1 to desktopRepositoryState1))
+ whenever(persistentRepository.getDesktopRepositoryState(USER_ID_1))
+ .thenReturn(desktopRepositoryState1)
+ whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_1)).thenReturn(desktop1)
+ whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_2)).thenReturn(desktop2)
+
+ repositoryInitializer.initialize(desktopUserRepositories)
+
+ assertThat(repositoryInitializer.isInitialized.value).isTrue()
+ }
+
+ @Test
+ @EnableFlags(
+ FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE,
+ FLAG_ENABLE_DESKTOP_WINDOWING_HSUM,
+ FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
+ )
+ fun initWithPersistence_multipleUsers_addedCorrectly() =
runTest(StandardTestDispatcher()) {
whenever(persistentRepository.getUserDesktopRepositoryMap())
.thenReturn(
@@ -104,50 +120,74 @@ class DesktopRepositoryInitializerTest : ShellTestCase() {
repositoryInitializer.initialize(desktopUserRepositories)
- // Desktop Repository currently returns all tasks across desktops for a specific user
- // since the repository currently doesn't handle desktops. This test logic should be
- // updated
- // once the repository handles multiple desktops.
assertThat(
- desktopUserRepositories.getProfile(USER_ID_1).getActiveTasks(DEFAULT_DISPLAY)
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getActiveTaskIdsInDesk(DESKTOP_ID_1)
)
- .containsExactly(1, 3, 4, 5)
+ .containsExactly(1, 3)
.inOrder()
assertThat(
desktopUserRepositories
.getProfile(USER_ID_1)
- .getExpandedTasksOrdered(DEFAULT_DISPLAY)
+ .getActiveTaskIdsInDesk(DESKTOP_ID_2)
)
- .containsExactly(5, 1)
+ .containsExactly(4, 5)
.inOrder()
assertThat(
- desktopUserRepositories.getProfile(USER_ID_1).getMinimizedTasks(DEFAULT_DISPLAY)
+ desktopUserRepositories
+ .getProfile(USER_ID_2)
+ .getActiveTaskIdsInDesk(DESKTOP_ID_3)
)
- .containsExactly(3, 4)
+ .containsExactly(7, 8)
+ .inOrder()
+ assertThat(
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_1)
+ )
+ .containsExactly(1)
.inOrder()
-
assertThat(
- desktopUserRepositories.getProfile(USER_ID_2).getActiveTasks(DEFAULT_DISPLAY)
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_2)
)
- .containsExactly(7, 8)
+ .containsExactly(5)
.inOrder()
assertThat(
desktopUserRepositories
.getProfile(USER_ID_2)
- .getExpandedTasksOrdered(DEFAULT_DISPLAY)
+ .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_3)
+ )
+ .containsExactly(7)
+ .inOrder()
+ assertThat(
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getMinimizedTaskIdsInDesk(DESKTOP_ID_1)
+ )
+ .containsExactly(3)
+ .inOrder()
+ assertThat(
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getMinimizedTaskIdsInDesk(DESKTOP_ID_2)
)
- .contains(7)
+ .containsExactly(4)
+ .inOrder()
assertThat(
- desktopUserRepositories.getProfile(USER_ID_2).getMinimizedTasks(DEFAULT_DISPLAY)
+ desktopUserRepositories
+ .getProfile(USER_ID_2)
+ .getMinimizedTaskIdsInDesk(DESKTOP_ID_3)
)
.containsExactly(8)
+ .inOrder()
}
@Test
- @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE)
- /** TODO: b/362720497 - add multi-desk version when implemented. */
- @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
- fun initWithPersistence_singleUser_addedCorrectly_multiDesksDisabled() =
+ @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
+ fun initWithPersistence_singleUser_addedCorrectly() =
runTest(StandardTestDispatcher()) {
whenever(persistentRepository.getUserDesktopRepositoryMap())
.thenReturn(mapOf(USER_ID_1 to desktopRepositoryState1))
@@ -161,23 +201,44 @@ class DesktopRepositoryInitializerTest : ShellTestCase() {
assertThat(
desktopUserRepositories
.getProfile(USER_ID_1)
- .getActiveTaskIdsInDesk(deskId = DEFAULT_DISPLAY)
+ .getActiveTaskIdsInDesk(DESKTOP_ID_1)
+ )
+ .containsExactly(1, 3)
+ .inOrder()
+ assertThat(
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getActiveTaskIdsInDesk(DESKTOP_ID_2)
+ )
+ .containsExactly(4, 5)
+ .inOrder()
+ assertThat(
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_1)
+ )
+ .containsExactly(1)
+ .inOrder()
+ assertThat(
+ desktopUserRepositories
+ .getProfile(USER_ID_1)
+ .getExpandedTasksIdsInDeskOrdered(DESKTOP_ID_2)
)
- .containsExactly(1, 3, 4, 5)
+ .containsExactly(5)
.inOrder()
assertThat(
desktopUserRepositories
.getProfile(USER_ID_1)
- .getExpandedTasksIdsInDeskOrdered(deskId = DEFAULT_DISPLAY)
+ .getMinimizedTaskIdsInDesk(DESKTOP_ID_1)
)
- .containsExactly(5, 1)
+ .containsExactly(3)
.inOrder()
assertThat(
desktopUserRepositories
.getProfile(USER_ID_1)
- .getMinimizedTaskIdsInDesk(deskId = DEFAULT_DISPLAY)
+ .getMinimizedTaskIdsInDesk(DESKTOP_ID_2)
)
- .containsExactly(3, 4)
+ .containsExactly(4)
.inOrder()
}
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 275e4882a79d..42f65dd71f16 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
@@ -17,6 +17,7 @@
package com.android.wm.shell.pip2.phone;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -86,6 +87,7 @@ public class PipSchedulerTest {
@Mock private SurfaceControl.Transaction mMockTransaction;
@Mock private PipAlphaAnimator mMockAlphaAnimator;
@Mock private SplitScreenController mMockSplitScreenController;
+ @Mock private SurfaceControl mMockLeash;
@Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor;
@Captor private ArgumentCaptor<WindowContainerTransaction> mWctArgumentCaptor;
@@ -315,6 +317,30 @@ public class PipSchedulerTest {
verify(mMockAlphaAnimator, never()).start();
}
+ @Test
+ public void onPipTransitionStateChanged_exiting_endAnimation() {
+ mPipScheduler.setOverlayFadeoutAnimator(mMockAlphaAnimator);
+ when(mMockAlphaAnimator.isStarted()).thenReturn(true);
+ mPipScheduler.onPipTransitionStateChanged(PipTransitionState.ENTERED_PIP,
+ PipTransitionState.EXITING_PIP, null);
+
+ verify(mMockAlphaAnimator, times(1)).end();
+ assertNull("mOverlayFadeoutAnimator should be reset to null",
+ mPipScheduler.getOverlayFadeoutAnimator());
+ }
+
+ @Test
+ public void onPipTransitionStateChanged_scheduledBoundsChange_endAnimation() {
+ mPipScheduler.setOverlayFadeoutAnimator(mMockAlphaAnimator);
+ when(mMockAlphaAnimator.isStarted()).thenReturn(true);
+ mPipScheduler.onPipTransitionStateChanged(PipTransitionState.ENTERED_PIP,
+ PipTransitionState.SCHEDULED_BOUNDS_CHANGE, null);
+
+ verify(mMockAlphaAnimator, times(1)).end();
+ assertNull("mOverlayFadeoutAnimator should be reset to null",
+ mPipScheduler.getOverlayFadeoutAnimator());
+ }
+
private void setNullPipTaskToken() {
when(mMockPipTransitionState.getPipTaskToken()).thenReturn(null);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
index 6ecebd76a951..75f6bda4d750 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/GroupedTaskInfoTest.kt
@@ -203,7 +203,7 @@ class GroupedTaskInfoTest : ShellTestCase() {
assertThat(taskInfoFromParcel.taskInfoList).hasSize(3)
// Only compare task ids
val taskIdComparator = Correspondence.transforming<TaskInfo, Int>(
- { it?.taskId }, "has taskId of"
+ { it.taskId }, "has taskId of"
)
assertThat(taskInfoFromParcel.taskInfoList).comparingElementsUsing(taskIdComparator)
.containsExactly(1, 2, 3).inOrder()
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 fb62ba75e056..edf91fe62e7d 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
@@ -234,14 +234,25 @@ class DesktopModeStatusTest : ShellTestCase() {
assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse()
}
+ @DisableFlags(Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE)
@Test
fun isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktopOff_returnsFalse() {
doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported))
- doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops))
+ doReturn(false).whenever(mockResources)
+ .getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops))
assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse()
}
+ @EnableFlags(Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE)
+ @Test
+ fun isPDDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktopOff_returnsTrue() {
+ doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported))
+ doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops))
+
+ assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue()
+ }
+
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION)
@Test
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 e89a122595d5..d69509faf4ec 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
@@ -115,7 +115,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
.spyStatic(DragPositioningCallbackUtility::class.java)
.startMocking()
- doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(Mockito.any()) }
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupportedOnDisplay(Mockito.any(),
+ Mockito.any()) }
doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(Mockito.any()) }
doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(Mockito.any()) }
@@ -394,7 +395,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN)
- doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(any()) }
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupportedOnDisplay(any(), any()) }
setUpMockDecorationsForTasks(task)
onTaskOpening(task)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
index 81dfaed56b6f..a1f40fdefee9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt
@@ -79,6 +79,7 @@ import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.util.StubTransaction
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeKeyguardChangeListener
import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
+import com.android.wm.shell.windowdecor.common.AppHandleAndHeaderVisibilityHelper
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier
@@ -174,6 +175,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
internal lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener
protected lateinit var desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel
protected lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy
+ protected lateinit var appHandleAndHeaderVisibilityHelper: AppHandleAndHeaderVisibilityHelper
fun setUpCommon() {
spyContext = spy(mContext)
@@ -185,9 +187,13 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository)
whenever(mockDisplayController.getDisplayContext(any())).thenReturn(spyContext)
whenever(mockDisplayController.getDisplay(any())).thenReturn(display)
+ whenever(display.type).thenReturn(Display.TYPE_INTERNAL)
whenever(mockDesktopUserRepositories.getProfile(anyInt()))
.thenReturn(mockDesktopRepository)
desktopModeCompatPolicy = DesktopModeCompatPolicy(spyContext)
+ appHandleAndHeaderVisibilityHelper =
+ AppHandleAndHeaderVisibilityHelper(spyContext, mockDisplayController,
+ desktopModeCompatPolicy)
desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel(
spyContext,
testShellExecutor,
@@ -222,6 +228,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() {
Optional.of(mockTasksLimiter),
mockAppHandleEducationController,
mockAppToWebEducationController,
+ appHandleAndHeaderVisibilityHelper,
mockCaptionHandleRepository,
Optional.of(mockActivityOrientationChangeHandler),
mockTaskPositionerFactory,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index a2927fa3527b..9a2e2fad50be 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -60,6 +60,7 @@ import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Handler;
+import android.os.LocaleList;
import android.testing.AndroidTestingRunner;
import android.util.DisplayMetrics;
import android.view.AttachedSurfaceControl;
@@ -97,6 +98,7 @@ import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import java.util.function.Supplier;
/**
@@ -475,6 +477,50 @@ public class WindowDecorationTests extends ShellTestCase {
}
@Test
+ public void testReinflateViewsOnLocaleListChange() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setVisible(true)
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .build();
+ taskInfo.configuration.setLocales(new LocaleList(Locale.FRANCE, Locale.US));
+ final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo));
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+ clearInvocations(windowDecor);
+
+ final ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder()
+ .setVisible(true)
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .build();
+ taskInfo2.configuration.setLocales(new LocaleList(Locale.US, Locale.FRANCE));
+ windowDecor.relayout(taskInfo2, true /* hasGlobalFocus */, Region.obtain());
+ // WindowDecoration#releaseViews should be called since the locale list has changed.
+ verify(windowDecor, times(1)).releaseViews(any());
+ }
+
+ @Test
+ public void testViewNotReinflatedWhenLocaleListNotChanged() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setVisible(true)
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .build();
+ taskInfo.configuration.setLocales(new LocaleList(Locale.FRANCE, Locale.US));
+ final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo));
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+ clearInvocations(windowDecor);
+ windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain());
+ // WindowDecoration#releaseViews should not be called since nothing has changed.
+ verify(windowDecor, never()).releaseViews(any());
+ }
+
+ @Test
public void testLayoutResultCalculation_fullWidthCaption() {
final Display defaultDisplay = mock(Display.class);
doReturn(defaultDisplay).when(mMockDisplayController)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt
index c61e0eb3b5af..c8ccac35d4c4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt
@@ -23,6 +23,7 @@ import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
+import android.os.LocaleList
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import android.testing.TestableContext
@@ -39,6 +40,7 @@ import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.sysui.UserChangeListener
import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader.AppResources
import com.google.common.truth.Truth.assertThat
+import java.util.Locale
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
@@ -116,8 +118,10 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() {
@Test
fun testGetName_cached_returnsFromCache() {
val task = createTaskInfo(context.userId)
+ task.configuration.setLocales(LocaleList(Locale.US))
loader.onWindowDecorCreated(task)
loader.taskToResourceCache[task.taskId] = AppResources("App Name", mock(), mock())
+ loader.localeListOnCache[task.taskId] = LocaleList(Locale.US)
loader.getName(task)
@@ -130,6 +134,19 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() {
}
@Test
+ fun testGetName_cached_localesChanged_loadsResourceAndCaches() {
+ val task = createTaskInfo(context.userId)
+ loader.onWindowDecorCreated(task)
+ loader.taskToResourceCache[task.taskId] = AppResources("App Name", mock(), mock())
+ loader.localeListOnCache[task.taskId] = LocaleList(Locale.US, Locale.FRANCE)
+ task.configuration.setLocales(LocaleList(Locale.FRANCE, Locale.US))
+ doReturn("App Name but in French").whenever(mockPackageManager).getApplicationLabel(any())
+
+ assertThat(loader.getName(task)).isEqualTo("App Name but in French")
+ assertThat(loader.taskToResourceCache[task.taskId]?.appName).isEqualTo("App Name but in French")
+ }
+
+ @Test
fun testGetHeaderIcon_notCached_loadsResourceAndCaches() {
val task = createTaskInfo(context.userId)
loader.onWindowDecorCreated(task)
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index a18c5f5f92f6..8ecd6ba9b253 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -6520,41 +6520,79 @@ base::expected<StringPiece16, NullOrIOError> StringPoolRef::string16() const {
}
bool ResTable::getResourceFlags(uint32_t resID, uint32_t* outFlags) const {
- if (mError != NO_ERROR) {
- return false;
- }
+ if (mError != NO_ERROR) {
+ return false;
+ }
- const ssize_t p = getResourcePackageIndex(resID);
- const int t = Res_GETTYPE(resID);
- const int e = Res_GETENTRY(resID);
+ const ssize_t p = getResourcePackageIndex(resID);
+ const int t = Res_GETTYPE(resID);
+ const int e = Res_GETENTRY(resID);
- if (p < 0) {
- if (Res_GETPACKAGE(resID)+1 == 0) {
- ALOGW("No package identifier when getting flags for resource number 0x%08x", resID);
- } else {
- ALOGW("No known package when getting flags for resource number 0x%08x", resID);
- }
- return false;
- }
- if (t < 0) {
- ALOGW("No type identifier when getting flags for resource number 0x%08x", resID);
- return false;
+ if (p < 0) {
+ if (Res_GETPACKAGE(resID)+1 == 0) {
+ ALOGW("No package identifier when getting flags for resource number 0x%08x", resID);
+ } else {
+ ALOGW("No known package when getting flags for resource number 0x%08x", resID);
}
+ return false;
+ }
+ if (t < 0) {
+ ALOGW("No type identifier when getting flags for resource number 0x%08x", resID);
+ return false;
+ }
- const PackageGroup* const grp = mPackageGroups[p];
- if (grp == NULL) {
- ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID);
- return false;
- }
+ const PackageGroup* const grp = mPackageGroups[p];
+ if (grp == NULL) {
+ ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID);
+ return false;
+ }
- Entry entry;
- status_t err = getEntry(grp, t, e, NULL, &entry);
- if (err != NO_ERROR) {
- return false;
+ Entry entry;
+ status_t err = getEntry(grp, t, e, NULL, &entry);
+ if (err != NO_ERROR) {
+ return false;
+ }
+
+ *outFlags = entry.specFlags;
+ return true;
+}
+
+bool ResTable::getResourceEntryFlags(uint32_t resID, uint32_t* outFlags) const {
+ if (mError != NO_ERROR) {
+ return false;
+ }
+
+ const ssize_t p = getResourcePackageIndex(resID);
+ const int t = Res_GETTYPE(resID);
+ const int e = Res_GETENTRY(resID);
+
+ if (p < 0) {
+ if (Res_GETPACKAGE(resID)+1 == 0) {
+ ALOGW("No package identifier when getting flags for resource number 0x%08x", resID);
+ } else {
+ ALOGW("No known package when getting flags for resource number 0x%08x", resID);
}
+ return false;
+ }
+ if (t < 0) {
+ ALOGW("No type identifier when getting flags for resource number 0x%08x", resID);
+ return false;
+ }
- *outFlags = entry.specFlags;
- return true;
+ const PackageGroup* const grp = mPackageGroups[p];
+ if (grp == NULL) {
+ ALOGW("Bad identifier when getting flags for resource number 0x%08x", resID);
+ return false;
+ }
+
+ Entry entry;
+ status_t err = getEntry(grp, t, e, NULL, &entry);
+ if (err != NO_ERROR) {
+ return false;
+ }
+
+ *outFlags = entry.entry->flags();
+ return true;
}
bool ResTable::isPackageDynamic(uint8_t packageID) const {
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 0d45149267cf..63b28da075cd 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -1593,6 +1593,8 @@ union ResTable_entry
// If set, this is a compact entry with data type and value directly
// encoded in the this entry, see ResTable_entry::compact
FLAG_COMPACT = 0x0008,
+ // If set, this entry relies on read write android feature flags
+ FLAG_USES_FEATURE_FLAGS = 0x0010,
};
struct Full {
@@ -1622,6 +1624,7 @@ union ResTable_entry
uint16_t flags() const { return dtohs(full.flags); };
bool is_compact() const { return flags() & FLAG_COMPACT; }
bool is_complex() const { return flags() & FLAG_COMPLEX; }
+ bool uses_feature_flags() const { return flags() & FLAG_USES_FEATURE_FLAGS; }
size_t size() const {
return is_compact() ? sizeof(ResTable_entry) : dtohs(this->full.size);
@@ -2039,6 +2042,8 @@ public:
bool getResourceFlags(uint32_t resID, uint32_t* outFlags) const;
+ bool getResourceEntryFlags(uint32_t resID, uint32_t* outFlags) const;
+
/**
* Returns whether or not the package for the given resource has been dynamically assigned.
* If the resource can't be found, returns 'false'.
diff --git a/libs/hostgraphics/HostBufferQueue.cpp b/libs/hostgraphics/HostBufferQueue.cpp
index 7e14b88a47fa..ef5406250251 100644
--- a/libs/hostgraphics/HostBufferQueue.cpp
+++ b/libs/hostgraphics/HostBufferQueue.cpp
@@ -29,6 +29,7 @@ public:
}
virtual status_t detachBuffer(int slot) {
+ mBuffer.clear();
return OK;
}
diff --git a/media/java/android/media/IMediaRouter2.aidl b/media/java/android/media/IMediaRouter2.aidl
index 85bc8efe2750..e9590d50d719 100644
--- a/media/java/android/media/IMediaRouter2.aidl
+++ b/media/java/android/media/IMediaRouter2.aidl
@@ -18,6 +18,7 @@ package android.media;
import android.media.MediaRoute2Info;
import android.media.RoutingSessionInfo;
+import android.media.SuggestedDeviceInfo;
import android.os.Bundle;
import android.os.UserHandle;
@@ -37,4 +38,6 @@ oneway interface IMediaRouter2 {
*/
void requestCreateSessionByManager(long uniqueRequestId, in RoutingSessionInfo oldSession,
in MediaRoute2Info route);
+ void notifyDeviceSuggestionsUpdated(String suggestingPackageName,
+ in List<SuggestedDeviceInfo> suggestions);
}
diff --git a/media/java/android/media/IMediaRouter2Manager.aidl b/media/java/android/media/IMediaRouter2Manager.aidl
index 21908b2ca2e0..1c399d6958bb 100644
--- a/media/java/android/media/IMediaRouter2Manager.aidl
+++ b/media/java/android/media/IMediaRouter2Manager.aidl
@@ -21,6 +21,7 @@ import android.media.MediaRoute2Info;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
+import android.media.SuggestedDeviceInfo;
/**
* {@hide}
@@ -33,6 +34,8 @@ oneway interface IMediaRouter2Manager {
in RouteDiscoveryPreference discoveryPreference);
void notifyRouteListingPreferenceChange(String packageName,
in @nullable RouteListingPreference routeListingPreference);
+ void notifyDeviceSuggestionsUpdated(String packageName, String suggestingPackageName,
+ in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo);
void notifyRoutesUpdated(in List<MediaRoute2Info> routes);
void notifyRequestFailed(int requestId, int reason);
void invalidateInstance();
diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl
index 961962f6a010..60881f4bfc30 100644
--- a/media/java/android/media/IMediaRouterService.aidl
+++ b/media/java/android/media/IMediaRouterService.aidl
@@ -25,6 +25,7 @@ import android.media.MediaRouterClientState;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
+import android.media.SuggestedDeviceInfo;
import android.os.Bundle;
import android.os.UserHandle;
/**
@@ -72,6 +73,10 @@ interface IMediaRouterService {
in MediaRoute2Info route);
void setSessionVolumeWithRouter2(IMediaRouter2 router, String sessionId, int volume);
void releaseSessionWithRouter2(IMediaRouter2 router, String sessionId);
+ void setDeviceSuggestionsWithRouter2(IMediaRouter2 router,
+ in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo);
+ @nullable Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2(
+ IMediaRouter2 router);
// Methods for MediaRouter2Manager
List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager);
@@ -98,4 +103,8 @@ interface IMediaRouterService {
String sessionId, int volume);
void releaseSessionWithManager(IMediaRouter2Manager manager, int requestId, String sessionId);
boolean showMediaOutputSwitcherWithProxyRouter(IMediaRouter2Manager manager);
+ void setDeviceSuggestionsWithManager(IMediaRouter2Manager manager,
+ in @nullable List<SuggestedDeviceInfo> suggestedDeviceInfo);
+ @nullable Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager(
+ IMediaRouter2Manager manager);
}
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index 4e86eacea404..f3b21bfdaa3c 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -3836,6 +3836,151 @@ public final class MediaCodecInfo {
maxBlocks, maxBlocksPerSecond,
blockSize, blockSize,
1 /* widthAlignment */, 1 /* heightAlignment */);
+ } else if (GetFlag(() -> android.media.codec.Flags.apvSupport())
+ && mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_APV)) {
+ maxBlocksPerSecond = 11880;
+ maxBps = 7000000;
+
+ // Sample rate, and Bit rate for APV Codec,
+ // corresponding to the definitions in
+ // "10.1.4. Levels and bands"
+ // found at https://www.ietf.org/archive/id/draft-lim-apv-03.html
+ for (CodecProfileLevel profileLevel: profileLevels) {
+ long SR = 0; // luma sample rate
+ int BR = 0; // bit rate bps
+ switch (profileLevel.level) {
+ case CodecProfileLevel.APVLevel1Band0:
+ SR = 3041280; BR = 7000000; break;
+ case CodecProfileLevel.APVLevel1Band1:
+ SR = 3041280; BR = 11000000; break;
+ case CodecProfileLevel.APVLevel1Band2:
+ SR = 3041280; BR = 14000000; break;
+ case CodecProfileLevel.APVLevel1Band3:
+ SR = 3041280; BR = 21000000; break;
+ case CodecProfileLevel.APVLevel11Band0:
+ SR = 6082560; BR = 14000000; break;
+ case CodecProfileLevel.APVLevel11Band1:
+ SR = 6082560; BR = 21000000; break;
+ case CodecProfileLevel.APVLevel11Band2:
+ SR = 6082560; BR = 28000000; break;
+ case CodecProfileLevel.APVLevel11Band3:
+ SR = 6082560; BR = 42000000; break;
+ case CodecProfileLevel.APVLevel2Band0:
+ SR = 15667200; BR = 36000000; break;
+ case CodecProfileLevel.APVLevel2Band1:
+ SR = 15667200; BR = 53000000; break;
+ case CodecProfileLevel.APVLevel2Band2:
+ SR = 15667200; BR = 71000000; break;
+ case CodecProfileLevel.APVLevel2Band3:
+ SR = 15667200; BR = 106000000; break;
+ case CodecProfileLevel.APVLevel21Band0:
+ SR = 31334400; BR = 71000000; break;
+ case CodecProfileLevel.APVLevel21Band1:
+ SR = 31334400; BR = 106000000; break;
+ case CodecProfileLevel.APVLevel21Band2:
+ SR = 31334400; BR = 141000000; break;
+ case CodecProfileLevel.APVLevel21Band3:
+ SR = 31334400; BR = 212000000; break;
+ case CodecProfileLevel.APVLevel3Band0:
+ SR = 66846720; BR = 101000000; break;
+ case CodecProfileLevel.APVLevel3Band1:
+ SR = 66846720; BR = 151000000; break;
+ case CodecProfileLevel.APVLevel3Band2:
+ SR = 66846720; BR = 201000000; break;
+ case CodecProfileLevel.APVLevel3Band3:
+ SR = 66846720; BR = 301000000; break;
+ case CodecProfileLevel.APVLevel31Band0:
+ SR = 133693440; BR = 201000000; break;
+ case CodecProfileLevel.APVLevel31Band1:
+ SR = 133693440; BR = 301000000; break;
+ case CodecProfileLevel.APVLevel31Band2:
+ SR = 133693440; BR = 401000000; break;
+ case CodecProfileLevel.APVLevel31Band3:
+ SR = 133693440; BR = 602000000; break;
+ case CodecProfileLevel.APVLevel4Band0:
+ SR = 265420800; BR = 401000000; break;
+ case CodecProfileLevel.APVLevel4Band1:
+ SR = 265420800; BR = 602000000; break;
+ case CodecProfileLevel.APVLevel4Band2:
+ SR = 265420800; BR = 780000000; break;
+ case CodecProfileLevel.APVLevel4Band3:
+ SR = 265420800; BR = 1170000000; break;
+ case CodecProfileLevel.APVLevel41Band0:
+ SR = 530841600; BR = 780000000; break;
+ case CodecProfileLevel.APVLevel41Band1:
+ SR = 530841600; BR = 1170000000; break;
+ case CodecProfileLevel.APVLevel41Band2:
+ SR = 530841600; BR = 1560000000; break;
+ case CodecProfileLevel.APVLevel41Band3:
+ // Current API allows bitrates only up to Max Integer
+ // Hence we are limiting internal limits to Integer.MAX_VALUE
+ // even when actual Level/Band limits are higher
+ SR = 530841600; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel5Band0:
+ SR = 1061683200; BR = 1560000000; break;
+ case CodecProfileLevel.APVLevel5Band1:
+ SR = 1061683200; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel5Band2:
+ SR = 1061683200; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel5Band3:
+ SR = 1061683200; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel51Band0:
+ case CodecProfileLevel.APVLevel51Band1:
+ case CodecProfileLevel.APVLevel51Band2:
+ case CodecProfileLevel.APVLevel51Band3:
+ SR = 2123366400; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel6Band0:
+ case CodecProfileLevel.APVLevel6Band1:
+ case CodecProfileLevel.APVLevel6Band2:
+ case CodecProfileLevel.APVLevel6Band3:
+ SR = 4777574400L; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel61Band0:
+ case CodecProfileLevel.APVLevel61Band1:
+ case CodecProfileLevel.APVLevel61Band2:
+ case CodecProfileLevel.APVLevel61Band3:
+ SR = 8493465600L; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel7Band0:
+ case CodecProfileLevel.APVLevel7Band1:
+ case CodecProfileLevel.APVLevel7Band2:
+ case CodecProfileLevel.APVLevel7Band3:
+ SR = 16986931200L; BR = Integer.MAX_VALUE; break;
+ case CodecProfileLevel.APVLevel71Band0:
+ case CodecProfileLevel.APVLevel71Band1:
+ case CodecProfileLevel.APVLevel71Band2:
+ case CodecProfileLevel.APVLevel71Band3:
+ SR = 33973862400L; BR = Integer.MAX_VALUE; break;
+ default:
+ Log.w(TAG, "Unrecognized level "
+ + profileLevel.level + " for " + mime);
+ errors |= ERROR_UNRECOGNIZED;
+ }
+ switch (profileLevel.profile) {
+ case CodecProfileLevel.APVProfile422_10:
+ case CodecProfileLevel.APVProfile422_10HDR10:
+ case CodecProfileLevel.APVProfile422_10HDR10Plus:
+ break;
+ default:
+ Log.w(TAG, "Unrecognized profile "
+ + profileLevel.profile + " for " + mime);
+ errors |= ERROR_UNRECOGNIZED;
+ }
+ errors &= ~ERROR_NONE_SUPPORTED;
+ maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond);
+ maxBps = Math.max(BR, maxBps);
+ }
+
+ final int blockSize = 16;
+ maxBlocks = Integer.MAX_VALUE;
+ maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize);
+ maxBlocks = (int) Math.min((long) maxBlocks, maxBlocksPerSecond);
+ // Max frame size in APV is 2^24
+ int maxLengthInBlocks = Utils.divUp((int) Math.pow(2, 24), blockSize);
+ maxLengthInBlocks = Math.min(maxLengthInBlocks, maxBlocks);
+ applyMacroBlockLimits(
+ maxLengthInBlocks, maxLengthInBlocks,
+ maxBlocks, maxBlocksPerSecond,
+ blockSize, blockSize,
+ 2 /* widthAlignment */, 1 /* heightAlignment */);
} else {
Log.w(TAG, "Unsupported mime " + mime);
// using minimal bitrate here. should be overriden by
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 3af36a404c30..db305effac3f 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -22,6 +22,7 @@ import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES;
import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL;
import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2;
import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING;
+import static com.android.media.flags.Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API;
import android.Manifest;
import android.annotation.CallbackExecutor;
@@ -159,6 +160,8 @@ public final class MediaRouter2 {
new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<ControllerCallbackRecord> mControllerCallbackRecords =
new CopyOnWriteArrayList<>();
+ private final CopyOnWriteArrayList<DeviceSuggestionsCallbackRecord>
+ mDeviceSuggestionsCallbackRecords = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<ControllerCreationRequest> mControllerCreationRequests =
new CopyOnWriteArrayList<>();
@@ -198,6 +201,10 @@ public final class MediaRouter2 {
@Nullable
private RouteListingPreference mRouteListingPreference;
+ @GuardedBy("mLock")
+ @Nullable
+ private Map<String, List<SuggestedDeviceInfo>> mSuggestedDeviceInfo = new HashMap<>();
+
/**
* Stores an auxiliary copy of {@link #mFilteredRoutes} at the time of the last route callback
* dispatch. This is only used to determine what callback a route should be assigned to (added,
@@ -760,6 +767,27 @@ public final class MediaRouter2 {
}
/**
+ * Registers the given callback to be invoked when the {@link SuggestedDeviceInfo} of the target
+ * router changes.
+ *
+ * <p>Calls using a previously registered callback will overwrite the previous executor.
+ *
+ * @hide
+ */
+ public void registerDeviceSuggestionsCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback) {
+ Objects.requireNonNull(executor, "executor must not be null");
+ Objects.requireNonNull(deviceSuggestionsCallback, "callback must not be null");
+
+ DeviceSuggestionsCallbackRecord record =
+ new DeviceSuggestionsCallbackRecord(executor, deviceSuggestionsCallback);
+
+ mDeviceSuggestionsCallbackRecords.remove(record);
+ mDeviceSuggestionsCallbackRecords.add(record);
+ }
+
+ /**
* Unregisters the given callback to not receive {@link RouteListingPreference} change events.
*
* @see #registerRouteListingPreferenceUpdatedCallback(Executor, Consumer)
@@ -779,6 +807,21 @@ public final class MediaRouter2 {
}
/**
+ * Unregisters the given callback to not receive {@link SuggestedDeviceInfo} change events.
+ *
+ * @see #registerDeviceSuggestionsCallback(Executor, DeviceSuggestionsCallback)
+ * @hide
+ */
+ public void unregisterDeviceSuggestionsCallback(@NonNull DeviceSuggestionsCallback callback) {
+ Objects.requireNonNull(callback, "callback must not be null");
+
+ if (!mDeviceSuggestionsCallbackRecords.remove(
+ new DeviceSuggestionsCallbackRecord(/* executor */ null, callback))) {
+ Log.w(TAG, "unregisterDeviceSuggestionsCallback: Ignoring an unknown" + " callback");
+ }
+ }
+
+ /**
* Shows the system output switcher dialog.
*
* <p>Should only be called when the context of MediaRouter2 is in the foreground and visible on
@@ -832,6 +875,36 @@ public final class MediaRouter2 {
}
/**
+ * Sets the suggested devices.
+ *
+ * <p>Use this method to inform the system UI that this device is suggested in the Output
+ * Switcher and media controls.
+ *
+ * <p>You should pass null to this method to clear a previously set suggestion without setting a
+ * new one.
+ *
+ * @param suggestedDeviceInfo The {@link SuggestedDeviceInfo} the router suggests should be
+ * provided to the user.
+ * @hide
+ */
+ @FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API)
+ public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ mImpl.setDeviceSuggestions(suggestedDeviceInfo);
+ }
+
+ /**
+ * Gets the current suggested devices.
+ *
+ * @return the suggested devices, keyed by the package name providing each suggestion list.
+ * @hide
+ */
+ @FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API)
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() {
+ return mImpl.getDeviceSuggestions();
+ }
+
+ /**
* Returns the current {@link RouteListingPreference} of the target router.
*
* <p>If this instance was created using {@code #getInstance(Context, String)}, then it returns
@@ -1518,6 +1591,17 @@ public final class MediaRouter2 {
}
}
+ private void notifyDeviceSuggestionsUpdated(
+ @NonNull String suggestingPackageName,
+ @Nullable List<SuggestedDeviceInfo> deviceSuggestions) {
+ for (DeviceSuggestionsCallbackRecord record : mDeviceSuggestionsCallbackRecords) {
+ record.mExecutor.execute(
+ () ->
+ record.mDeviceSuggestionsCallback.onSuggestionUpdated(
+ suggestingPackageName, deviceSuggestions));
+ }
+ }
+
private void notifyTransfer(RoutingController oldController, RoutingController newController) {
for (TransferCallbackRecord record : mTransferCallbackRecords) {
record.mExecutor.execute(
@@ -1568,6 +1652,25 @@ public final class MediaRouter2 {
.build();
}
+ /**
+ * Callback for receiving events about device suggestions
+ *
+ * @hide
+ */
+ public interface DeviceSuggestionsCallback {
+
+ /**
+ * Called when suggestions are updated. Whenever you register a callback, this will be
+ * invoked with the current suggestions.
+ *
+ * @param suggestingPackageName the package that provided the suggestions.
+ * @param suggestedDeviceInfo the suggestions provided by the package.
+ */
+ void onSuggestionUpdated(
+ @NonNull String suggestingPackageName,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo);
+ }
+
/** Callback for receiving events about media route discovery. */
public abstract static class RouteCallback {
/**
@@ -2326,6 +2429,35 @@ public final class MediaRouter2 {
}
}
+ private static final class DeviceSuggestionsCallbackRecord {
+ public final Executor mExecutor;
+ public final DeviceSuggestionsCallback mDeviceSuggestionsCallback;
+
+ /* package */ DeviceSuggestionsCallbackRecord(
+ @NonNull Executor executor,
+ @NonNull DeviceSuggestionsCallback deviceSuggestionsCallback) {
+ mExecutor = executor;
+ mDeviceSuggestionsCallback = deviceSuggestionsCallback;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof DeviceSuggestionsCallbackRecord)) {
+ return false;
+ }
+ return mDeviceSuggestionsCallback
+ == ((DeviceSuggestionsCallbackRecord) obj).mDeviceSuggestionsCallback;
+ }
+
+ @Override
+ public int hashCode() {
+ return mDeviceSuggestionsCallback.hashCode();
+ }
+ }
+
static final class TransferCallbackRecord {
public final Executor mExecutor;
public final TransferCallback mTransferCallback;
@@ -2446,6 +2578,17 @@ public final class MediaRouter2 {
}
@Override
+ public void notifyDeviceSuggestionsUpdated(
+ String suggestingPackageName, List<SuggestedDeviceInfo> suggestions) {
+ mHandler.sendMessage(
+ obtainMessage(
+ MediaRouter2::notifyDeviceSuggestionsUpdated,
+ MediaRouter2.this,
+ suggestingPackageName,
+ suggestions));
+ }
+
+ @Override
public void requestCreateSessionByManager(
long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route) {
mHandler.sendMessage(
@@ -2487,6 +2630,11 @@ public final class MediaRouter2 {
void setRouteListingPreference(@Nullable RouteListingPreference preference);
+ void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo);
+
+ @Nullable
+ Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions();
+
boolean showSystemOutputSwitcher();
List<MediaRoute2Info> getAllRoutes();
@@ -2687,6 +2835,29 @@ public final class MediaRouter2 {
}
@Override
+ public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ synchronized (mLock) {
+ try {
+ mMediaRouterService.setDeviceSuggestionsWithManager(
+ mClient, suggestedDeviceInfo);
+ } catch (RemoteException ex) {
+ ex.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ @Override
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() {
+ synchronized (mLock) {
+ try {
+ return mMediaRouterService.getDeviceSuggestionsWithManager(mClient);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ @Override
public boolean showSystemOutputSwitcher() {
try {
return mMediaRouterService.showMediaOutputSwitcherWithProxyRouter(mClient);
@@ -3296,6 +3467,23 @@ public final class MediaRouter2 {
notifyRouteListingPreferenceUpdated(routeListingPreference);
}
+ private void onDeviceSuggestionsChangeHandler(
+ @NonNull String packageName,
+ @NonNull String suggestingPackageName,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ if (!TextUtils.equals(getClientPackageName(), packageName)) {
+ return;
+ }
+ synchronized (mLock) {
+ if (Objects.equals(
+ mSuggestedDeviceInfo.get(suggestingPackageName), suggestedDeviceInfo)) {
+ return;
+ }
+ mSuggestedDeviceInfo.put(suggestingPackageName, suggestedDeviceInfo);
+ }
+ notifyDeviceSuggestionsUpdated(suggestingPackageName, suggestedDeviceInfo);
+ }
+
private void onRequestFailedOnHandler(int requestId, int reason) {
MediaRouter2Manager.TransferRequest matchingRequest = null;
for (MediaRouter2Manager.TransferRequest request : mTransferRequests) {
@@ -3390,6 +3578,20 @@ public final class MediaRouter2 {
}
@Override
+ public void notifyDeviceSuggestionsUpdated(
+ String packageName,
+ String suggestingPackageName,
+ @Nullable List<SuggestedDeviceInfo> deviceSuggestions) {
+ mHandler.sendMessage(
+ obtainMessage(
+ ProxyMediaRouter2Impl::onDeviceSuggestionsChangeHandler,
+ ProxyMediaRouter2Impl.this,
+ packageName,
+ suggestingPackageName,
+ deviceSuggestions));
+ }
+
+ @Override
public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
mHandler.sendMessage(
obtainMessage(
@@ -3553,6 +3755,30 @@ public final class MediaRouter2 {
}
@Override
+ public void setDeviceSuggestions(@Nullable List<SuggestedDeviceInfo> deviceSuggestions) {
+ synchronized (mLock) {
+ try {
+ registerRouterStubIfNeededLocked();
+ mMediaRouterService.setDeviceSuggestionsWithRouter2(mStub, deviceSuggestions);
+ } catch (RemoteException ex) {
+ ex.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ @Override
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestions() {
+ synchronized (mLock) {
+ try {
+ return mMediaRouterService.getDeviceSuggestionsWithRouter2(mStub);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ @Override
public boolean showSystemOutputSwitcher() {
synchronized (mLock) {
try {
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index 3f18eef2f9aa..bf88709eec33 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -1138,6 +1138,14 @@ public final class MediaRouter2Manager {
}
@Override
+ public void notifyDeviceSuggestionsUpdated(
+ String packageName,
+ String suggestingPackageName,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ // MediaRouter2Manager doesn't support device suggestions
+ }
+
+ @Override
public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
mHandler.sendMessage(
obtainMessage(
diff --git a/media/java/android/media/SuggestedDeviceInfo.aidl b/media/java/android/media/SuggestedDeviceInfo.aidl
new file mode 100644
index 000000000000..eab642572ed2
--- /dev/null
+++ b/media/java/android/media/SuggestedDeviceInfo.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+parcelable SuggestedDeviceInfo;
diff --git a/media/java/android/media/SuggestedDeviceInfo.java b/media/java/android/media/SuggestedDeviceInfo.java
new file mode 100644
index 000000000000..2aa139fcca17
--- /dev/null
+++ b/media/java/android/media/SuggestedDeviceInfo.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+import static com.android.media.flags.Flags.FLAG_ENABLE_SUGGESTED_DEVICE_API;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * Allows applications to suggest routes to the system UI (for example, in the System UI Output
+ * Switcher).
+ *
+ * @see MediaRouter2#setSuggestedDevice
+ * @hide
+ */
+@FlaggedApi(FLAG_ENABLE_SUGGESTED_DEVICE_API)
+public final class SuggestedDeviceInfo implements Parcelable {
+ @NonNull
+ public static final Creator<SuggestedDeviceInfo> CREATOR =
+ new Creator<>() {
+ @Override
+ public SuggestedDeviceInfo createFromParcel(Parcel in) {
+ return new SuggestedDeviceInfo(in);
+ }
+
+ @Override
+ public SuggestedDeviceInfo[] newArray(int size) {
+ return new SuggestedDeviceInfo[size];
+ }
+ };
+
+ @NonNull private final String mDeviceDisplayName;
+
+ @NonNull private final String mRouteId;
+
+ private final int mType;
+
+ @NonNull private final Bundle mExtras;
+
+ private SuggestedDeviceInfo(Builder builder) {
+ mDeviceDisplayName = builder.mDeviceDisplayName;
+ mRouteId = builder.mRouteId;
+ mType = builder.mType;
+ mExtras = builder.mExtras;
+ }
+
+ private SuggestedDeviceInfo(Parcel in) {
+ mDeviceDisplayName = in.readString();
+ mRouteId = in.readString();
+ mType = in.readInt();
+ mExtras = in.readBundle();
+ }
+
+ /**
+ * Returns the name to be displayed to the user.
+ *
+ * @return The device display name.
+ */
+ @NonNull
+ public String getDeviceDisplayName() {
+ return mDeviceDisplayName;
+ }
+
+ /**
+ * Returns the route ID associated with the suggestion.
+ *
+ * @return The route ID.
+ */
+ @NonNull
+ public String getRouteId() {
+ return mRouteId;
+ }
+
+ /**
+ * Returns the device type associated with the suggestion.
+ *
+ * @return The device type.
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Returns the extras associated with the suggestion.
+ *
+ * @return The extras.
+ */
+ @Nullable
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ // SuggestedDeviceInfo Parcelable implementation.
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeString(mDeviceDisplayName);
+ dest.writeString(mRouteId);
+ dest.writeInt(mType);
+ dest.writeBundle(mExtras);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof SuggestedDeviceInfo)) {
+ return false;
+ }
+ return Objects.equals(mDeviceDisplayName, ((SuggestedDeviceInfo) obj).mDeviceDisplayName)
+ && Objects.equals(mRouteId, ((SuggestedDeviceInfo) obj).mRouteId)
+ && mType == ((SuggestedDeviceInfo) obj).mType;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDeviceDisplayName, mRouteId, mType);
+ }
+
+ @Override
+ public String toString() {
+ return mDeviceDisplayName + " | " + mRouteId + " | " + mType;
+ }
+
+ /** Builder for {@link SuggestedDeviceInfo}. */
+ public static final class Builder {
+ @NonNull private String mDeviceDisplayName;
+
+ @NonNull private String mRouteId;
+
+ @NonNull private Integer mType;
+
+ private Bundle mExtras = Bundle.EMPTY;
+
+ /**
+ * Creates a new SuggestedDeviceInfo. The device display name, route ID, and type must be
+ * set. The extras cannot be null, but default to an empty {@link Bundle}.
+ *
+ * @throws IllegalArgumentException if the builder has a mandatory unset field.
+ */
+ public SuggestedDeviceInfo build() {
+ if (mDeviceDisplayName == null) {
+ throw new IllegalArgumentException("Device display name cannot be null");
+ }
+
+ if (mRouteId == null) {
+ throw new IllegalArgumentException("Route ID cannot be null.");
+ }
+
+ if (mType == null) {
+ throw new IllegalArgumentException("Device type cannot be null.");
+ }
+
+ if (mExtras == null) {
+ throw new IllegalArgumentException("Extras cannot be null.");
+ }
+
+ return new SuggestedDeviceInfo(this);
+ }
+
+ /**
+ * Sets the {@link #getDeviceDisplayName() device display name}.
+ *
+ * @throws IllegalArgumentException if the name is null or empty.
+ */
+ public Builder setDeviceDisplayName(@NonNull String deviceDisplayName) {
+ if (TextUtils.isEmpty(deviceDisplayName)) {
+ throw new IllegalArgumentException("Device display name cannot be null");
+ }
+ mDeviceDisplayName = deviceDisplayName;
+ return this;
+ }
+
+ /**
+ * Sets the {@link #getRouteId() route id}.
+ *
+ * @throws IllegalArgumentException if the route id is null or empty.
+ */
+ public Builder setRouteId(@NonNull String routeId) {
+ if (TextUtils.isEmpty(routeId)) {
+ throw new IllegalArgumentException("Device display name cannot be null");
+ }
+ mRouteId = routeId;
+ return this;
+ }
+
+ /** Sets the {@link #getType() device type}. */
+ public Builder setType(int type) {
+ mType = type;
+ return this;
+ }
+
+ /**
+ * Sets the {@link #getExtras() extras}.
+ *
+ * <p>The default value is an empty {@link Bundle}.
+ *
+ * <p>Do not mutate the given {@link Bundle} after passing it to this method. You can use
+ * {@link Bundle#deepCopy()} to keep a mutable copy.
+ *
+ * @throws NullPointerException if the extras are null.
+ */
+ public Builder setExtras(@NonNull Bundle extras) {
+ mExtras = Objects.requireNonNull(extras, "extras must not be null");
+ return this;
+ }
+ }
+}
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index 0deed3982d9b..e39a0aa8717e 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -62,6 +62,14 @@ flag {
}
flag {
+ name: "enable_suggested_device_api"
+ is_exported: true
+ namespace: "media_better_together"
+ description: "Enables the API allowing proxy routers to suggest routes."
+ bug: "393216553"
+}
+
+flag {
name: "enable_full_scan_with_media_content_control"
namespace: "media_better_together"
description: "Allows holders of the MEDIA_CONTENT_CONTROL permission to scan for routes while not in the foreground."
diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java
index e4de3e4420fe..fccdba8e727f 100644
--- a/media/java/android/media/quality/MediaQualityContract.java
+++ b/media/java/android/media/quality/MediaQualityContract.java
@@ -82,7 +82,7 @@ public class MediaQualityContract {
String PARAMETER_NAME = "_name";
String PARAMETER_PACKAGE = "_package";
String PARAMETER_INPUT_ID = "_input_id";
-
+ String VENDOR_PARAMETERS = "_vendor_parameters";
}
/**
diff --git a/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl b/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl
index cdf6e23f4b47..40848fe6f875 100644
--- a/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl
+++ b/media/java/android/media/tv/extension/scan/IHDPlusInfo.aidl
@@ -21,5 +21,5 @@ package android.media.tv.extension.scan;
*/
interface IHDPlusInfo {
// Specifying a HDPlusInfo and start a network scan.
- int setHDPlusInfo(String isBlindScanContinue, String isHDMode);
+ int setHDPlusInfo(boolean isBlindScanContinue, boolean isHDMode);
}
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
index 0c64fa7d34dd..bf739620bc99 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java
@@ -187,7 +187,9 @@ public class IllustrationPreference extends Preference implements GroupSectionDi
if (mLottieDynamicColor) {
LottieColorUtils.applyDynamicColors(getContext(), illustrationView);
}
- LottieColorUtils.applyMaterialColor(getContext(), illustrationView);
+ if (SettingsThemeHelper.isExpressiveTheme(getContext())) {
+ LottieColorUtils.applyMaterialColor(getContext(), illustrationView);
+ }
if (mOnBindListener != null) {
mOnBindListener.onBind(illustrationView);
@@ -447,6 +449,10 @@ public class IllustrationPreference extends Preference implements GroupSectionDi
illustrationView.setMaxWidth((int) (restrictedMaxHeight * aspectRatio));
}
+ public boolean isAnimatable() {
+ return mIsAnimatable;
+ }
+
private void startAnimation(Drawable drawable) {
if (!(drawable instanceof Animatable)) {
return;
diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
index 4421424c0e39..e59cc81d3ba8 100644
--- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
+++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java
@@ -157,10 +157,6 @@ public class LottieColorUtils {
/** Applies material colors. */
public static void applyMaterialColor(@NonNull Context context,
@NonNull LottieAnimationView lottieAnimationView) {
- if (!SettingsThemeHelper.isExpressiveTheme(context)) {
- return;
- }
-
for (String key : MATERIAL_COLOR_MAP.keySet()) {
final int color = context.getColor(MATERIAL_COLOR_MAP.get(key));
lottieAnimationView.addValueCallback(
diff --git a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
index 0f6a2a082e0c..1170f1e7c695 100644
--- a/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
+++ b/packages/SettingsLib/SettingsSpinner/src/com/android/settingslib/widget/SettingsSpinnerPreference.java
@@ -19,6 +19,7 @@ package com.android.settingslib.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
import android.widget.AdapterView;
import android.widget.Spinner;
@@ -110,7 +111,6 @@ public class SettingsSpinnerPreference extends Preference
notifyChanged();
}
-
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
@@ -119,6 +119,18 @@ public class SettingsSpinnerPreference extends Preference
spinner.setSelection(mPosition);
spinner.setOnItemSelectedListener(mOnSelectedListener);
spinner.setLongClickable(false);
+ spinner.setAccessibilityDelegate(
+ new View.AccessibilityDelegate() {
+ @Override
+ public void sendAccessibilityEvent(View host, int eventType) {
+ if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) {
+ // Ignore the INTERRUPT events TYPE_VIEW_SELECTED or Talkback will speak
+ // for it while fragment updating.
+ return;
+ }
+ super.sendAccessibilityEvent(host, eventType);
+ }
+ });
if (mShouldPerformClick) {
mShouldPerformClick = false;
// To show dropdown view.
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 42f576a9a151..9dd49d6b1015 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1401,6 +1401,16 @@ flag {
}
flag {
+ name: "media_controls_device_manager_background_execution"
+ namespace: "systemui"
+ description: "Sends some instances creation to background thread"
+ bug: "400200474"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "output_switcher_redesign"
namespace: "systemui"
description: "Enables visual update for Media Output Switcher"
@@ -1843,6 +1853,16 @@ flag {
}
flag {
+ name: "disable_shade_visible_with_blur"
+ namespace: "systemui"
+ description: "Removes the check for a blur radius when determining shade window visibility"
+ bug: "356804470"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "notification_row_transparency"
namespace: "systemui"
description: "Enables transparency on the Notification Shade."
@@ -2121,3 +2141,10 @@ flag {
description: "Enables moving the launching window on top of the origin window in the Animation library."
bug: "390422470"
}
+
+flag {
+ name: "status_bar_chips_return_animations"
+ namespace: "systemui"
+ description: "Enables return animations for status bar chips"
+ bug: "202516970"
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
index 9a746870c6ff..e07d7b337ba2 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/FontVariationUtils.kt
@@ -16,17 +16,18 @@
package com.android.systemui.animation
+import kotlin.text.buildString
+
class FontVariationUtils {
private var mWeight = -1
private var mWidth = -1
private var mOpticalSize = -1
private var mRoundness = -1
- private var isUpdated = false
+ private var mCurrentFVar = ""
/*
* generate fontVariationSettings string, used for key in typefaceCache in TextAnimator
* the order of axes should align to the order of parameters
- * if every axis remains unchanged, return ""
*/
fun updateFontVariation(
weight: Int = -1,
@@ -34,15 +35,17 @@ class FontVariationUtils {
opticalSize: Int = -1,
roundness: Int = -1,
): String {
- isUpdated = false
+ var isUpdated = false
if (weight >= 0 && mWeight != weight) {
isUpdated = true
mWeight = weight
}
+
if (width >= 0 && mWidth != width) {
isUpdated = true
mWidth = width
}
+
if (opticalSize >= 0 && mOpticalSize != opticalSize) {
isUpdated = true
mOpticalSize = opticalSize
@@ -52,23 +55,32 @@ class FontVariationUtils {
isUpdated = true
mRoundness = roundness
}
- var resultString = ""
- if (mWeight >= 0) {
- resultString += "'${GSFAxes.WEIGHT.tag}' $mWeight"
- }
- if (mWidth >= 0) {
- resultString +=
- (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.WIDTH.tag}' $mWidth"
- }
- if (mOpticalSize >= 0) {
- resultString +=
- (if (resultString.isBlank()) "" else ", ") +
- "'${GSFAxes.OPTICAL_SIZE.tag}' $mOpticalSize"
- }
- if (mRoundness >= 0) {
- resultString +=
- (if (resultString.isBlank()) "" else ", ") + "'${GSFAxes.ROUND.tag}' $mRoundness"
+
+ if (!isUpdated) {
+ return mCurrentFVar
}
- return if (isUpdated) resultString else ""
+
+ return buildString {
+ if (mWeight >= 0) {
+ if (!isBlank()) append(", ")
+ append("'${GSFAxes.WEIGHT.tag}' $mWeight")
+ }
+
+ if (mWidth >= 0) {
+ if (!isBlank()) append(", ")
+ append("'${GSFAxes.WIDTH.tag}' $mWidth")
+ }
+
+ if (mOpticalSize >= 0) {
+ if (!isBlank()) append(", ")
+ append("'${GSFAxes.OPTICAL_SIZE.tag}' $mOpticalSize")
+ }
+
+ if (mRoundness >= 0) {
+ if (!isBlank()) append(", ")
+ append("'${GSFAxes.ROUND.tag}' $mRoundness")
+ }
+ }
+ .also { mCurrentFVar = it }
}
}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt
index d08d859ec0d7..fc4d53af4b53 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt
@@ -22,7 +22,6 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
@@ -32,7 +31,6 @@ import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.modifier.ModifierLocalModifierNode
import androidx.compose.ui.node.DrawModifierNode
@@ -50,11 +48,7 @@ import androidx.compose.ui.util.fastForEach
* The elements redirected to this container will be drawn above the content of this composable.
*/
fun Modifier.container(state: ContainerState): Modifier {
- return onPlaced { state.lastOffsetInWindow = it.positionInWindow() }
- .drawWithContent {
- drawContent()
- state.drawInOverlay(this)
- }
+ return this then ContainerElement(state)
}
/**
@@ -105,6 +99,30 @@ internal interface LayerRenderer {
fun drawInOverlay(drawScope: DrawScope)
}
+private data class ContainerElement(private val state: ContainerState) :
+ ModifierNodeElement<ContainerNode>() {
+ override fun create(): ContainerNode {
+ return ContainerNode(state)
+ }
+
+ override fun update(node: ContainerNode) {
+ node.state = state
+ }
+}
+
+/** A node implementing [container] that can be delegated to. */
+class ContainerNode(var state: ContainerState) :
+ Modifier.Node(), LayoutAwareModifierNode, DrawModifierNode {
+ override fun onPlaced(coordinates: LayoutCoordinates) {
+ state.lastOffsetInWindow = coordinates.positionInWindow()
+ }
+
+ override fun ContentDrawScope.draw() {
+ drawContent()
+ state.drawInOverlay(this)
+ }
+}
+
private data class DrawInContainerElement(
var state: ContainerState,
var enabled: () -> Boolean,
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 061fdd99eb1b..0a711487ccb1 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
@@ -356,7 +356,8 @@ private fun ContentScope.QuickSettingsScene(
modifier = Modifier.padding(horizontal = 16.dp),
)
}
- else -> CollapsedShadeHeader(viewModel = headerViewModel)
+ else ->
+ CollapsedShadeHeader(viewModel = headerViewModel, isSplitShade = false)
}
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 542f081fe469..8f0fb20cef36 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
@@ -258,15 +258,18 @@ fun ContentScope.QuickSettingsLayout(
modifier = Modifier.padding(horizontal = QuickSettingsShade.Dimensions.Padding),
)
- BrightnessSliderContainer(
- viewModel = viewModel.brightnessSliderViewModel,
- containerColors = ContainerColors.singleColor(OverlayShade.Colors.PanelBackground),
- modifier =
- Modifier.systemGestureExclusionInShade(
- enabled = { layoutState.transitionState is TransitionState.Idle }
- )
- .fillMaxWidth(),
- )
+ Box(
+ Modifier.systemGestureExclusionInShade(
+ enabled = { layoutState.transitionState is TransitionState.Idle }
+ )
+ ) {
+ BrightnessSliderContainer(
+ viewModel = viewModel.brightnessSliderViewModel,
+ containerColors =
+ ContainerColors.singleColor(OverlayShade.Colors.PanelBackground),
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
Box {
GridAnchor()
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 23baeacd76ec..86c8fc34a63c 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
@@ -127,6 +127,7 @@ object ShadeHeader {
@Composable
fun ContentScope.CollapsedShadeHeader(
viewModel: ShadeHeaderViewModel,
+ isSplitShade: Boolean,
modifier: Modifier = Modifier,
) {
val cutoutLocation = LocalDisplayCutout.current.location
@@ -141,8 +142,6 @@ fun ContentScope.CollapsedShadeHeader(
}
}
- 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.
@@ -154,7 +153,7 @@ fun ContentScope.CollapsedShadeHeader(
horizontalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.padding(horizontal = horizontalPadding),
) {
- Clock(scale = 1f, onClick = viewModel::onClockClicked)
+ Clock(onClick = viewModel::onClockClicked)
VariableDayDate(
longerDateText = viewModel.longerDateText,
shorterDateText = viewModel.shorterDateText,
@@ -184,11 +183,11 @@ fun ContentScope.CollapsedShadeHeader(
Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
.padding(horizontal = horizontalPadding),
) {
- if (isShadeLayoutWide) {
+ if (isSplitShade) {
ShadeCarrierGroup(viewModel = viewModel)
}
SystemIconChip(
- onClick = viewModel::onSystemIconChipClicked.takeIf { isShadeLayoutWide }
+ onClick = viewModel::onSystemIconChipClicked.takeIf { isSplitShade }
) {
StatusIcons(
viewModel = viewModel,
@@ -233,13 +232,11 @@ fun ContentScope.ExpandedShadeHeader(
.defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight),
) {
Box(modifier = Modifier.fillMaxWidth()) {
- Box {
- Clock(
- scale = 2.57f,
- onClick = viewModel::onClockClicked,
- modifier = Modifier.align(Alignment.CenterStart),
- )
- }
+ Clock(
+ onClick = viewModel::onClockClicked,
+ modifier = Modifier.align(Alignment.CenterStart),
+ scale = 2.57f,
+ )
Box(
modifier =
Modifier.element(ShadeHeader.Elements.ShadeCarrierGroup).fillMaxWidth()
@@ -291,8 +288,6 @@ fun ContentScope.OverlayShadeHeader(
val horizontalPadding =
max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)
- 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.
@@ -301,16 +296,15 @@ fun ContentScope.OverlayShadeHeader(
startContent = {
Row(
verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(5.dp),
modifier = Modifier.padding(horizontal = horizontalPadding),
) {
val chipHighlight = viewModel.notificationsChipHighlight
- if (isShadeLayoutWide) {
+ if (viewModel.showClock) {
Clock(
- scale = 1f,
onClick = viewModel::onClockClicked,
modifier = Modifier.padding(horizontal = 4.dp),
)
- Spacer(modifier = Modifier.width(5.dp))
}
NotificationsChip(
onClick = viewModel::onNotificationIconChipClicked,
@@ -437,7 +431,11 @@ private fun CutoutAwareShadeHeader(
}
@Composable
-private fun ContentScope.Clock(scale: Float, onClick: () -> Unit, modifier: Modifier = Modifier) {
+private fun ContentScope.Clock(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ scale: Float = 1f,
+) {
val layoutDirection = LocalLayoutDirection.current
ElementWithValues(key = ShadeHeader.Elements.Clock, modifier = modifier) {
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 5040490da8f6..885d34fb95c9 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
@@ -56,11 +56,11 @@ import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
+import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
@@ -68,6 +68,7 @@ import com.android.compose.animation.scene.LowestZIndexContentPicker
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.animateContentDpAsState
+import com.android.compose.animation.scene.animateContentFloatAsState
import com.android.compose.animation.scene.animateSceneFloatAsState
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.modifiers.padding
@@ -223,9 +224,6 @@ private fun ContentScope.ShadeScene(
viewModel = viewModel,
headerViewModel = headerViewModel,
notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
- createTintedIconManager = createTintedIconManager,
- createBatteryMeterViewController = createBatteryMeterViewController,
- statusBarIconController = statusBarIconController,
mediaCarouselController = mediaCarouselController,
mediaHost = qqsMediaHost,
modifier = modifier,
@@ -253,9 +251,6 @@ private fun ContentScope.SingleShade(
viewModel: ShadeSceneContentViewModel,
headerViewModel: ShadeHeaderViewModel,
notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
- createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
- createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
- statusBarIconController: StatusBarIconController,
mediaCarouselController: MediaCarouselController,
mediaHost: MediaHost,
modifier: Modifier = Modifier,
@@ -340,6 +335,7 @@ private fun ContentScope.SingleShade(
content = {
CollapsedShadeHeader(
viewModel = headerViewModel,
+ isSplitShade = false,
modifier = Modifier.layoutId(SingleShadeMeasurePolicy.LayoutId.ShadeHeader),
)
@@ -434,15 +430,13 @@ private fun ContentScope.SplitShade(
val footerActionsViewModel =
remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
val tileSquishiness by
- animateSceneFloatAsState(
+ animateContentFloatAsState(
value = 1f,
key = QuickSettings.SharedValues.TilesSquishiness,
canOverflow = false,
)
val unfoldTranslationXForStartSide by
viewModel.unfoldTranslationX(isOnStartSide = true).collectAsStateWithLifecycle(0f)
- val unfoldTranslationXForEndSide by
- viewModel.unfoldTranslationX(isOnStartSide = false).collectAsStateWithLifecycle(0f)
val notificationStackPadding = dimensionResource(id = R.dimen.notification_side_paddings)
val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
@@ -512,6 +506,7 @@ private fun ContentScope.SplitShade(
Column(modifier = Modifier.fillMaxSize()) {
CollapsedShadeHeader(
viewModel = headerViewModel,
+ isSplitShade = true,
modifier =
Modifier.then(brightnessMirrorShowingModifier)
.padding(horizontal = { unfoldTranslationXForStartSide.roundToInt() }),
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index 72ee75ad2d47..90bf92ae1dd0 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
@@ -32,12 +32,17 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ApproachLayoutModifierNode
+import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LookaheadScope
-import androidx.compose.ui.layout.approachLayout
-import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.zIndex
import com.android.compose.animation.scene.Ancestor
import com.android.compose.animation.scene.AnimatedState
import com.android.compose.animation.scene.ContentKey
@@ -68,8 +73,8 @@ import com.android.compose.gesture.NestedScrollControlState
import com.android.compose.gesture.NestedScrollableBound
import com.android.compose.gesture.nestedScrollController
import com.android.compose.modifiers.thenIf
+import com.android.compose.ui.graphics.ContainerNode
import com.android.compose.ui.graphics.ContainerState
-import com.android.compose.ui.graphics.container
import kotlin.math.pow
/** A content defined in a [SceneTransitionLayout], i.e. a scene or an overlay. */
@@ -158,24 +163,14 @@ internal sealed class Content(
fun Content(modifier: Modifier = Modifier, isInvisible: Boolean = false) {
// If this content has a custom factory, provide it to the content so that the factory is
// automatically used when calling rememberOverscrollEffect().
+ val isElevationPossible =
+ layoutImpl.state.isElevationPossible(content = key, element = null)
Box(
- modifier
- .thenIf(isInvisible) { InvisibleModifier }
- .zIndex(zIndex)
- .approachLayout(
- isMeasurementApproachInProgress = { layoutImpl.state.isTransitioning() }
- ) { measurable, constraints ->
- // TODO(b/353679003): Use the ModifierNode API to set this *before* the
- // approach
- // pass is started.
- targetSize = lookaheadSize
- val placeable = measurable.measure(constraints)
- layout(placeable.width, placeable.height) { placeable.place(0, 0) }
- }
- .thenIf(layoutImpl.state.isElevationPossible(content = key, element = null)) {
- Modifier.container(containerState)
- }
- .thenIf(layoutImpl.implicitTestTags) { Modifier.testTag(key.testTag) }
+ modifier.then(ContentElement(this, isElevationPossible, isInvisible)).thenIf(
+ layoutImpl.implicitTestTags
+ ) {
+ Modifier.testTag(key.testTag)
+ }
) {
CompositionLocalProvider(LocalOverscrollFactory provides lastFactory) {
scope.content()
@@ -194,6 +189,72 @@ internal sealed class Content(
}
}
+private data class ContentElement(
+ private val content: Content,
+ private val isElevationPossible: Boolean,
+ private val isInvisible: Boolean,
+) : ModifierNodeElement<ContentNode>() {
+ override fun create(): ContentNode = ContentNode(content, isElevationPossible, isInvisible)
+
+ override fun update(node: ContentNode) {
+ node.update(content, isElevationPossible, isInvisible)
+ }
+}
+
+private class ContentNode(
+ private var content: Content,
+ private var isElevationPossible: Boolean,
+ private var isInvisible: Boolean,
+) : DelegatingNode(), ApproachLayoutModifierNode {
+ private var containerDelegate = containerDelegate(isElevationPossible)
+
+ private fun containerDelegate(isElevationPossible: Boolean): ContainerNode? {
+ return if (isElevationPossible) delegate(ContainerNode(content.containerState)) else null
+ }
+
+ fun update(content: Content, isElevationPossible: Boolean, isInvisible: Boolean) {
+ if (content != this.content || isElevationPossible != this.isElevationPossible) {
+ this.content = content
+ this.isElevationPossible = isElevationPossible
+
+ containerDelegate?.let { undelegate(it) }
+ containerDelegate = containerDelegate(isElevationPossible)
+ }
+
+ this.isInvisible = isInvisible
+ }
+
+ override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean = false
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints,
+ ): MeasureResult {
+ check(isLookingAhead)
+ return measurable.measure(constraints).run {
+ content.targetSize = IntSize(width, height)
+ layout(width, height) {
+ if (!isInvisible) {
+ place(0, 0, zIndex = content.zIndex)
+ }
+ }
+ }
+ }
+
+ override fun ApproachMeasureScope.approachMeasure(
+ measurable: Measurable,
+ constraints: Constraints,
+ ): MeasureResult {
+ return measurable.measure(constraints).run {
+ layout(width, height) {
+ if (!isInvisible) {
+ place(0, 0, zIndex = content.zIndex)
+ }
+ }
+ }
+ }
+}
+
internal class ContentEffects(factory: OverscrollFactory) {
val overscrollEffect = factory.createOverscrollEffect()
val gestureEffect = GestureEffect(overscrollEffect)
@@ -307,8 +368,3 @@ internal class ContentScopeImpl(
)
}
}
-
-private val InvisibleModifier =
- Modifier.layout { measurable, constraints ->
- measurable.measure(constraints).run { layout(width, height) {} }
- }
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json
index 0fcdfa3e1b53..57f67665242c 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragFullyClose.json
@@ -60,7 +60,9 @@
912,
928,
944,
- 960
+ 960,
+ 976,
+ 992
],
"features": [
{
@@ -310,6 +312,14 @@
{
"x": 64,
"y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
}
]
},
@@ -491,67 +501,75 @@
},
{
"width": 170.4,
- "height": 39.2
+ "height": 28.8
},
{
"width": 166.8,
- "height": 36
+ "height": 15.2
},
{
"width": 164,
- "height": 31.6
+ "height": 6.4
},
{
"width": 162.4,
- "height": 26.8
+ "height": 0.8
},
{
"width": 161.2,
- "height": 22
+ "height": 0
},
{
"width": 160.4,
- "height": 17.6
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
},
{
"width": 160,
- "height": 14
+ "height": 0
},
{
"width": 160,
- "height": 10.8
+ "height": 0
},
{
"width": 160,
- "height": 8
+ "height": 0
},
{
"width": 160,
- "height": 6
+ "height": 0
},
{
"width": 160,
- "height": 4.4
+ "height": 0
},
{
"width": 160,
- "height": 2.8
+ "height": 0
},
{
"width": 160,
- "height": 2
+ "height": 0
},
{
"width": 160,
- "height": 1.2
+ "height": 0
},
{
"width": 160,
- "height": 0.8
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
},
{
"width": 160,
@@ -627,6 +645,8 @@
0,
0,
0,
+ 0,
+ 0,
0
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json
index 3196334c5314..01bc852cf7f4 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragHalfClose.json
@@ -59,7 +59,9 @@
896,
912,
928,
- 944
+ 944,
+ 960,
+ 976
],
"features": [
{
@@ -305,6 +307,14 @@
{
"x": 64,
"y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
}
]
},
@@ -482,71 +492,79 @@
},
{
"width": 171.6,
- "height": 39.6
+ "height": 32
},
{
"width": 167.6,
- "height": 36.8
+ "height": 18
},
{
"width": 164.8,
- "height": 32.4
+ "height": 8.8
},
{
"width": 162.8,
- "height": 27.6
+ "height": 2.8
},
{
"width": 161.6,
- "height": 22.8
+ "height": 0
},
{
"width": 160.8,
- "height": 18.4
+ "height": 0
},
{
"width": 160.4,
- "height": 14.4
+ "height": 0
},
{
"width": 160,
- "height": 11.2
+ "height": 0
},
{
"width": 160,
- "height": 8.4
+ "height": 0
},
{
"width": 160,
- "height": 6.4
+ "height": 0
},
{
"width": 160,
- "height": 4.4
+ "height": 0
},
{
"width": 160,
- "height": 3.2
+ "height": 0
},
{
"width": 160,
- "height": 2
+ "height": 0
},
{
"width": 160,
- "height": 1.6
+ "height": 0
},
{
"width": 160,
- "height": 0.8
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
},
{
"width": 160,
@@ -617,6 +635,8 @@
0,
0,
0,
+ 0,
+ 0,
0
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json
index 4b0306853903..b6e423afc6c4 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_dragOpen.json
@@ -336,55 +336,55 @@
},
{
"width": 188,
- "height": 24.8
+ "height": 25.6
},
{
"width": 188,
- "height": 32.8
+ "height": 36.4
},
{
"width": 188,
- "height": 44
+ "height": 45.6
},
{
"width": 188,
- "height": 57.2
+ "height": 59.2
},
{
"width": 188,
- "height": 70.8
+ "height": 72.8
},
{
"width": 188,
- "height": 78
+ "height": 79.6
},
{
"width": 188,
- "height": 91.2
+ "height": 92.8
},
{
"width": 188,
- "height": 103.2
+ "height": 104.4
},
{
"width": 188,
- "height": 114.4
+ "height": 115.2
},
{
"width": 188,
- "height": 124.4
+ "height": 125.2
},
{
"width": 188,
- "height": 134
+ "height": 134.8
},
{
"width": 188,
- "height": 142.8
+ "height": 143.2
},
{
"width": 188,
- "height": 150.8
+ "height": 151.2
},
{
"width": 188,
@@ -392,7 +392,7 @@
},
{
"width": 188,
- "height": 159.6
+ "height": 160
},
{
"width": 188,
@@ -400,7 +400,7 @@
},
{
"width": 188,
- "height": 174
+ "height": 174.4
},
{
"width": 188,
@@ -412,7 +412,7 @@
},
{
"width": 188,
- "height": 187.6
+ "height": 188
},
{
"width": 188,
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json
index 10a9ba7e2760..a82db346ed58 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingClose.json
@@ -39,7 +39,9 @@
576,
592,
608,
- 624
+ 624,
+ 640,
+ 656
],
"features": [
{
@@ -205,6 +207,14 @@
{
"x": 64,
"y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
}
]
},
@@ -302,67 +312,75 @@
},
{
"width": 170.8,
- "height": 39.6
+ "height": 29.6
},
{
"width": 166.8,
- "height": 36.8
+ "height": 12.8
},
{
"width": 164,
- "height": 32.4
+ "height": 2.4
},
{
"width": 162,
- "height": 27.6
+ "height": 0
},
{
"width": 160.8,
- "height": 22.8
+ "height": 0
},
{
"width": 160.4,
- "height": 18.4
+ "height": 0
},
{
"width": 160,
- "height": 14.4
+ "height": 0
},
{
"width": 160,
- "height": 11.2
+ "height": 0
},
{
"width": 160,
- "height": 8.4
+ "height": 0
},
{
"width": 160,
- "height": 6
+ "height": 0
},
{
"width": 160,
- "height": 4.4
+ "height": 0
},
{
"width": 160,
- "height": 3.2
+ "height": 0
},
{
"width": 160,
- "height": 2
+ "height": 0
},
{
"width": 160,
- "height": 1.2
+ "height": 0
},
{
"width": 160,
- "height": 0.8
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
},
{
"width": 160,
@@ -417,6 +435,8 @@
0,
0,
0,
+ 0,
+ 0,
0
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json
index d8bf48d32d20..6dc5a0e79e81 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_flingOpen.json
@@ -32,8 +32,7 @@
464,
480,
496,
- 512,
- 528
+ 512
],
"features": [
{
@@ -163,10 +162,6 @@
{
"x": 50,
"y": 50
- },
- {
- "x": 50,
- "y": 50
}
]
},
@@ -228,63 +223,59 @@
},
{
"width": 188,
- "height": 42.4
+ "height": 44.4
},
{
"width": 188,
- "height": 100
+ "height": 103.6
},
{
"width": 188,
- "height": 161.6
+ "height": 166
},
{
"width": 188,
- "height": 218
+ "height": 222.4
},
{
"width": 188,
- "height": 265.6
+ "height": 270
},
{
"width": 188,
- "height": 303.6
+ "height": 307.2
},
{
"width": 188,
- "height": 332.4
+ "height": 335.6
},
{
"width": 188,
- "height": 354
+ "height": 356.4
},
{
"width": 188,
- "height": 369.2
+ "height": 371.2
},
{
"width": 188,
- "height": 380
+ "height": 381.6
},
{
"width": 188,
- "height": 387.2
+ "height": 388.8
},
{
"width": 188,
- "height": 392
+ "height": 393.2
},
{
"width": 188,
- "height": 395.2
+ "height": 396
},
{
"width": 188,
- "height": 397.6
- },
- {
- "width": 188,
- "height": 398.4
+ "height": 398
},
{
"width": 188,
@@ -356,7 +347,6 @@
1,
1,
1,
- 1,
1
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json
index 57bdf3e1ecab..1cd971aa2898 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_gesture_magneticDetachAndReattach.json
@@ -69,9 +69,7 @@
1056,
1072,
1088,
- 1104,
- 1120,
- 1136
+ 1104
],
"features": [
{
@@ -353,14 +351,6 @@
{
"x": 64,
"y": 50
- },
- {
- "x": 64,
- "y": 50
- },
- {
- "x": 64,
- "y": 50
}
]
},
@@ -466,43 +456,43 @@
},
{
"width": 188,
- "height": 24
+ "height": 24.8
},
{
"width": 188,
- "height": 29.6
+ "height": 30
},
{
"width": 188,
- "height": 37.2
+ "height": 38
},
{
"width": 188,
- "height": 45.6
+ "height": 46
},
{
"width": 188,
- "height": 53.6
+ "height": 54
},
{
"width": 188,
- "height": 60.4
+ "height": 61.2
},
{
"width": 188,
- "height": 66.4
+ "height": 66.8
},
{
"width": 188,
- "height": 71.2
+ "height": 71.6
},
{
"width": 188,
- "height": 75.2
+ "height": 75.6
},
{
"width": 188,
- "height": 77.6
+ "height": 78
},
{
"width": 188,
@@ -510,7 +500,7 @@
},
{
"width": 188,
- "height": 80.4
+ "height": 80.8
},
{
"width": 188,
@@ -522,7 +512,7 @@
},
{
"width": 188,
- "height": 79.2
+ "height": 79.6
},
{
"width": 187.6,
@@ -530,7 +520,7 @@
},
{
"width": 186.8,
- "height": 76
+ "height": 76.4
},
{
"width": 186,
@@ -578,43 +568,39 @@
},
{
"width": 172.4,
- "height": 39.2
+ "height": 37.6
},
{
"width": 170.8,
- "height": 38.4
+ "height": 38
},
{
"width": 169.2,
- "height": 34.8
+ "height": 30.4
},
{
"width": 167.6,
- "height": 30
+ "height": 25.2
},
{
"width": 166,
- "height": 25.2
+ "height": 20.4
},
{
"width": 164,
- "height": 20.4
+ "height": 16
},
{
"width": 162.4,
- "height": 16.4
+ "height": 12.4
},
{
"width": 160.8,
- "height": 12.8
- },
- {
- "width": 160,
- "height": 9.6
+ "height": 9.2
},
{
"width": 160,
- "height": 7.2
+ "height": 6.8
},
{
"width": 160,
@@ -626,7 +612,7 @@
},
{
"width": 160,
- "height": 2.8
+ "height": 2.4
},
{
"width": 160,
@@ -634,10 +620,6 @@
},
{
"width": 160,
- "height": 1.2
- },
- {
- "width": 160,
"height": 0.8
},
{
@@ -646,7 +628,7 @@
},
{
"width": 160,
- "height": 0
+ "height": 0.4
},
{
"width": 160,
@@ -735,8 +717,6 @@
0.03147719,
0.019312752,
0.011740655,
- 0,
- 0,
0
]
}
diff --git a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json
index 9aa91c2d5e17..1030455e873f 100644
--- a/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json
+++ b/packages/SystemUI/compose/scene/tests/goldens/verticalReveal_triggeredRevealCloseTransition.json
@@ -27,7 +27,9 @@
384,
400,
416,
- 432
+ 432,
+ 448,
+ 464
],
"features": [
{
@@ -145,6 +147,14 @@
{
"x": 64,
"y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
+ },
+ {
+ "x": 64,
+ "y": 50
}
]
},
@@ -194,67 +204,75 @@
},
{
"width": 168.8,
- "height": 38.4
+ "height": 22.4
},
{
"width": 165.2,
- "height": 34.8
+ "height": 10
},
{
"width": 162.8,
- "height": 30
+ "height": 2.4
},
{
"width": 161.2,
- "height": 25.2
+ "height": 0
},
{
"width": 160.4,
- "height": 20.4
+ "height": 0
},
{
"width": 160,
- "height": 16.4
+ "height": 0
},
{
"width": 160,
- "height": 12.8
+ "height": 0
},
{
"width": 160,
- "height": 9.6
+ "height": 0
},
{
"width": 160,
- "height": 7.2
+ "height": 0
},
{
"width": 160,
- "height": 5.2
+ "height": 0
},
{
"width": 160,
- "height": 3.6
+ "height": 0
},
{
"width": 160,
- "height": 2.8
+ "height": 0
},
{
"width": 160,
- "height": 1.6
+ "height": 0
},
{
"width": 160,
- "height": 1.2
+ "height": 0
},
{
"width": 160,
- "height": 0.8
+ "height": 0
},
{
"width": 160,
- "height": 0.4
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
+ },
+ {
+ "width": 160,
+ "height": 0
},
{
"width": 160,
@@ -297,6 +315,8 @@
0,
0,
0,
+ 0,
+ 0,
0
]
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 4bf0ceb51784..6e29e6932629 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -38,8 +38,9 @@ import com.android.systemui.animation.TypefaceVariantCacheImpl
import com.android.systemui.customization.R
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.core.LogcatOnlyMessageBuffer
-import com.android.systemui.log.core.Logger
import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.ClockLogger
+import com.android.systemui.plugins.clocks.ClockLogger.Companion.escapeTime
import java.io.PrintWriter
import java.util.Calendar
import java.util.Locale
@@ -67,7 +68,7 @@ constructor(
var messageBuffer: MessageBuffer
get() = logger.buffer
set(value) {
- logger = Logger(value, TAG)
+ logger = ClockLogger(this, value, TAG)
}
var hasCustomPositionUpdatedAnimation: Boolean = false
@@ -185,7 +186,9 @@ constructor(
time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis()
contentDescription = DateFormat.format(descFormat, time)
val formattedText = DateFormat.format(format, time)
- logger.d({ "refreshTime: new formattedText=$str1" }) { str1 = formattedText?.toString() }
+ logger.d({ "refreshTime: new formattedText=${escapeTime(str1)}" }) {
+ str1 = formattedText?.toString()
+ }
// Setting text actually triggers a layout pass in TextView (because the text view is set to
// wrap_content width and TextView always relayouts for this). This avoids needless relayout
@@ -195,7 +198,7 @@ constructor(
}
text = formattedText
- logger.d({ "refreshTime: done setting new time text to: $str1" }) {
+ logger.d({ "refreshTime: done setting new time text to: ${escapeTime(str1)}" }) {
str1 = formattedText?.toString()
}
@@ -225,7 +228,7 @@ constructor(
@SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
- logger.d("onMeasure")
+ logger.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (!isSingleLineInternal && MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
// Call straight into TextView.setTextSize to avoid setting lastUnconstrainedTextSize
@@ -263,14 +266,14 @@ constructor(
canvas.translate(parentWidth / 4f, 0f)
}
- logger.d({ "onDraw($str1)" }) { str1 = text.toString() }
+ logger.onDraw("$text")
// intentionally doesn't call super.onDraw here or else the text will be rendered twice
textAnimator?.draw(canvas)
canvas.restore()
}
override fun invalidate() {
- logger.d("invalidate")
+ logger.invalidate()
super.invalidate()
}
@@ -280,7 +283,7 @@ constructor(
lengthBefore: Int,
lengthAfter: Int,
) {
- logger.d({ "onTextChanged($str1)" }) { str1 = text.toString() }
+ logger.d({ "onTextChanged(${escapeTime(str1)})" }) { str1 = "$text" }
super.onTextChanged(text, start, lengthBefore, lengthAfter)
}
@@ -370,7 +373,7 @@ constructor(
return
}
- logger.d("animateCharge")
+ logger.animateCharge()
val startAnimPhase2 = Runnable {
setTextStyle(
weight = if (isDozing()) dozingWeight else lockScreenWeight,
@@ -394,7 +397,7 @@ constructor(
}
fun animateDoze(isDozing: Boolean, animate: Boolean) {
- logger.d("animateDoze")
+ logger.animateDoze(isDozing, animate)
setTextStyle(
weight = if (isDozing) dozingWeight else lockScreenWeight,
color = if (isDozing) dozingColor else lockScreenColor,
@@ -484,7 +487,7 @@ constructor(
isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12
else -> DOUBLE_LINE_FORMAT_12_HOUR
}
- logger.d({ "refreshFormat($str1)" }) { str1 = format?.toString() }
+ logger.d({ "refreshFormat(${escapeTime(str1)})" }) { str1 = format?.toString() }
descFormat = if (use24HourFormat) Patterns.sClockView24 else Patterns.sClockView12
refreshTime()
@@ -634,7 +637,7 @@ constructor(
companion object {
private val TAG = AnimatableClockView::class.simpleName!!
- private val DEFAULT_LOGGER = Logger(LogcatOnlyMessageBuffer(LogLevel.WARNING), TAG)
+ private val DEFAULT_LOGGER = ClockLogger(null, LogcatOnlyMessageBuffer(LogLevel.DEBUG), TAG)
const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600
private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm"
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt
index dd1599e5259d..9857d7f3d69d 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/CanvasUtil.kt
@@ -17,6 +17,8 @@
package com.android.systemui.shared.clocks
import android.graphics.Canvas
+import com.android.systemui.plugins.clocks.VPoint
+import com.android.systemui.plugins.clocks.VPointF
object CanvasUtil {
fun Canvas.translate(pt: VPointF) = this.translate(pt.x, pt.y)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
index f5ccc52c8c6b..941cebfb4014 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DigitTranslateAnimator.kt
@@ -20,7 +20,8 @@ import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.TimeInterpolator
import android.animation.ValueAnimator
-import com.android.systemui.shared.clocks.VPointF.Companion.times
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.times
class DigitTranslateAnimator(private val updateCallback: (VPointF) -> Unit) {
var currentTranslation = VPointF.ZERO
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
index 336c66eed889..9ac9e60f05fd 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
@@ -16,13 +16,13 @@
package com.android.systemui.shared.clocks
-import android.graphics.RectF
import android.view.View
import androidx.annotation.VisibleForTesting
import com.android.systemui.plugins.clocks.ClockAnimations
import com.android.systemui.plugins.clocks.ClockEvents
import com.android.systemui.plugins.clocks.ClockFaceConfig
import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.VRectF
interface SimpleClockLayerController {
val view: View
@@ -32,5 +32,5 @@ interface SimpleClockLayerController {
val config: ClockFaceConfig
@VisibleForTesting var fakeTimeMills: Long?
- var onViewBoundsChanged: ((RectF) -> Unit)?
+ var onViewBoundsChanged: ((VRectF) -> Unit)?
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt
index 1e90a2370786..0740b0e504cb 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ViewUtils.kt
@@ -18,8 +18,9 @@ package com.android.systemui.shared.clocks
import android.graphics.Rect
import android.view.View
-import com.android.systemui.shared.clocks.VPoint.Companion.center
-import com.android.systemui.shared.clocks.VPointF.Companion.center
+import com.android.systemui.plugins.clocks.VPoint.Companion.center
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.center
object ViewUtils {
fun View.computeLayoutDiff(targetRegion: Rect, isLargeClock: Boolean): VPointF {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
index 2dc3e2b7af73..ba32ab083063 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt
@@ -17,7 +17,6 @@
package com.android.systemui.shared.clocks.view
import android.graphics.Canvas
-import android.graphics.RectF
import android.icu.text.NumberFormat
import android.util.MathUtils.constrainedMap
import android.view.View
@@ -29,14 +28,15 @@ import com.android.app.animation.Interpolators
import com.android.systemui.customization.R
import com.android.systemui.plugins.clocks.ClockFontAxisSetting
import com.android.systemui.plugins.clocks.ClockLogger
+import com.android.systemui.plugins.clocks.VPoint
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.max
+import com.android.systemui.plugins.clocks.VPointF.Companion.times
+import com.android.systemui.plugins.clocks.VRectF
import com.android.systemui.shared.clocks.CanvasUtil.translate
import com.android.systemui.shared.clocks.CanvasUtil.use
import com.android.systemui.shared.clocks.ClockContext
import com.android.systemui.shared.clocks.DigitTranslateAnimator
-import com.android.systemui.shared.clocks.VPoint
-import com.android.systemui.shared.clocks.VPointF
-import com.android.systemui.shared.clocks.VPointF.Companion.max
-import com.android.systemui.shared.clocks.VPointF.Companion.times
import com.android.systemui.shared.clocks.ViewUtils.measuredSize
import java.util.Locale
import kotlin.collections.filterNotNull
@@ -101,7 +101,7 @@ class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) {
updateLocale(Locale.getDefault())
}
- var onViewBoundsChanged: ((RectF) -> Unit)? = null
+ var onViewBoundsChanged: ((VRectF) -> Unit)? = null
private val digitOffsets = mutableMapOf<Int, Float>()
protected fun calculateSize(
@@ -189,13 +189,7 @@ class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) {
fun updateLocation() {
val layoutBounds = this.layoutBounds ?: return
- val bounds =
- RectF(
- layoutBounds.centerX() - measuredWidth / 2f,
- layoutBounds.centerY() - measuredHeight / 2f,
- layoutBounds.centerX() + measuredWidth / 2f,
- layoutBounds.centerY() + measuredHeight / 2f,
- )
+ val bounds = VRectF.fromCenter(layoutBounds.center, this.measuredSize)
setFrame(
bounds.left.roundToInt(),
bounds.top.roundToInt(),
@@ -215,16 +209,11 @@ class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) {
onAnimateDoze = null
}
- private val layoutBounds = RectF()
+ private var layoutBounds = VRectF.ZERO
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
logger.onLayout(changed, left, top, right, bottom)
-
- layoutBounds.left = left.toFloat()
- layoutBounds.top = top.toFloat()
- layoutBounds.right = right.toFloat()
- layoutBounds.bottom = bottom.toFloat()
-
+ layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
updateChildFrames(isLayout = true)
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index 0ec2d188833a..2af25fe339a2 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -23,7 +23,6 @@ import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.Rect
-import android.graphics.RectF
import android.os.VibrationEffect
import android.text.Layout
import android.text.TextPaint
@@ -45,6 +44,10 @@ import com.android.systemui.plugins.clocks.ClockFontAxisSetting
import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.replace
import com.android.systemui.plugins.clocks.ClockFontAxisSetting.Companion.toFVar
import com.android.systemui.plugins.clocks.ClockLogger
+import com.android.systemui.plugins.clocks.VPoint
+import com.android.systemui.plugins.clocks.VPointF
+import com.android.systemui.plugins.clocks.VPointF.Companion.size
+import com.android.systemui.plugins.clocks.VRectF
import com.android.systemui.shared.Flags.ambientAod
import com.android.systemui.shared.clocks.CanvasUtil.translate
import com.android.systemui.shared.clocks.CanvasUtil.use
@@ -53,9 +56,6 @@ import com.android.systemui.shared.clocks.DigitTranslateAnimator
import com.android.systemui.shared.clocks.DimensionParser
import com.android.systemui.shared.clocks.FLEX_CLOCK_ID
import com.android.systemui.shared.clocks.FontTextStyle
-import com.android.systemui.shared.clocks.VPoint
-import com.android.systemui.shared.clocks.VPointF
-import com.android.systemui.shared.clocks.VPointF.Companion.size
import com.android.systemui.shared.clocks.ViewUtils.measuredSize
import com.android.systemui.shared.clocks.ViewUtils.size
import com.android.systemui.shared.clocks.toClockAxisSetting
@@ -66,11 +66,11 @@ import kotlin.math.roundToInt
private val TAG = SimpleDigitalClockTextView::class.simpleName!!
-private fun Paint.getTextBounds(text: CharSequence, result: RectF = RectF()): RectF {
- val rect = Rect()
- this.getTextBounds(text, 0, text.length, rect)
- result.set(rect)
- return result
+private val tempRect = Rect()
+
+private fun Paint.getTextBounds(text: CharSequence): VRectF {
+ this.getTextBounds(text, 0, text.length, tempRect)
+ return VRectF(tempRect)
}
enum class VerticalAlignment {
@@ -143,7 +143,7 @@ open class SimpleDigitalClockTextView(
fidgetFontVariation = buildFidgetVariation(lsFontAxes).toFVar()
}
- var onViewBoundsChanged: ((RectF) -> Unit)? = null
+ var onViewBoundsChanged: ((VRectF) -> Unit)? = null
private val parser = DimensionParser(clockCtx.context)
var maxSingleDigitHeight = -1f
var maxSingleDigitWidth = -1f
@@ -159,13 +159,13 @@ open class SimpleDigitalClockTextView(
private val initThread = Thread.currentThread()
// textBounds is the size of text in LS, which only measures current text in lockscreen style
- var textBounds = RectF()
+ var textBounds = VRectF.ZERO
// prevTextBounds and targetTextBounds are to deal with dozing animation between LS and AOD
// especially for the textView which has different bounds during the animation
// prevTextBounds holds the state we are transitioning from
- private val prevTextBounds = RectF()
+ private var prevTextBounds = VRectF.ZERO
// targetTextBounds holds the state we are interpolating to
- private val targetTextBounds = RectF()
+ private var targetTextBounds = VRectF.ZERO
protected val logger = ClockLogger(this, clockCtx.messageBuffer, this::class.simpleName!!)
get() = field ?: ClockLogger.INIT_LOGGER
@@ -215,8 +215,8 @@ open class SimpleDigitalClockTextView(
lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
typeface = lockScreenPaint.typeface
- lockScreenPaint.getTextBounds(text, textBounds)
- targetTextBounds.set(textBounds)
+ textBounds = lockScreenPaint.getTextBounds(text)
+ targetTextBounds = textBounds
textAnimator.setTextStyle(TextAnimator.Style(fVar = lsFontVariation))
measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
@@ -287,7 +287,7 @@ open class SimpleDigitalClockTextView(
canvas.use {
digitTranslateAnimator?.apply { canvas.translate(currentTranslation) }
canvas.translate(getDrawTranslation(interpBounds))
- if (isLayoutRtl()) canvas.translate(interpBounds.width() - textBounds.width(), 0f)
+ if (isLayoutRtl()) canvas.translate(interpBounds.width - textBounds.width, 0f)
textAnimator.draw(canvas)
}
}
@@ -302,16 +302,12 @@ open class SimpleDigitalClockTextView(
super.setAlpha(alpha)
}
- private val layoutBounds = RectF()
+ private var layoutBounds = VRectF.ZERO
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
logger.onLayout(changed, left, top, right, bottom)
-
- layoutBounds.left = left.toFloat()
- layoutBounds.top = top.toFloat()
- layoutBounds.right = right.toFloat()
- layoutBounds.bottom = bottom.toFloat()
+ layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
}
override fun invalidate() {
@@ -327,7 +323,7 @@ open class SimpleDigitalClockTextView(
fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
if (!this::textAnimator.isInitialized) return
- logger.animateDoze()
+ logger.animateDoze(isDozing, isAnimated)
textAnimator.setTextStyle(
TextAnimator.Style(
fVar = if (isDozing) aodFontVariation else lsFontVariation,
@@ -341,6 +337,11 @@ open class SimpleDigitalClockTextView(
),
)
updateTextBoundsForTextAnimator()
+
+ if (!isAnimated) {
+ requestLayout()
+ (parent as? FlexClockView)?.requestLayout()
+ }
}
fun animateCharge() {
@@ -408,10 +409,10 @@ open class SimpleDigitalClockTextView(
}
fun refreshText() {
- lockScreenPaint.getTextBounds(text, textBounds)
- if (this::textAnimator.isInitialized) {
- textAnimator.textInterpolator.targetPaint.getTextBounds(text, targetTextBounds)
- }
+ textBounds = lockScreenPaint.getTextBounds(text)
+ targetTextBounds =
+ if (!this::textAnimator.isInitialized) textBounds
+ else textAnimator.textInterpolator.targetPaint.getTextBounds(text)
if (layout == null) {
requestLayout()
@@ -432,23 +433,23 @@ open class SimpleDigitalClockTextView(
}
/** Returns the interpolated text bounding rect based on interpolation progress */
- private fun getInterpolatedTextBounds(progress: Float = getInterpolatedProgress()): RectF {
+ private fun getInterpolatedTextBounds(progress: Float = getInterpolatedProgress()): VRectF {
if (progress <= 0f) {
return prevTextBounds
} else if (!textAnimator.isRunning || progress >= 1f) {
return targetTextBounds
}
- return RectF().apply {
- left = lerp(prevTextBounds.left, targetTextBounds.left, progress)
- right = lerp(prevTextBounds.right, targetTextBounds.right, progress)
- top = lerp(prevTextBounds.top, targetTextBounds.top, progress)
- bottom = lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress)
- }
+ return VRectF(
+ left = lerp(prevTextBounds.left, targetTextBounds.left, progress),
+ right = lerp(prevTextBounds.right, targetTextBounds.right, progress),
+ top = lerp(prevTextBounds.top, targetTextBounds.top, progress),
+ bottom = lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress),
+ )
}
private fun computeMeasuredSize(
- interpBounds: RectF,
+ interpBounds: VRectF,
widthMeasureSpec: Int = measuredWidthAndState,
heightMeasureSpec: Int = measuredHeightAndState,
): VPointF {
@@ -461,11 +462,11 @@ open class SimpleDigitalClockTextView(
return VPointF(
when {
mode.x == EXACTLY -> MeasureSpec.getSize(widthMeasureSpec).toFloat()
- else -> interpBounds.width() + 2 * lockScreenPaint.strokeWidth
+ else -> interpBounds.width + 2 * lockScreenPaint.strokeWidth
},
when {
mode.y == EXACTLY -> MeasureSpec.getSize(heightMeasureSpec).toFloat()
- else -> interpBounds.height() + 2 * lockScreenPaint.strokeWidth
+ else -> interpBounds.height + 2 * lockScreenPaint.strokeWidth
},
)
}
@@ -489,44 +490,23 @@ open class SimpleDigitalClockTextView(
}
/** Set the location of the view to match the interpolated text bounds */
- private fun setInterpolatedLocation(measureSize: VPointF): RectF {
- val targetRect = RectF()
- targetRect.apply {
- when (xAlignment) {
- XAlignment.LEFT -> {
- left = layoutBounds.left
- right = layoutBounds.left + measureSize.x
- }
- XAlignment.CENTER -> {
- left = layoutBounds.centerX() - measureSize.x / 2f
- right = layoutBounds.centerX() + measureSize.x / 2f
- }
- XAlignment.RIGHT -> {
- left = layoutBounds.right - measureSize.x
- right = layoutBounds.right
- }
- }
-
- when (verticalAlignment) {
- VerticalAlignment.TOP -> {
- top = layoutBounds.top
- bottom = layoutBounds.top + measureSize.y
- }
- VerticalAlignment.CENTER -> {
- top = layoutBounds.centerY() - measureSize.y / 2f
- bottom = layoutBounds.centerY() + measureSize.y / 2f
- }
- VerticalAlignment.BOTTOM -> {
- top = layoutBounds.bottom - measureSize.y
- bottom = layoutBounds.bottom
- }
- VerticalAlignment.BASELINE -> {
- top = layoutBounds.centerY() - measureSize.y / 2f
- bottom = layoutBounds.centerY() + measureSize.y / 2f
- }
- }
- }
+ private fun setInterpolatedLocation(measureSize: VPointF): VRectF {
+ val pos =
+ VPointF(
+ when (xAlignment) {
+ XAlignment.LEFT -> layoutBounds.left
+ XAlignment.CENTER -> layoutBounds.center.x - measureSize.x / 2f
+ XAlignment.RIGHT -> layoutBounds.right - measureSize.x
+ },
+ when (verticalAlignment) {
+ VerticalAlignment.TOP -> layoutBounds.top
+ VerticalAlignment.CENTER -> layoutBounds.center.y - measureSize.y / 2f
+ VerticalAlignment.BOTTOM -> layoutBounds.bottom - measureSize.y
+ VerticalAlignment.BASELINE -> layoutBounds.center.y - measureSize.y / 2f
+ },
+ )
+ val targetRect = VRectF.fromTopLeft(pos, measureSize)
setFrame(
targetRect.left.roundToInt(),
targetRect.top.roundToInt(),
@@ -537,7 +517,7 @@ open class SimpleDigitalClockTextView(
return targetRect
}
- private fun getDrawTranslation(interpBounds: RectF): VPointF {
+ private fun getDrawTranslation(interpBounds: VRectF): VPointF {
val sizeDiff = this.measuredSize - interpBounds.size
val alignment =
VPointF(
@@ -586,11 +566,11 @@ open class SimpleDigitalClockTextView(
if (fontSizePx > 0) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
lockScreenPaint.textSize = textSize
- lockScreenPaint.getTextBounds(text, textBounds)
- targetTextBounds.set(textBounds)
+ textBounds = lockScreenPaint.getTextBounds(text)
+ targetTextBounds = textBounds
}
if (!constrainedByHeight) {
- val lastUnconstrainedHeight = textBounds.height() + lockScreenPaint.strokeWidth * 2
+ val lastUnconstrainedHeight = textBounds.height + lockScreenPaint.strokeWidth * 2
fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize
}
@@ -608,8 +588,8 @@ open class SimpleDigitalClockTextView(
for (i in 0..9) {
val rectForCalculate = lockScreenPaint.getTextBounds("$i")
- maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height())
- maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width())
+ maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height)
+ maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width)
}
maxSingleDigitWidth += 2 * lockScreenPaint.strokeWidth
maxSingleDigitHeight += 2 * lockScreenPaint.strokeWidth
@@ -637,8 +617,8 @@ open class SimpleDigitalClockTextView(
* and targetPaint will store the state we transition to
*/
private fun updateTextBoundsForTextAnimator() {
- textAnimator.textInterpolator.basePaint.getTextBounds(text, prevTextBounds)
- textAnimator.textInterpolator.targetPaint.getTextBounds(text, targetTextBounds)
+ prevTextBounds = textAnimator.textInterpolator.basePaint.getTextBounds(text)
+ targetTextBounds = textAnimator.textInterpolator.targetPaint.getTextBounds(text)
}
/**
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
index e26e19d27417..29647cd082b1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java
@@ -16,12 +16,18 @@
package com.android.keyguard;
+import static com.android.internal.widget.flags.Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
+import android.hardware.input.InputManager;
+import android.platform.test.annotations.EnableFlags;
import android.testing.TestableLooper.RunWithLooper;
import android.view.View;
import android.view.ViewGroup;
@@ -90,6 +96,8 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase {
@Mock
private UserActivityNotifier mUserActivityNotifier;
private NumPadKey[] mButtons = new NumPadKey[]{};
+ @Mock
+ private InputManager mInputManager;
private KeyguardPinBasedInputViewController mKeyguardPinViewController;
@@ -118,12 +126,13 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase {
new KeyguardKeyboardInteractor(new FakeKeyboardRepository());
FakeFeatureFlags featureFlags = new FakeFeatureFlags();
mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_REVAMPED_BOUNCER_MESSAGES);
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false);
mKeyguardPinViewController = new KeyguardPinBasedInputViewController(mPinBasedInputView,
mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback,
mKeyguardMessageAreaControllerFactory, mLatencyTracker,
mEmergencyButtonController, mFalsingCollector, featureFlags,
mSelectedUserInteractor, keyguardKeyboardInteractor, mBouncerHapticPlayer,
- mUserActivityNotifier) {
+ mUserActivityNotifier, mInputManager) {
@Override
public void onResume(int reason) {
super.onResume(reason);
@@ -148,4 +157,112 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase {
mKeyguardPinViewController.resetState();
verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin);
}
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_addDevice_notKeyboard() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ mKeyguardPinViewController.onViewAttached();
+ mKeyguardPinViewController.onInputDeviceAdded(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_addDevice_keyboard() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ mKeyguardPinViewController.onViewAttached();
+ mKeyguardPinViewController.onInputDeviceAdded(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_addDevice_multipleKeyboards() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ mKeyguardPinViewController.onViewAttached();
+ mKeyguardPinViewController.onInputDeviceAdded(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ mKeyguardPinViewController.onInputDeviceAdded(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_removeDevice_notKeyboard() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ mKeyguardPinViewController.onViewAttached();
+ mKeyguardPinViewController.onInputDeviceRemoved(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_removeDevice_keyboard() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, false);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(0)).setShowPassword(false);
+ mKeyguardPinViewController.onViewAttached();
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ mKeyguardPinViewController.onInputDeviceRemoved(1);
+ verify(mPasswordEntry, times(2)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_removeDevice_multipleKeyboards() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, true);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(0)).setShowPassword(false);
+ mKeyguardPinViewController.onViewAttached();
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ mKeyguardPinViewController.onInputDeviceRemoved(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_updateDevice_notKeyboard() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(false);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ mKeyguardPinViewController.onViewAttached();
+ mKeyguardPinViewController.onInputDeviceChanged(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_updateDevice_keyboard() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, false);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(0)).setShowPassword(false);
+ mKeyguardPinViewController.onViewAttached();
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ mKeyguardPinViewController.onInputDeviceChanged(1);
+ verify(mPasswordEntry, times(2)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT)
+ public void updateAnimations_updateDevice_multipleKeyboards() {
+ when(mLockPatternUtils.isPinEnhancedPrivacyEnabled(anyInt())).thenReturn(true, true);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(0)).setShowPassword(false);
+ mKeyguardPinViewController.onViewAttached();
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ mKeyguardPinViewController.onInputDeviceChanged(1);
+ verify(mPasswordEntry, times(1)).setShowPassword(true);
+ verify(mPasswordEntry, times(1)).setShowPassword(false);
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
index 142a2868ec14..7fea06ec7f41 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
@@ -16,6 +16,7 @@
package com.android.keyguard
+import android.hardware.input.InputManager
import android.testing.TestableLooper
import android.view.View
import android.view.ViewGroup
@@ -104,6 +105,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() {
@Mock lateinit var enterButton: View
@Mock lateinit var uiEventLogger: UiEventLogger
@Mock lateinit var mUserActivityNotifier: UserActivityNotifier
+ @Mock lateinit var inputManager: InputManager
@Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback>
@@ -154,6 +156,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() {
keyguardKeyboardInteractor,
kosmos.bouncerHapticPlayer,
mUserActivityNotifier,
+ inputManager,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
index c751a7db51dc..003669da498e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
@@ -16,6 +16,7 @@
package com.android.keyguard
+import android.hardware.input.InputManager
import android.telephony.TelephonyManager
import android.testing.TestableLooper
import android.view.LayoutInflater
@@ -73,6 +74,7 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() {
@Mock private lateinit var mUserActivityNotifier: UserActivityNotifier
private val updateMonitorCallbackArgumentCaptor =
ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
+ @Mock private lateinit var inputManager: InputManager
private val kosmos = testKosmos()
@@ -107,6 +109,7 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() {
keyguardKeyboardInteractor,
kosmos.bouncerHapticPlayer,
mUserActivityNotifier,
+ inputManager,
)
underTest.init()
underTest.onViewAttached()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
index c34682551eda..85cb388ace5c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
@@ -16,6 +16,7 @@
package com.android.keyguard
+import android.hardware.input.InputManager
import android.telephony.PinResult
import android.telephony.TelephonyManager
import android.testing.TestableLooper
@@ -65,6 +66,7 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() {
private lateinit var keyguardMessageAreaController:
KeyguardMessageAreaController<BouncerKeyguardMessageArea>
@Mock private lateinit var mUserActivityNotifier: UserActivityNotifier
+ @Mock private lateinit var inputManager: InputManager
private val kosmos = testKosmos()
@@ -102,6 +104,7 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() {
keyguardKeyboardInteractor,
kosmos.bouncerHapticPlayer,
mUserActivityNotifier,
+ inputManager,
)
underTest.init()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
index 0b13900d826b..7e4704a6179a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
@@ -71,6 +71,7 @@ import com.android.systemui.bluetooth.qsdialog.DeviceItem;
import com.android.systemui.bluetooth.qsdialog.DeviceItemType;
import com.android.systemui.model.SysUiState;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.qs.shared.QSSettingsPackageRepository;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.phone.SystemUIDialogManager;
@@ -108,6 +109,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
private static final String TEST_LABEL = "label";
private static final int TEST_PRESET_INDEX = 1;
private static final String TEST_PRESET_NAME = "test_preset";
+ private static final String SETTINGS_PACKAGE_NAME = "com.android.settings";
private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
@Mock
@@ -137,6 +139,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
@Mock
private HearingDevicesUiEventLogger mUiEventLogger;
@Mock
+ private QSSettingsPackageRepository mQSSettingsPackageRepository;
+ @Mock
private CachedBluetoothDevice mCachedDevice;
@Mock
private BluetoothDevice mDevice;
@@ -164,6 +168,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(List.of(mCachedDevice));
when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager);
when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState);
+ when(mQSSettingsPackageRepository.getSettingsPackageName())
+ .thenReturn(SETTINGS_PACKAGE_NAME);
when(mDevice.getBondState()).thenReturn(BOND_BONDED);
when(mDevice.isConnected()).thenReturn(true);
when(mCachedDevice.getDevice()).thenReturn(mDevice);
@@ -195,6 +201,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
anyInt(), any());
assertThat(intentCaptor.getValue().getAction()).isEqualTo(
Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS);
+ assertThat(intentCaptor.getValue().getPackage()).isEqualTo(SETTINGS_PACKAGE_NAME);
verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR,
TEST_LAUNCH_SOURCE_ID);
}
@@ -210,6 +217,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
anyInt(), any());
assertThat(intentCaptor.getValue().getAction()).isEqualTo(
HearingDevicesDialogDelegate.ACTION_BLUETOOTH_DEVICE_DETAILS);
+ assertThat(intentCaptor.getValue().getPackage()).isEqualTo(SETTINGS_PACKAGE_NAME);
verify(mUiEventLogger).log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK,
TEST_LAUNCH_SOURCE_ID);
}
@@ -392,7 +400,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase {
mExecutor,
mExecutor,
mAudioManager,
- mUiEventLogger
+ mUiEventLogger,
+ mQSSettingsPackageRepository
);
mDialog = mDialogDelegate.createDialog();
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
index 8d3640d8d809..53b364c13063 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/animation/FontVariationUtilsTest.kt
@@ -41,21 +41,9 @@ class FontVariationUtilsTest : SysuiTestCase() {
@Test
fun testStyleValueUnchange_getBlankStr() {
val fontVariationUtils = FontVariationUtils()
- fontVariationUtils.updateFontVariation(
- weight = 100,
- width = 100,
- opticalSize = 0,
- roundness = 100,
- )
- val updatedFvar1 =
- fontVariationUtils.updateFontVariation(
- weight = 100,
- width = 100,
- opticalSize = 0,
- roundness = 100,
- )
- Assert.assertEquals("", updatedFvar1)
- val updatedFvar2 = fontVariationUtils.updateFontVariation()
- Assert.assertEquals("", updatedFvar2)
+ Assert.assertEquals("", fontVariationUtils.updateFontVariation())
+ val fVar = fontVariationUtils.updateFontVariation(weight = 100)
+ Assert.assertEquals(fVar, fontVariationUtils.updateFontVariation())
+ Assert.assertEquals(fVar, fontVariationUtils.updateFontVariation(weight = 100))
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
index 25a287c4cfff..15a6de896e92 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt
@@ -247,20 +247,20 @@ class PinInputViewModelTest : SysuiTestCase() {
}
private class PinInputSubject
-private constructor(metadata: FailureMetadata, private val actual: PinInputViewModel) :
+private constructor(metadata: FailureMetadata, private val actual: PinInputViewModel?) :
Subject(metadata, actual) {
fun matches(mnemonics: String) {
val actualMnemonics =
- actual.input
- .map { entry ->
+ actual?.input
+ ?.map { entry ->
when (entry) {
is Digit -> entry.input.digitToChar()
is ClearAll -> 'C'
else -> throw IllegalArgumentException()
}
}
- .joinToString(separator = "")
+ ?.joinToString(separator = "")
if (mnemonics != actualMnemonics) {
failWithActual(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt
index 0f400892f988..56b06de0a9ba 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandlerTest.kt
@@ -17,14 +17,12 @@
package com.android.systemui.common.ui.view
+import android.testing.TestableLooper
+import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Down
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Move
-import com.android.systemui.common.ui.view.TouchHandlingViewInteractionHandler.MotionEventModel.Up
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -33,18 +31,22 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito.never
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() {
@Mock private lateinit var postDelayed: (Runnable, Long) -> DisposableHandle
@Mock private lateinit var onLongPressDetected: (Int, Int) -> Unit
@Mock private lateinit var onSingleTapDetected: (Int, Int) -> Unit
+ @Mock private lateinit var onDoubleTapDetected: () -> Unit
private lateinit var underTest: TouchHandlingViewInteractionHandler
@@ -61,14 +63,17 @@ class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() {
underTest =
TouchHandlingViewInteractionHandler(
+ context = context,
postDelayed = postDelayed,
isAttachedToWindow = { isAttachedToWindow },
onLongPressDetected = onLongPressDetected,
onSingleTapDetected = onSingleTapDetected,
+ onDoubleTapDetected = onDoubleTapDetected,
longPressDuration = { ViewConfiguration.getLongPressTimeout().toLong() },
allowedTouchSlop = ViewConfiguration.getTouchSlop(),
)
underTest.isLongPressHandlingEnabled = true
+ underTest.isDoubleTapHandlingEnabled = true
}
@Test
@@ -76,63 +81,250 @@ class TouchHandlingViewInteractionHandlerTest : SysuiTestCase() {
val downX = 123
val downY = 456
dispatchTouchEvents(
- Down(x = downX, y = downY),
- Move(distanceMoved = ViewConfiguration.getTouchSlop() - 0.1f),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(
+ 0L,
+ 0L,
+ MotionEvent.ACTION_MOVE,
+ 123f + ViewConfiguration.getTouchSlop() - 0.1f,
+ 456f,
+ 0,
+ ),
)
delayedRunnable?.run()
verify(onLongPressDetected).invoke(downX, downY)
- verify(onSingleTapDetected, never()).invoke(any(), any())
+ verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
}
@Test
fun longPressButFeatureNotEnabled() = runTest {
underTest.isLongPressHandlingEnabled = false
- dispatchTouchEvents(Down(x = 123, y = 456))
+ dispatchTouchEvents(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0))
assertThat(delayedRunnable).isNull()
- verify(onLongPressDetected, never()).invoke(any(), any())
- verify(onSingleTapDetected, never()).invoke(any(), any())
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
}
@Test
fun longPressButViewNotAttached() = runTest {
isAttachedToWindow = false
- dispatchTouchEvents(Down(x = 123, y = 456))
+ dispatchTouchEvents(MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0))
delayedRunnable?.run()
- verify(onLongPressDetected, never()).invoke(any(), any())
- verify(onSingleTapDetected, never()).invoke(any(), any())
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
}
@Test
fun draggedTooFarToBeConsideredAlongPress() = runTest {
dispatchTouchEvents(
- Down(x = 123, y = 456),
- Move(distanceMoved = ViewConfiguration.getTouchSlop() + 0.1f),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123F, 456F, 0),
+ // Drag action within touch slop
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 123f, 456f, 0).apply {
+ addBatch(0L, 123f + ViewConfiguration.getTouchSlop() + 0.1f, 456f, 0f, 0f, 0)
+ },
)
assertThat(delayedRunnable).isNull()
- verify(onLongPressDetected, never()).invoke(any(), any())
- verify(onSingleTapDetected, never()).invoke(any(), any())
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, never()).invoke(anyInt(), anyInt())
}
@Test
fun heldDownTooBrieflyToBeConsideredAlongPress() = runTest {
dispatchTouchEvents(
- Down(x = 123, y = 456),
- Up(
- distanceMoved = ViewConfiguration.getTouchSlop().toFloat(),
- gestureDuration = ViewConfiguration.getLongPressTimeout() - 1L,
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(
+ 0L,
+ ViewConfiguration.getLongPressTimeout() - 1L,
+ MotionEvent.ACTION_UP,
+ 123f,
+ 456F,
+ 0,
),
)
assertThat(delayedRunnable).isNull()
- verify(onLongPressDetected, never()).invoke(any(), any())
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
verify(onSingleTapDetected).invoke(123, 456)
}
- private fun dispatchTouchEvents(vararg models: MotionEventModel) {
- models.forEach { model -> underTest.onTouchEvent(model) }
+ @Test
+ fun doubleTap() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ )
+
+ verify(onDoubleTapDetected).invoke()
+ assertThat(delayedRunnable).isNull()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun doubleTapButFeatureNotEnabled() = runTest {
+ underTest.isDoubleTapHandlingEnabled = false
+
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ )
+
+ verify(onDoubleTapDetected, never()).invoke()
+ assertThat(delayedRunnable).isNull()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun tapIntoLongPress() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ MotionEvent.obtain(
+ secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+ secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+ MotionEvent.ACTION_MOVE,
+ 123f + ViewConfiguration.getTouchSlop() - 0.1f,
+ 456f,
+ 0,
+ ),
+ )
+ delayedRunnable?.run()
+
+ verify(onDoubleTapDetected, never()).invoke()
+ verify(onSingleTapDetected).invoke(anyInt(), anyInt())
+ verify(onLongPressDetected).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun tapIntoDownHoldTooBrieflyToBeConsideredLongPress() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ MotionEvent.obtain(
+ secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+ secondTapTime + ViewConfiguration.getLongPressTimeout() + 1L,
+ MotionEvent.ACTION_UP,
+ 123f,
+ 456f,
+ 0,
+ ),
+ )
+ delayedRunnable?.run()
+
+ verify(onDoubleTapDetected, never()).invoke()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun tapIntoDrag() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f,
+ 456f,
+ 0,
+ ),
+ // Drag event within touch slop
+ MotionEvent.obtain(secondTapTime, secondTapTime, MotionEvent.ACTION_MOVE, 123f, 456f, 0)
+ .apply {
+ addBatch(
+ secondTapTime,
+ 123f + ViewConfiguration.getTouchSlop() + 0.1f,
+ 456f,
+ 0f,
+ 0f,
+ 0,
+ )
+ },
+ )
+ delayedRunnable?.run()
+
+ verify(onDoubleTapDetected, never()).invoke()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected).invoke(anyInt(), anyInt())
+ }
+
+ @Test
+ fun doubleTapOutOfAllowableSlop() = runTest {
+ val secondTapTime = ViewConfiguration.getDoubleTapTimeout() - 1L
+ val scaledDoubleTapSlop = ViewConfiguration.get(context).scaledDoubleTapSlop
+ dispatchTouchEvents(
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 123f, 456f, 0),
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 123f, 456f, 0),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_DOWN,
+ 123f + scaledDoubleTapSlop + 0.1f,
+ 456f + scaledDoubleTapSlop + 0.1f,
+ 0,
+ ),
+ MotionEvent.obtain(
+ secondTapTime,
+ secondTapTime,
+ MotionEvent.ACTION_UP,
+ 123f + scaledDoubleTapSlop + 0.1f,
+ 456f + scaledDoubleTapSlop + 0.1f,
+ 0,
+ ),
+ )
+
+ verify(onDoubleTapDetected, never()).invoke()
+ assertThat(delayedRunnable).isNull()
+ verify(onLongPressDetected, never()).invoke(anyInt(), anyInt())
+ verify(onSingleTapDetected, times(2)).invoke(anyInt(), anyInt())
+ }
+
+ private fun dispatchTouchEvents(vararg events: MotionEvent) {
+ events.forEach { event -> underTest.onTouchEvent(event) }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt
index 684af6fc8040..efc68f3b6884 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt
@@ -64,10 +64,10 @@ class KeyEventInteractorTest : SysuiTestCase() {
val isPowerButtonLongPressed by collectLastValue(
underTest.isPowerButtonLongPressed)
- repository.setPowerButtonBeingLongPressed(false)
+ repository.setPowerButtonLongPressed(false)
assertThat(isPowerButtonLongPressed).isFalse()
- repository.setPowerButtonBeingLongPressed(true)
+ repository.setPowerButtonLongPressed(true)
assertThat(isPowerButtonLongPressed).isTrue()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
index e203a276a2f2..1dddfc1bba9c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractorTest.kt
@@ -18,11 +18,17 @@
package com.android.systemui.keyguard.domain.interactor
import android.content.Intent
+import android.os.PowerManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.provider.Settings
import android.view.accessibility.accessibilityManagerWrapper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.logging.uiEventLogger
+import com.android.systemui.Flags.FLAG_DOUBLE_TAP_TO_SLEEP
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
@@ -39,6 +45,8 @@ import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
+import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.advanceTimeBy
@@ -46,14 +54,19 @@ import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mock
import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
private val kosmos =
testKosmos().apply {
@@ -61,17 +74,23 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
this.uiEventLogger = mock<UiEventLoggerFake>()
}
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
private lateinit var underTest: KeyguardTouchHandlingInteractor
private val logger = kosmos.uiEventLogger
private val testScope = kosmos.testScope
private val keyguardRepository = kosmos.fakeKeyguardRepository
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val secureSettingsRepository = kosmos.userAwareSecureSettingsRepository
+
+ @Mock private lateinit var powerManager: PowerManager
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, true)
+ overrideResource(com.android.internal.R.bool.config_supportDoubleTapSleep, true)
whenever(kosmos.accessibilityManagerWrapper.getRecommendedTimeoutMillis(anyInt(), anyInt()))
.thenAnswer { it.arguments[0] }
@@ -80,13 +99,13 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
@After
fun tearDown() {
- mContext
- .getOrCreateTestableResources()
- .removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled)
+ val testableResource = mContext.getOrCreateTestableResources()
+ testableResource.removeOverride(R.bool.long_press_keyguard_customize_lockscreen_enabled)
+ testableResource.removeOverride(com.android.internal.R.bool.config_supportDoubleTapSleep)
}
@Test
- fun isEnabled() =
+ fun isLongPressEnabled() =
testScope.runTest {
val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
KeyguardState.values().forEach { keyguardState ->
@@ -101,7 +120,7 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
}
@Test
- fun isEnabled_alwaysFalseWhenQuickSettingsAreVisible() =
+ fun isLongPressEnabled_alwaysFalseWhenQuickSettingsAreVisible() =
testScope.runTest {
val isEnabled = collectLastValue(underTest.isLongPressHandlingEnabled)
KeyguardState.values().forEach { keyguardState ->
@@ -112,7 +131,7 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
}
@Test
- fun isEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() =
+ fun isLongPressEnabled_alwaysFalseWhenConfigEnabledBooleanIsFalse() =
testScope.runTest {
overrideResource(R.bool.long_press_keyguard_customize_lockscreen_enabled, false)
createUnderTest()
@@ -294,6 +313,119 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
assertThat(isMenuVisible).isFalse()
}
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_flagEnabled_userSettingEnabled_onlyTrueInLockScreenState() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState)
+
+ if (keyguardState == KeyguardState.LOCKSCREEN) {
+ assertThat(isEnabled()).isTrue()
+ } else {
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_flagEnabled_userSettingDisabled_alwaysFalse() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, false)
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState)
+
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+
+ @Test
+ @DisableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_flagDisabled_userSettingEnabled_alwaysFalse() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState)
+
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+
+
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_flagEnabledAndConfigDisabled_alwaysFalse() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+ overrideResource(com.android.internal.R.bool.config_supportDoubleTapSleep, false)
+ createUnderTest()
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState)
+
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun isDoubleTapEnabled_quickSettingsVisible_alwaysFalse() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+
+ val isEnabled = collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ KeyguardState.entries.forEach { keyguardState ->
+ setUpState(keyguardState = keyguardState, isQuickSettingsVisible = true)
+
+ assertThat(isEnabled()).isFalse()
+ }
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun onDoubleClick_doubleTapEnabled() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, true)
+ val isEnabled by collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ runCurrent()
+
+ underTest.onDoubleClick()
+
+ assertThat(isEnabled).isTrue()
+ verify(powerManager).goToSleep(anyLong())
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_DOUBLE_TAP_TO_SLEEP)
+ fun onDoubleClick_doubleTapDisabled() {
+ testScope.runTest {
+ secureSettingsRepository.setBoolean(Settings.Secure.DOUBLE_TAP_TO_SLEEP, false)
+ val isEnabled by collectLastValue(underTest.isDoubleTapHandlingEnabled)
+ runCurrent()
+
+ underTest.onDoubleClick()
+
+ assertThat(isEnabled).isFalse()
+ verify(powerManager, never()).goToSleep(anyLong())
+ }
+ }
+
private suspend fun createUnderTest(isRevampedWppFeatureEnabled: Boolean = true) {
// This needs to be re-created for each test outside of kosmos since the flag values are
// read during initialization to set up flows. Maybe there is a better way to handle that.
@@ -309,6 +441,9 @@ class KeyguardTouchHandlingInteractorTest : SysuiTestCase() {
accessibilityManager = kosmos.accessibilityManagerWrapper,
pulsingGestureListener = kosmos.pulsingGestureListener,
faceAuthInteractor = kosmos.deviceEntryFaceAuthInteractor,
+ secureSettingsRepository = secureSettingsRepository,
+ powerManager = powerManager,
+ systemClock = kosmos.fakeSystemClock,
)
setUpState()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
index 8dff20eeb80a..9c4d93c17d00 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
@@ -48,10 +48,12 @@ import androidx.test.filters.SmallTest;
import com.android.media.flags.Flags;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.res.R;
import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListeningExecutorService;
import org.junit.Before;
import org.junit.Test;
@@ -61,6 +63,7 @@ import org.mockito.Captor;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.Executor;
import java.util.stream.Collectors;
@SmallTest
@@ -95,6 +98,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
private List<MediaDevice> mMediaDevices = new ArrayList<>();
private List<MediaItem> mMediaItems = new ArrayList<>();
MediaOutputSeekbar mSpyMediaOutputSeekbar;
+ Executor mMainExecutor = mContext.getMainExecutor();
+ ListeningExecutorService mBackgroundExecutor = ThreadUtils.getBackgroundExecutor();
@Before
public void setUp() {
@@ -108,6 +113,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
when(mMediaSwitchingController.getSessionVolumeMax()).thenReturn(TEST_MAX_VOLUME);
when(mMediaSwitchingController.getSessionVolume()).thenReturn(TEST_CURRENT_VOLUME);
when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME);
+ when(mMediaSwitchingController.getColorSchemeLegacy()).thenReturn(
+ mock(MediaOutputColorSchemeLegacy.class));
when(mIconCompat.toIcon(mContext)).thenReturn(mIcon);
when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1);
when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1);
@@ -122,7 +129,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice1, true));
mMediaItems.add(MediaItem.createDeviceMediaItem(mMediaDevice2, false));
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -148,7 +156,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
@Test
public void onBindViewHolder_bindPairNew_verifyView() {
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -173,7 +182,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
.map((item) -> item.getMediaDevice().get())
.collect(Collectors.toList()));
when(mMediaSwitchingController.getSessionName()).thenReturn(TEST_SESSION_NAME);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -195,7 +205,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
.map((item) -> item.getMediaDevice().get())
.collect(Collectors.toList()));
when(mMediaSwitchingController.getSessionName()).thenReturn(null);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -665,7 +676,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
@Test
public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() {
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -683,7 +695,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mMediaDevice2.getState()).isEqualTo(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -701,7 +714,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
assertThat(mMediaDevice2.getState()).isEqualTo(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_TRANSFER);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController,
+ mContext.getMainExecutor(), ThreadUtils.getBackgroundExecutor());
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
@@ -723,7 +737,8 @@ public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
when(mMediaDevice2.getState()).thenReturn(
LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
when(mMediaDevice2.getSelectionBehavior()).thenReturn(SELECTION_BEHAVIOR_GO_TO_APP);
- mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mMediaOutputAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mMainExecutor,
+ mBackgroundExecutor);
mMediaOutputAdapter.updateItems();
mViewHolder = (MediaOutputAdapterLegacy.MediaDeviceViewHolderLegacy) mMediaOutputAdapter
.onCreateViewHolder(new LinearLayout(mContext), 0);
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 68a591dd075f..1d42424bc6ed 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
@@ -16,11 +16,9 @@
package com.android.systemui.qs.panels.ui.viewmodel
-
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.FakeQSTile
import com.android.systemui.qs.pipeline.data.repository.tileSpecRepository
@@ -48,45 +46,43 @@ class DetailsViewModelTest : SysuiTestCase() {
}
@Test
- fun changeTileDetailsViewModel() = with(kosmos) {
- testScope.runTest {
- val specs = listOf(
- spec,
- specNoDetails,
- )
- tileSpecRepository.setTiles(0, specs)
- runCurrent()
+ fun changeTileDetailsViewModel() =
+ with(kosmos) {
+ testScope.runTest {
+ val specs = listOf(spec, specNoDetails)
+ tileSpecRepository.setTiles(0, specs)
+ runCurrent()
- val tiles = currentTilesInteractor.currentTiles.value
+ 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(currentTilesInteractor.currentTilesSpecs.size).isEqualTo(2)
+ assertThat(tiles[1].spec).isEqualTo(specNoDetails)
+ (tiles[1].tile as FakeQSTile).hasDetailsViewModel = false
- assertThat(underTest.activeTileDetails).isNull()
+ assertThat(underTest.activeTileDetails).isNull()
- // Click on the tile who has the `spec`.
- assertThat(underTest.onTileClicked(spec)).isTrue()
- assertThat(underTest.activeTileDetails).isNotNull()
- assertThat(underTest.activeTileDetails?.getTitle()).isEqualTo("internet")
+ // Click on the tile who has the `spec`.
+ assertThat(underTest.onTileClicked(spec)).isTrue()
+ assertThat(underTest.activeTileDetails).isNotNull()
+ assertThat(underTest.activeTileDetails?.title).isEqualTo("internet")
- // Click on a tile who dose not have a valid spec.
- assertThat(underTest.onTileClicked(null)).isFalse()
- assertThat(underTest.activeTileDetails).isNull()
+ // Click on a tile who dose not have a valid spec.
+ assertThat(underTest.onTileClicked(null)).isFalse()
+ assertThat(underTest.activeTileDetails).isNull()
- // Click again on the tile who has the `spec`.
- assertThat(underTest.onTileClicked(spec)).isTrue()
- assertThat(underTest.activeTileDetails).isNotNull()
- assertThat(underTest.activeTileDetails?.getTitle()).isEqualTo("internet")
+ // Click again on the tile who has the `spec`.
+ assertThat(underTest.onTileClicked(spec)).isTrue()
+ assertThat(underTest.activeTileDetails).isNotNull()
+ assertThat(underTest.activeTileDetails?.title).isEqualTo("internet")
- // Click on a tile who dose not have a detailed view.
- assertThat(underTest.onTileClicked(specNoDetails)).isFalse()
- assertThat(underTest.activeTileDetails).isNull()
+ // Click on a tile who dose not have a detailed view.
+ assertThat(underTest.onTileClicked(specNoDetails)).isFalse()
+ assertThat(underTest.activeTileDetails).isNull()
- underTest.closeDetailedView()
- assertThat(underTest.activeTileDetails).isNull()
+ underTest.closeDetailedView()
+ assertThat(underTest.activeTileDetails).isNull()
- assertThat(underTest.onTileClicked(null)).isFalse()
+ assertThat(underTest.onTileClicked(null)).isFalse()
+ }
}
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
index c3089761effc..5bde7ad27b7a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/CurrentTilesInteractorImplTest.kt
@@ -691,11 +691,11 @@ class CurrentTilesInteractorImplTest : SysuiTestCase() {
var currentModel: TileDetailsViewModel? = null
val setCurrentModel = { model: TileDetailsViewModel? -> currentModel = model }
tiles!![0].tile.getDetailsViewModel(setCurrentModel)
- assertThat(currentModel?.getTitle()).isEqualTo("a")
+ assertThat(currentModel?.title).isEqualTo("a")
currentModel = null
tiles!![1].tile.getDetailsViewModel(setCurrentModel)
- assertThat(currentModel?.getTitle()).isEqualTo("b")
+ assertThat(currentModel?.title).isEqualTo("b")
currentModel = null
tiles!![2].tile.getDetailsViewModel(setCurrentModel)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt
index 00ee1c36590c..1b497a2b36ed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractorTest.kt
@@ -24,6 +24,7 @@ import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager
import com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.Companion.LAUNCH_SOURCE_QS_TILE
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.shared.QSSettingsPackageRepository
import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
@@ -40,6 +41,7 @@ import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
@SmallTest
@EnabledOnRavenwood
@@ -53,14 +55,18 @@ class HearingDevicesTileUserActionInteractorTest : SysuiTestCase() {
@Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
@Mock private lateinit var dialogManager: HearingDevicesDialogManager
+ @Mock private lateinit var settingsPackageRepository: QSSettingsPackageRepository
@Before
fun setUp() {
+ whenever(settingsPackageRepository.getSettingsPackageName())
+ .thenReturn(SETTINGS_PACKAGE_NAME)
underTest =
HearingDevicesTileUserActionInteractor(
testScope.coroutineContext,
inputHandler,
dialogManager,
+ settingsPackageRepository,
)
}
@@ -91,6 +97,11 @@ class HearingDevicesTileUserActionInteractorTest : SysuiTestCase() {
QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput {
assertThat(it.intent.action).isEqualTo(Settings.ACTION_HEARING_DEVICES_SETTINGS)
+ assertThat(it.intent.`package`).isEqualTo(SETTINGS_PACKAGE_NAME)
}
}
+
+ companion object {
+ private const val SETTINGS_PACKAGE_NAME = "com.android.settings"
+ }
}
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 a832f486ef32..04eb709b8894 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
@@ -94,6 +94,36 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
}
@Test
+ fun showClock_wideLayout_returnsTrue() =
+ testScope.runTest {
+ kosmos.enableDualShade(wideLayout = true)
+
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade)
+ assertThat(underTest.showClock).isTrue()
+
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade)
+ assertThat(underTest.showClock).isTrue()
+ }
+
+ @Test
+ fun showClock_narrowLayoutOnNotificationsShade_returnsFalse() =
+ testScope.runTest {
+ kosmos.enableDualShade(wideLayout = false)
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade)
+
+ assertThat(underTest.showClock).isFalse()
+ }
+
+ @Test
+ fun showClock_narrowLayoutOnQuickSettingsShade_returnsTrue() =
+ testScope.runTest {
+ kosmos.enableDualShade(wideLayout = false)
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade)
+
+ assertThat(underTest.showClock).isTrue()
+ }
+
+ @Test
fun onShadeCarrierGroupClicked_launchesNetworkSettings() =
testScope.runTest {
val activityStarter = kosmos.activityStarter
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt
new file mode 100644
index 000000000000..e04162bf990a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationGroupingUtilTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar
+
+import android.platform.test.flag.junit.FlagsParameterization
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@SmallTest
+@RunWith(ParameterizedAndroidJunit4::class)
+class NotificationGroupingUtilTest(flags: FlagsParameterization) : SysuiTestCase() {
+
+ private lateinit var underTest: NotificationGroupingUtil
+
+ private lateinit var testHelper: NotificationTestHelper
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf(NotificationBundleUi.FLAG_NAME)
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
+ @Before
+ fun setup() {
+ testHelper = NotificationTestHelper(mContext, mDependency)
+ }
+
+ @Test
+ fun showsTime() {
+ val row = testHelper.createRow()
+
+ underTest = NotificationGroupingUtil(row)
+ assertThat(underTest.showsTime(row)).isTrue()
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
index c9d910c530ea..01046cd10d87 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationRemoteInputManagerTest.java
@@ -20,16 +20,29 @@ import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
import static junit.framework.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.ActivityOptions;
import android.app.Notification;
+import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
import android.os.SystemClock;
import android.os.UserHandle;
+import android.platform.test.flag.junit.FlagsParameterization;
import android.testing.TestableLooper;
+import android.util.Pair;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
@@ -42,19 +55,37 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.statusbar.policy.RemoteInputUriController;
import com.android.systemui.util.kotlin.JavaAdapter;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper
public class NotificationRemoteInputManagerTest extends SysuiTestCase {
+
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return FlagsParameterization.allCombinationsOf(NotificationBundleUi.FLAG_NAME);
+ }
+
private static final String TEST_PACKAGE_NAME = "test";
private static final int TEST_UID = 0;
@@ -69,14 +100,34 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
@Mock private NotificationClickNotifier mClickNotifier;
@Mock private NotificationLockscreenUserManager mLockscreenUserManager;
@Mock private PowerInteractor mPowerInteractor;
+ @Mock
+ NotificationRemoteInputManager.RemoteInputListener mRemoteInputListener;
+ private ActionClickLogger mActionClickLogger;
+ @Captor
+ ArgumentCaptor<NotificationRemoteInputManager.ClickHandler> mClickHandlerArgumentCaptor;
+ private Context mSpyContext;
+ private NotificationTestHelper mTestHelper;
private TestableNotificationRemoteInputManager mRemoteInputManager;
private NotificationEntry mEntry;
+ public NotificationRemoteInputManagerTest(FlagsParameterization flags) {
+ super();
+ mSetFlagsRule.setFlagsParameterization(flags);
+ }
+
@Before
- public void setUp() {
+ public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+ mSpyContext = spy(mContext);
+ doNothing().when(mSpyContext).startIntentSender(
+ any(), any(), anyInt(), anyInt(), anyInt(), any());
+
+
+ mTestHelper = new NotificationTestHelper(mSpyContext, mDependency);
+ mActionClickLogger = spy(new ActionClickLogger(logcatLogBuffer()));
+
mRemoteInputManager = new TestableNotificationRemoteInputManager(mContext,
mock(NotifPipelineFlags.class),
mLockscreenUserManager,
@@ -87,9 +138,10 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
mRemoteInputUriController,
new RemoteInputControllerLogger(logcatLogBuffer()),
mClickNotifier,
- new ActionClickLogger(logcatLogBuffer()),
+ mActionClickLogger,
mock(JavaAdapter.class),
mock(ShadeInteractor.class));
+ mRemoteInputManager.setRemoteInputListener(mRemoteInputListener);
mEntry = new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_NAME)
.setOpPkg(TEST_PACKAGE_NAME)
@@ -133,6 +185,70 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
assertTrue(mRemoteInputManager.shouldKeepForSmartReplyHistory(mEntry));
}
+ @Test
+ public void testActionClick() throws Exception {
+ RemoteViews.RemoteResponse response = mock(RemoteViews.RemoteResponse.class);
+ when(response.getLaunchOptions(any())).thenReturn(
+ Pair.create(mock(Intent.class), mock(ActivityOptions.class)));
+ ExpandableNotificationRow row = getRowWithReplyAction();
+ View actionView = ((LinearLayout) row.getPrivateLayout().getExpandedChild().findViewById(
+ com.android.internal.R.id.actions)).getChildAt(0);
+ Notification n = getNotification(row);
+ CountDownLatch latch = new CountDownLatch(1);
+ Consumer<NotificationEntry> consumer = notificationEntry -> latch.countDown();
+ if (!NotificationBundleUi.isEnabled()) {
+ mRemoteInputManager.addActionPressListener(consumer);
+ }
+
+ mRemoteInputManager.getRemoteViewsOnClickHandler().onInteraction(
+ actionView,
+ n.actions[0].actionIntent,
+ response);
+
+ verify(mActionClickLogger).logInitialClick(row.getKey(), 0, n.actions[0].actionIntent);
+ verify(mClickNotifier).onNotificationActionClick(
+ eq(row.getKey()), eq(0), eq(n.actions[0]), any(), eq(false));
+ verify(mCallback).handleRemoteViewClick(eq(actionView), eq(n.actions[0].actionIntent),
+ eq(false), eq(0), mClickHandlerArgumentCaptor.capture());
+
+ mClickHandlerArgumentCaptor.getValue().handleClick();
+ verify(mActionClickLogger).logStartingIntentWithDefaultHandler(
+ row.getKey(), n.actions[0].actionIntent, 0);
+
+ verify(mRemoteInputListener).releaseNotificationIfKeptForRemoteInputHistory(row.getKey());
+ if (NotificationBundleUi.isEnabled()) {
+ verify(row.getEntryAdapter()).onNotificationActionClicked();
+ } else {
+ latch.await(10, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private Notification getNotification(ExpandableNotificationRow row) {
+ if (NotificationBundleUi.isEnabled()) {
+ return row.getEntryAdapter().getSbn().getNotification();
+ } else {
+ return row.getEntry().getSbn().getNotification();
+ }
+ }
+
+ private ExpandableNotificationRow getRowWithReplyAction() throws Exception {
+ PendingIntent pi = PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"),
+ PendingIntent.FLAG_IMMUTABLE);
+ Notification n = new Notification.Builder(mSpyContext, "")
+ .setSmallIcon(com.android.systemui.res.R.drawable.ic_person)
+ .addAction(new Notification.Action(com.android.systemui.res.R.drawable.ic_person,
+ "reply", pi))
+ .build();
+ ExpandableNotificationRow row = mTestHelper.createRow(n);
+ row.onNotificationUpdated();
+ row.getPrivateLayout().setExpandedChild(Notification.Builder.recoverBuilder(mSpyContext, n)
+ .createBigContentView().apply(
+ mSpyContext,
+ row.getPrivateLayout(),
+ mRemoteInputManager.getRemoteViewsOnClickHandler()));
+ return row;
+ }
+
private class TestableNotificationRemoteInputManager extends NotificationRemoteInputManager {
TestableNotificationRemoteInputManager(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index b085ba4a4dad..485b9febc284 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -19,8 +19,8 @@ package com.android.systemui.statusbar.chips.call.ui.viewmodel
import android.app.PendingIntent
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import android.view.View
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Expandable
@@ -33,13 +33,16 @@ import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.plugins.activityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.chips.StatusBarChipsReturnAnimations
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.statusbar.core.StatusBarRootModernization
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.phone.ongoingcall.DisableChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.EnableChipsModernization
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.addOngoingCallState
import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallTestHelper.removeOngoingCallState
import com.android.systemui.testKosmos
@@ -51,10 +54,16 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class CallChipViewModelTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+class CallChipViewModelTest(flags: FlagsParameterization) : SysuiTestCase() {
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
private val kosmos = testKosmos().useUnconfinedTestDispatcher()
private val chipBackgroundView = mock<ChipBackgroundContainer>()
@@ -87,9 +96,10 @@ class CallChipViewModelTest : SysuiTestCase() {
kosmos.runTest {
val latest by collectLastValue(underTest.chip)
- addOngoingCallState(startTimeMs = 0)
+ addOngoingCallState(startTimeMs = 0, isAppVisible = false)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
}
@Test
@@ -97,9 +107,10 @@ class CallChipViewModelTest : SysuiTestCase() {
kosmos.runTest {
val latest by collectLastValue(underTest.chip)
- addOngoingCallState(startTimeMs = -2)
+ addOngoingCallState(startTimeMs = -2, isAppVisible = false)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
}
@Test
@@ -107,9 +118,82 @@ class CallChipViewModelTest : SysuiTestCase() {
kosmos.runTest {
val latest by collectLastValue(underTest.chip)
- addOngoingCallState(startTimeMs = 345)
+ addOngoingCallState(startTimeMs = 345, isAppVisible = false)
assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isHidden).isFalse()
+ }
+
+ @Test
+ @DisableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
+ fun chipLegacy_inCallWithVisibleApp_zeroStartTime_isHiddenAsInactive() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ addOngoingCallState(startTimeMs = 0, isAppVisible = true)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
+ }
+
+ @Test
+ @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
+ @EnableChipsModernization
+ fun chipWithReturnAnimation_inCallWithVisibleApp_zeroStartTime_isHiddenAsIconOnly() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ addOngoingCallState(startTimeMs = 0, isAppVisible = true)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue()
+ }
+
+ @Test
+ @DisableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
+ fun chipLegacy_inCallWithVisibleApp_negativeStartTime_isHiddenAsInactive() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ addOngoingCallState(startTimeMs = -2, isAppVisible = true)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
+ }
+
+ @Test
+ @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
+ @EnableChipsModernization
+ fun chipWithReturnAnimation_inCallWithVisibleApp_negativeStartTime_isHiddenAsIconOnly() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ addOngoingCallState(startTimeMs = -2, isAppVisible = true)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.IconOnly::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue()
+ }
+
+ @Test
+ @DisableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
+ fun chipLegacy_inCallWithVisibleApp_positiveStartTime_isHiddenAsInactive() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ addOngoingCallState(startTimeMs = 345, isAppVisible = true)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
+ }
+
+ @Test
+ @EnableFlags(StatusBarChipsReturnAnimations.FLAG_NAME)
+ @EnableChipsModernization
+ fun chipWithReturnAnimation_inCallWithVisibleApp_positiveStartTime_isHiddenAsTimer() =
+ kosmos.runTest {
+ val latest by collectLastValue(underTest.chip)
+
+ addOngoingCallState(startTimeMs = 345, isAppVisible = true)
+
+ assertThat(latest).isInstanceOf(OngoingActivityChipModel.Active.Timer::class.java)
+ assertThat((latest as OngoingActivityChipModel.Active).isHidden).isTrue()
}
@Test
@@ -418,5 +502,18 @@ class CallChipViewModelTest : SysuiTestCase() {
private const val PROMOTED_BACKGROUND_COLOR = 65
private const val PROMOTED_PRIMARY_TEXT_COLOR = 98
+
+ @get:Parameters(name = "{0}")
+ @JvmStatic
+ val flags: List<FlagsParameterization>
+ get() = buildList {
+ addAll(
+ FlagsParameterization.allCombinationsOf(
+ StatusBarRootModernization.FLAG_NAME,
+ StatusBarChipsModernization.FLAG_NAME,
+ StatusBarChipsReturnAnimations.FLAG_NAME,
+ )
+ )
+ }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index e2d1498270c8..e39fa7099953 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -137,7 +137,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- addOngoingCallState()
+ addOngoingCallState(isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
@@ -163,7 +163,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- addOngoingCallState()
+ addOngoingCallState(isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
@@ -178,7 +178,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- addOngoingCallState(key = notificationKey)
+ addOngoingCallState(key = notificationKey, isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
@@ -190,7 +190,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
kosmos.runTest {
// Start with just the lowest priority chip shown
val callNotificationKey = "call"
- addOngoingCallState(key = callNotificationKey)
+ addOngoingCallState(key = callNotificationKey, isAppVisible = false)
// And everything else hidden
mediaProjectionState.value = MediaProjectionState.NotProjecting
screenRecordState.value = ScreenRecordModel.DoingNothing
@@ -225,7 +225,7 @@ class OngoingActivityChipsViewModelTest : SysuiTestCase() {
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
val callNotificationKey = "call"
- addOngoingCallState(key = callNotificationKey)
+ addOngoingCallState(key = callNotificationKey, isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
index 670ebadcf5a7..f06244f4f637 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt
@@ -236,7 +236,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun primaryChip_screenRecordShowAndCallShow_screenRecordShown() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- addOngoingCallState("call")
+ addOngoingCallState("call", isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
@@ -249,7 +249,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
kosmos.runTest {
val callNotificationKey = "call"
screenRecordState.value = ScreenRecordModel.Recording
- addOngoingCallState(callNotificationKey)
+ addOngoingCallState(callNotificationKey, isAppVisible = false)
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -296,7 +296,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
@Test
fun chipsLegacy_oneChip_notSquished() =
kosmos.runTest {
- addOngoingCallState()
+ addOngoingCallState(isAppVisible = false)
val latest by collectLastValue(underTest.chipsLegacy)
@@ -323,7 +323,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun chipsLegacy_twoTimerChips_isSmallPortrait_bothSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
val latest by collectLastValue(underTest.chipsLegacy)
@@ -385,7 +385,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun chipsLegacy_countdownChipAndTimerChip_countdownNotSquished_butTimerSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Starting(millisUntilStarted = 2000)
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
val latest by collectLastValue(underTest.chipsLegacy)
@@ -431,7 +431,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
.isInstanceOf(OngoingActivityChipModel.Inactive::class.java)
// WHEN there's 2 chips
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
// THEN they both become squished
assertThat(latest!!.primary)
@@ -487,7 +487,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun chipsLegacy_twoChips_isLandscape_notSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
// WHEN we're in landscape
val config =
@@ -533,7 +533,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
fun chipsLegacy_twoChips_isLargeScreen_notSquished() =
kosmos.runTest {
screenRecordState.value = ScreenRecordModel.Recording
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
// WHEN we're on a large screen
kosmos.displayStateRepository.setIsLargeScreen(true)
@@ -627,7 +627,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
@@ -642,7 +642,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.DoingNothing
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- addOngoingCallState(key = "call")
+ addOngoingCallState(key = "call", isAppVisible = false)
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -681,7 +681,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
mediaProjectionState.value = MediaProjectionState.NotProjecting
val callNotificationKey = "call"
- addOngoingCallState(key = callNotificationKey)
+ addOngoingCallState(key = callNotificationKey, isAppVisible = false)
val latest by collectLastValue(underTest.primaryChip)
@@ -697,7 +697,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
// MediaProjection covers both share-to-app and cast-to-other-device
mediaProjectionState.value = MediaProjectionState.NotProjecting
- addOngoingCallState(key = callNotificationKey)
+ addOngoingCallState(key = callNotificationKey, isAppVisible = false)
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
@@ -913,7 +913,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
),
activeNotificationModel(
key = "fourthNotif",
- statusBarChipIcon = thirdIcon,
+ statusBarChipIcon = fourthIcon,
promotedContent =
PromotedNotificationContentModel.Builder("fourthNotif").build(),
),
@@ -975,7 +975,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val unused by collectLastValue(underTest.chips)
val callNotificationKey = "call"
- addOngoingCallState(callNotificationKey)
+ addOngoingCallState(callNotificationKey, isAppVisible = false)
val firstIcon = createStatusBarIconViewOrNull()
activeNotificationListRepository.addNotifs(
@@ -1053,7 +1053,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
val latest by collectLastValue(underTest.chipsLegacy)
val unused by collectLastValue(underTest.chips)
- addOngoingCallState(callNotificationKey)
+ addOngoingCallState(callNotificationKey, isAppVisible = false)
screenRecordState.value = ScreenRecordModel.Recording
activeNotificationListRepository.addNotif(
activeNotificationModel(
@@ -1160,7 +1160,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
assertIsNotifChip(latest, context, notifIcon, "notif")
// WHEN the higher priority call chip is added
- addOngoingCallState(callNotificationKey)
+ addOngoingCallState(callNotificationKey, isAppVisible = false)
// THEN the higher priority call chip is used
assertIsCallChip(latest, callNotificationKey, context)
@@ -1191,7 +1191,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
screenRecordState.value = ScreenRecordModel.Recording
mediaProjectionState.value =
MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
- addOngoingCallState(callNotificationKey)
+ addOngoingCallState(callNotificationKey, isAppVisible = false)
val notifIcon = createStatusBarIconViewOrNull()
activeNotificationListRepository.addNotif(
activeNotificationModel(
@@ -1253,7 +1253,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() {
assertThat(unused).isEqualTo(MultipleOngoingActivityChipsModel())
// WHEN the higher priority call chip is added
- addOngoingCallState(callNotificationKey)
+ addOngoingCallState(callNotificationKey, isAppVisible = false)
// THEN the higher priority call chip is used as primary and notif is demoted to
// secondary
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt
index b6889afa4e8a..faafa073be4c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapterTest.kt
@@ -29,8 +29,10 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.res.R
import com.android.systemui.statusbar.RankingBuilder
+import com.android.systemui.statusbar.notification.mockNotificationActivityStarter
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.entryAdapterFactory
+import com.android.systemui.statusbar.notification.row.mockNotificationActionClickManager
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
import com.android.systemui.testKosmos
@@ -355,16 +357,27 @@ class NotificationEntryAdapterTest : SysuiTestCase() {
val notification: Notification =
Notification.Builder(mContext, "").setSmallIcon(R.drawable.ic_person).build()
- val entry =
- NotificationEntryBuilder()
- .setNotification(notification)
- .setImportance(NotificationManager.IMPORTANCE_MIN)
- .build()
+ val entry = NotificationEntryBuilder().setNotification(notification).build()
underTest = factory.create(entry) as NotificationEntryAdapter
underTest.onNotificationBubbleIconClicked()
- verify((factory as? EntryAdapterFactoryImpl)?.getNotificationActivityStarter())
- ?.onNotificationBubbleIconClicked(entry)
+ verify(kosmos.mockNotificationActivityStarter).onNotificationBubbleIconClicked(entry)
+ }
+
+ @Test
+ @EnableFlags(NotificationBundleUi.FLAG_NAME)
+ fun onNotificationActionClicked() {
+ val notification: Notification =
+ Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .addAction(Mockito.mock(Notification.Action::class.java))
+ .build()
+
+ val entry = NotificationEntryBuilder().setNotification(notification).build()
+
+ underTest = factory.create(entry) as NotificationEntryAdapter
+ underTest.onNotificationActionClicked()
+ verify(kosmos.mockNotificationActionClickManager).onNotificationActionClicked(entry)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt
index 8a9720ea3cb0..732180810880 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinatorTest.kt
@@ -34,6 +34,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
+import kotlin.test.assertEquals
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -87,6 +88,18 @@ class BundleCoordinatorTest : SysuiTestCase() {
isFalse()
}
+ @Test
+ fun testBundler_getBundleIdOrNull_returnBundleId() {
+ val classifiedEntry = makeEntryOfChannelType(PROMOTIONS_ID)
+ assertEquals(coordinator.bundler.getBundleIdOrNull(classifiedEntry), PROMOTIONS_ID)
+ }
+
+ @Test
+ fun testBundler_getBundleIdOrNull_returnNull() {
+ val unclassifiedEntry = makeEntryOfChannelType("not system channel")
+ assertEquals(coordinator.bundler.getBundleIdOrNull(unclassifiedEntry), null)
+ }
+
private fun makeEntryOfChannelType(
type: String,
buildBlock: NotificationEntryBuilder.() -> Unit = {}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index 30983550f0f9..44d88c31c5f1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -50,6 +50,8 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.DecisionImpl
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderWrapper.FullScreenIntentDecisionImpl
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider
+import com.android.systemui.statusbar.notification.row.mockNotificationActionClickManager
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.phone.NotificationGroupTestHelper
import com.android.systemui.testKosmos
import com.android.systemui.util.concurrency.FakeExecutor
@@ -138,6 +140,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
headsUpViewBinder,
visualInterruptionDecisionProvider,
remoteInputManager,
+ kosmos.mockNotificationActionClickManager,
launchFullScreenIntentProvider,
flags,
statusBarNotificationChipsInteractor,
@@ -161,8 +164,14 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
verify(notifPipeline).addOnBeforeFinalizeFilterListener(capture())
}
onHeadsUpChangedListener = withArgCaptor { verify(headsUpManager).addListener(capture()) }
- actionPressListener = withArgCaptor {
- verify(remoteInputManager).addActionPressListener(capture())
+ actionPressListener = if (NotificationBundleUi.isEnabled) {
+ withArgCaptor {
+ verify(kosmos.mockNotificationActionClickManager).addActionClickListener(capture())
+ }
+ } else {
+ withArgCaptor {
+ verify(remoteInputManager).addActionPressListener(capture())
+ }
}
given(headsUpManager.allEntries).willAnswer { huns.stream() }
given(headsUpManager.isHeadsUpEntry(anyString())).willAnswer { invocation ->
@@ -260,7 +269,7 @@ class HeadsUpCoordinatorTest : SysuiTestCase() {
addHUN(entry)
actionPressListener.accept(entry)
- verify(headsUpManager, times(1)).setUserActionMayIndirectlyRemove(entry)
+ verify(headsUpManager, times(1)).setUserActionMayIndirectlyRemove(entry.key)
whenever(headsUpManager.canRemoveImmediately(anyString())).thenReturn(true)
assertFalse(notifLifetimeExtender.maybeExtendLifetime(entry, 0))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
index a90539413adb..e28e587d2cdc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinatorTest.kt
@@ -28,6 +28,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.statusbar.statusBarStateController
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.notification.collection.BundleEntry
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -280,6 +281,8 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
val group = GroupEntryBuilder().setSummary(parent).addChild(child1).addChild(child2).build()
val listEntryList = listOf(group, solo1, solo2)
val notificationEntryList = listOf(solo1, solo2, parent, child1, child2)
+ val bundle = BundleEntry("bundleKey")
+ val bundleList = listOf(bundle)
runCoordinatorTest {
// All entries are added (and now unseen)
@@ -300,6 +303,11 @@ class LockScreenMinimalismCoordinatorTest : SysuiTestCase() {
assertThatTopOngoingKey().isEqualTo(null)
assertThatTopUnseenKey().isEqualTo(solo1.key)
+ // TEST: bundle is not picked
+ onBeforeTransformGroupsListener.onBeforeTransformGroups(bundleList)
+ assertThatTopOngoingKey().isEqualTo(null)
+ assertThatTopUnseenKey().isEqualTo(null)
+
// TEST: if top-ranked unseen is colorized, fall back to #2 ranked unseen
solo1.setColorizedFgs(true)
onBeforeTransformGroupsListener.onBeforeTransformGroups(listEntryList)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
index c5752691da44..65763a359c0f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinatorTest.java
@@ -45,6 +45,7 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.RankingBuilder;
import com.android.systemui.statusbar.SbnBuilder;
+import com.android.systemui.statusbar.notification.collection.BundleEntry;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -273,6 +274,18 @@ public class RankingCoordinatorTest extends SysuiTestCase {
}
@Test
+ public void testSilentSectioner_acceptsBundle() {
+ BundleEntry bundleEntry = new BundleEntry("testBundleKey");
+ assertTrue(mSilentSectioner.isInSection(bundleEntry));
+ }
+
+ @Test
+ public void testMinimizedSectioner_rejectsBundle() {
+ BundleEntry bundleEntry = new BundleEntry("testBundleKey");
+ assertFalse(mMinimizedSectioner.isInSection(bundleEntry));
+ }
+
+ @Test
public void testMinSection() {
when(mHighPriorityProvider.isHighPriority(mEntry)).thenReturn(false);
setRankingAmbient(true);
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 9804932918dc..8560b66d961f 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
@@ -1071,7 +1071,7 @@ class HeadsUpManagerImplTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(underTest.canRemoveImmediately(notifEntry.key)).isFalse()
- underTest.setUserActionMayIndirectlyRemove(notifEntry)
+ underTest.setUserActionMayIndirectlyRemove(notifEntry.key)
assertThat(underTest.canRemoveImmediately(notifEntry.key)).isTrue()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 6415f8c25a37..e6b2c2541447 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -29,6 +29,7 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -363,6 +364,7 @@ public class NotificationTestHelper {
.setUid(UID)
.setInitialPid(2000)
.setNotification(summary)
+ .setUser(USER_HANDLE)
.setParent(GroupEntry.ROOT_ENTRY)
.build();
GroupEntryBuilder groupEntry = new GroupEntryBuilder()
@@ -743,11 +745,12 @@ public class NotificationTestHelper {
mock(MetricsLogger.class),
mock(PeopleNotificationIdentifier.class),
mock(NotificationIconStyleProvider.class),
- mock(VisualStabilityCoordinator.class)
+ mock(VisualStabilityCoordinator.class),
+ mock(NotificationActionClickManager.class)
).create(entry);
row.initialize(
- entryAdapter,
+ spy(entryAdapter),
entry,
mock(RemoteInputViewSubcomponent.Factory.class),
APP_NAME,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
index 398f3fbecbd1..f4204af7829b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt
@@ -82,6 +82,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
statusBarChipIconView = testIconView,
contentIntent = testIntent,
promotedContent = testPromotedContent,
+ isAppVisible = false,
)
// Verify model is InCall and has the correct icon, intent, and promoted content.
@@ -98,7 +99,6 @@ class OngoingCallInteractorTest : SysuiTestCase() {
@Test
fun ongoingCallNotification_setsAllFields_withAppVisible() =
kosmos.runTest {
- kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true
val latest by collectLastValue(underTest.ongoingCallState)
// Set up notification with icon view and intent
@@ -113,6 +113,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
statusBarChipIconView = testIconView,
contentIntent = testIntent,
promotedContent = testPromotedContent,
+ isAppVisible = true,
)
// Verify model is InCall with visible app and has the correct icon, intent, and
@@ -141,10 +142,9 @@ class OngoingCallInteractorTest : SysuiTestCase() {
@Test
fun ongoingCallNotification_appVisibleInitially_emitsInCallWithVisibleApp() =
kosmos.runTest {
- kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = true
val latest by collectLastValue(underTest.ongoingCallState)
- addOngoingCallState(uid = UID)
+ addOngoingCallState(uid = UID, isAppVisible = true)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
assertThat((latest as OngoingCallModel.InCall).isAppVisible).isTrue()
@@ -153,10 +153,9 @@ class OngoingCallInteractorTest : SysuiTestCase() {
@Test
fun ongoingCallNotification_appNotVisibleInitially_emitsInCall() =
kosmos.runTest {
- kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
val latest by collectLastValue(underTest.ongoingCallState)
- addOngoingCallState(uid = UID)
+ addOngoingCallState(uid = UID, isAppVisible = false)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
assertThat((latest as OngoingCallModel.InCall).isAppVisible).isFalse()
@@ -168,8 +167,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
val latest by collectLastValue(underTest.ongoingCallState)
// Start with notification and app not visible
- kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
- addOngoingCallState(uid = UID)
+ addOngoingCallState(uid = UID, isAppVisible = false)
assertThat(latest).isInstanceOf(OngoingCallModel.InCall::class.java)
assertThat((latest as OngoingCallModel.InCall).isAppVisible).isFalse()
@@ -245,9 +243,7 @@ class OngoingCallInteractorTest : SysuiTestCase() {
.ongoingProcessRequiresStatusBarVisible
)
- kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false
-
- addOngoingCallState(uid = UID)
+ addOngoingCallState(uid = UID, isAppVisible = false)
assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java)
assertThat((ongoingCallState as OngoingCallModel.InCall).isAppVisible).isFalse()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt
new file mode 100644
index 000000000000..6d3813c90bfd
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/TopLevelWindowEffectsTest.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.topwindoweffects
+
+import android.view.View
+import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.app.viewcapture.ViewCapture
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository
+import com.android.systemui.keyevent.data.repository.keyEventRepository
+import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
+import com.android.systemui.keyevent.domain.interactor.keyEventInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.android.systemui.topwindoweffects.data.repository.fakeSqueezeEffectRepository
+import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor
+import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot
+import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TopLevelWindowEffectsTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ @Mock
+ private lateinit var windowManager: WindowManager
+
+ @Mock
+ private lateinit var viewCapture: Lazy<ViewCapture>
+
+ @Mock
+ private lateinit var viewModelFactory: SqueezeEffectViewModel.Factory
+
+ private val Kosmos.underTest by Kosmos.Fixture {
+ TopLevelWindowEffects(
+ context = mContext,
+ applicationScope = testScope.backgroundScope,
+ windowManager = ViewCaptureAwareWindowManager(
+ windowManager = windowManager,
+ lazyViewCapture = viewCapture,
+ isViewCaptureEnabled = false
+ ),
+ keyEventInteractor = keyEventInteractor,
+ viewModelFactory = viewModelFactory,
+ squeezeEffectInteractor = SqueezeEffectInteractor(
+ squeezeEffectRepository = fakeSqueezeEffectRepository
+ )
+ )
+ }
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ doNothing().whenever(windowManager).addView(any<View>(), any<WindowManager.LayoutParams>())
+ doNothing().whenever(windowManager).removeView(any<View>())
+ doNothing().whenever(windowManager).removeView(any<EffectsWindowRoot>())
+ }
+
+ @Test
+ fun noWindowWhenSqueezeEffectDisabled() =
+ kosmos.runTest {
+ fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = false
+
+ underTest.start()
+
+ verify(windowManager, never()).addView(any<View>(), any<WindowManager.LayoutParams>())
+ }
+
+ @Test
+ fun addViewToWindowWhenSqueezeEffectEnabled() =
+ kosmos.runTest {
+ fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = true
+ fakeKeyEventRepository.setPowerButtonDown(true)
+
+ underTest.start()
+
+ verify(windowManager, times(1)).addView(any<View>(),
+ any<WindowManager.LayoutParams>())
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryTest.kt
new file mode 100644
index 000000000000..9b01fd3242e5
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.data.repository
+
+import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.provider.Settings.Global.POWER_BUTTON_LONG_PRESS
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.shared.Flags
+import com.android.systemui.testKosmos
+import com.android.systemui.util.settings.FakeGlobalSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SqueezeEffectRepositoryTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+ private val globalSettings = FakeGlobalSettings(StandardTestDispatcher())
+
+ @Mock
+ private lateinit var bgHandler: Handler
+
+ private val Kosmos.underTest by Kosmos.Fixture {
+ SqueezeEffectRepositoryImpl(
+ bgHandler = bgHandler,
+ bgCoroutineContext = testScope.testScheduler,
+ globalSettings = globalSettings
+ )
+ }
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @DisableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT)
+ @Test
+ fun testSqueezeEffectDisabled_WhenFlagDisabled() =
+ kosmos.runTest {
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isFalse()
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT)
+ @Test
+ fun testSqueezeEffectDisabled_WhenFlagEnabled_GlobalSettingsDisabled() =
+ kosmos.runTest {
+ globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 0)
+
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isFalse()
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_LPP_SQUEEZE_EFFECT)
+ @Test
+ fun testSqueezeEffectEnabled_WhenFlagEnabled_GlobalSettingEnabled() =
+ kosmos.runTest {
+ globalSettings.putInt(POWER_BUTTON_LONG_PRESS, 5)
+
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isTrue()
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractorTest.kt
new file mode 100644
index 000000000000..a94d49c5d40e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/topwindoweffects/domain/interactor/SqueezeEffectInteractorTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.android.systemui.topwindoweffects.data.repository.fakeSqueezeEffectRepository
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SqueezeEffectInteractorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ private val Kosmos.underTest by Kosmos.Fixture {
+ SqueezeEffectInteractor(
+ squeezeEffectRepository = fakeSqueezeEffectRepository
+ )
+ }
+
+ @Test
+ fun testIsSqueezeEffectDisabled_whenDisabledInRepository() =
+ kosmos.runTest {
+ fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = false
+
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isFalse()
+ }
+
+ @Test
+ fun testIsSqueezeEffectEnabled_whenEnabledInRepository() =
+ kosmos.runTest {
+ fakeSqueezeEffectRepository.isSqueezeEffectEnabled.value = true
+
+ val isSqueezeEffectEnabled by collectLastValue(underTest.isSqueezeEffectEnabled)
+
+ assertThat(isSqueezeEffectEnabled).isTrue()
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
index b52db83d513c..7657a2179d4f 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockController.kt
@@ -13,7 +13,6 @@
*/
package com.android.systemui.plugins.clocks
-import android.graphics.RectF
import com.android.systemui.plugins.annotations.ProtectedInterface
import com.android.systemui.plugins.annotations.SimpleProperty
import java.io.PrintWriter
@@ -50,5 +49,5 @@ interface ClockController {
}
interface ClockEventListener {
- fun onBoundsChanged(bounds: RectF)
+ fun onBoundsChanged(bounds: VRectF)
}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
index 20ee6c120ee8..a658c15a1a99 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockFaceEvents.kt
@@ -45,6 +45,7 @@ interface ClockFaceEvents {
* render within the centered targetRect to avoid obstructing other elements. The specified
* targetRegion is relative to the parent view.
*/
+ @Deprecated("No longer necessary, pending removal")
fun onTargetRegionChanged(targetRegion: Rect?)
/** Called to notify the clock about its display. */
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
index 02a3902a042c..f9ff75d5fdc8 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockLogger.kt
@@ -16,7 +16,6 @@
package com.android.systemui.plugins.clocks
-import android.graphics.Rect
import android.view.View
import android.view.View.MeasureSpec
import com.android.systemui.log.core.LogLevel
@@ -56,12 +55,9 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) :
}
fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
- d({ "onLayout($bool1, ${Rect(int1, int2, long1.toInt(), long2.toInt())})" }) {
+ d({ "onLayout($bool1, ${VRect(long1.toULong())})" }) {
bool1 = changed
- int1 = left
- int2 = top
- long1 = right.toLong()
- long2 = bottom.toLong()
+ long1 = VRect(left, top, right, bottom).data.toLong()
}
}
@@ -108,8 +104,11 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) :
}
}
- fun animateDoze() {
- d("animateDoze()")
+ fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
+ d({ "animateDoze(isDozing=$bool1, isAnimated=$bool2)" }) {
+ bool1 = isDozing
+ bool2 = isAnimated
+ }
}
fun animateCharge() {
@@ -117,10 +116,7 @@ class ClockLogger(private val view: View?, buffer: MessageBuffer, tag: String) :
}
fun animateFidget(x: Float, y: Float) {
- d({ "animateFidget($str1, $str2)" }) {
- str1 = x.toString()
- str2 = y.toString()
- }
+ d({ "animateFidget(${VPointF(long1.toULong())})" }) { long1 = VPointF(x, y).data.toLong() }
}
companion object {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt
index 3dae5305542b..1fb37ec28835 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/VPoint.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VPoint.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.shared.clocks
+package com.android.systemui.plugins.clocks
import android.graphics.Point
import android.graphics.PointF
@@ -30,22 +30,20 @@ private val Y_MASK: ULong = 0x00000000FFFFFFFFU
private fun unpackX(data: ULong): Int = ((data and X_MASK) shr 32).toInt()
-private fun unpackY(data: ULong): Int = (data and Y_MASK).toInt()
+private fun unpackY(data: ULong): Int = ((data and Y_MASK) shr 0).toInt()
private fun pack(x: Int, y: Int): ULong {
- return ((x.toULong() shl 32) and X_MASK) or (y.toULong() and Y_MASK)
+ return ((x.toULong() shl 32) and X_MASK) or ((y.toULong() shl 0) and Y_MASK)
}
@JvmInline
-value class VPointF(private val data: ULong) {
+value class VPointF(val data: ULong) {
val x: Float
get() = Float.fromBits(unpackX(data))
val y: Float
get() = Float.fromBits(unpackY(data))
- constructor() : this(0f, 0f)
-
constructor(pt: PointF) : this(pt.x, pt.y)
constructor(x: Int, y: Int) : this(x.toFloat(), y.toFloat())
@@ -139,15 +137,13 @@ value class VPointF(private val data: ULong) {
}
@JvmInline
-value class VPoint(private val data: ULong) {
+value class VPoint(val data: ULong) {
val x: Int
get() = unpackX(data)
val y: Int
get() = unpackY(data)
- constructor() : this(0, 0)
-
constructor(x: Int, y: Int) : this(pack(x, y))
fun toPoint() = Point(x, y)
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt
new file mode 100644
index 000000000000..3c1adf22a405
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/VRect.kt
@@ -0,0 +1,188 @@
+/*
+ * 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.plugins.clocks
+
+import android.graphics.Rect
+import android.graphics.RectF
+import android.util.Half
+
+private val LEFT_MASK: ULong = 0xFFFF000000000000U
+private val TOP_MASK: ULong = 0x0000FFFF00000000U
+private val RIGHT_MASK: ULong = 0x00000000FFFF0000U
+private val BOTTOM_MASK: ULong = 0x000000000000FFFFU
+
+private fun unpackLeft(data: ULong): Short = ((data and LEFT_MASK) shr 48).toShort()
+
+private fun unpackTop(data: ULong): Short = ((data and TOP_MASK) shr 32).toShort()
+
+private fun unpackRight(data: ULong): Short = ((data and RIGHT_MASK) shr 16).toShort()
+
+private fun unpackBottom(data: ULong): Short = ((data and BOTTOM_MASK) shr 0).toShort()
+
+private fun pack(left: Short, top: Short, right: Short, bottom: Short): ULong {
+ return ((left.toULong() shl 48) and LEFT_MASK) or
+ ((top.toULong() shl 32) and TOP_MASK) or
+ ((right.toULong() shl 16) and RIGHT_MASK) or
+ ((bottom.toULong() shl 0) and BOTTOM_MASK)
+}
+
+@JvmInline
+value class VRectF(val data: ULong) {
+ val left: Float
+ get() = fromBits(unpackLeft(data))
+
+ val top: Float
+ get() = fromBits(unpackTop(data))
+
+ val right: Float
+ get() = fromBits(unpackRight(data))
+
+ val bottom: Float
+ get() = fromBits(unpackBottom(data))
+
+ val width: Float
+ get() = right - left
+
+ val height: Float
+ get() = bottom - top
+
+ constructor(rect: RectF) : this(rect.left, rect.top, rect.right, rect.bottom)
+
+ constructor(
+ rect: Rect
+ ) : this(
+ left = rect.left.toFloat(),
+ top = rect.top.toFloat(),
+ right = rect.right.toFloat(),
+ bottom = rect.bottom.toFloat(),
+ )
+
+ constructor(
+ left: Float,
+ top: Float,
+ right: Float,
+ bottom: Float,
+ ) : this(pack(toBits(left), toBits(top), toBits(right), toBits(bottom)))
+
+ val center: VPointF
+ get() = VPointF(left, top) + size / 2f
+
+ val size: VPointF
+ get() = VPointF(width, height)
+
+ override fun toString() = "($left, $top) -> ($right, $bottom)"
+
+ companion object {
+ private fun toBits(value: Float): Short = Half.halfToShortBits(Half.toHalf(value))
+
+ private fun fromBits(value: Short): Float = Half.toFloat(Half.intBitsToHalf(value.toInt()))
+
+ fun fromCenter(center: VPointF, size: VPointF): VRectF {
+ return VRectF(
+ center.x - size.x / 2,
+ center.y - size.y / 2,
+ center.x + size.x / 2,
+ center.y + size.y / 2,
+ )
+ }
+
+ fun fromTopLeft(pos: VPointF, size: VPointF): VRectF {
+ return VRectF(pos.x, pos.y, pos.x + size.x, pos.y + size.y)
+ }
+
+ val ZERO = VRectF(0f, 0f, 0f, 0f)
+ }
+}
+
+@JvmInline
+value class VRect(val data: ULong) {
+ val left: Int
+ get() = unpackLeft(data).toInt()
+
+ val top: Int
+ get() = unpackTop(data).toInt()
+
+ val right: Int
+ get() = unpackRight(data).toInt()
+
+ val bottom: Int
+ get() = unpackBottom(data).toInt()
+
+ val width: Int
+ get() = right - left
+
+ val height: Int
+ get() = bottom - top
+
+ constructor(
+ rect: Rect
+ ) : this(
+ left = rect.left.toShort(),
+ top = rect.top.toShort(),
+ right = rect.right.toShort(),
+ bottom = rect.bottom.toShort(),
+ )
+
+ constructor(
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ ) : this(
+ left = left.toShort(),
+ top = top.toShort(),
+ right = right.toShort(),
+ bottom = bottom.toShort(),
+ )
+
+ constructor(
+ left: Short,
+ top: Short,
+ right: Short,
+ bottom: Short,
+ ) : this(pack(left, top, right, bottom))
+
+ val center: VPoint
+ get() = VPoint(left, top) + size / 2
+
+ val size: VPoint
+ get() = VPoint(width, height)
+
+ override fun toString() = "($left, $top) -> ($right, $bottom)"
+
+ companion object {
+ val ZERO = VRect(0, 0, 0, 0)
+
+ fun fromCenter(center: VPoint, size: VPoint): VRect {
+ return VRect(
+ (center.x - size.x / 2).toShort(),
+ (center.y - size.y / 2).toShort(),
+ (center.x + size.x / 2).toShort(),
+ (center.y + size.y / 2).toShort(),
+ )
+ }
+
+ fun fromTopLeft(pos: VPoint, size: VPoint): VRect {
+ return VRect(
+ pos.x.toShort(),
+ pos.y.toShort(),
+ (pos.x + size.x).toShort(),
+ (pos.y + size.y).toShort(),
+ )
+ }
+ }
+}
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 be0362fd7481..ac7a85742385 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,15 +16,13 @@
package com.android.systemui.plugins.qs
-/**
- * The base view model class for rendering the Tile's TileDetailsView.
- */
-abstract class TileDetailsViewModel {
+/** The view model interface for rendering the Tile's TileDetailsView. */
+interface TileDetailsViewModel {
// The callback when the settings button is clicked. Currently this is the same as the on tile
// long press callback
- abstract fun clickOnSettingsButton()
+ fun clickOnSettingsButton()
- abstract fun getTitle(): String
+ val title: String
- abstract fun getSubTitle(): String
+ val subTitle: String
}
diff --git a/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml b/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml
new file mode 100644
index 000000000000..d398f60ddc3c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/notification_2025_smart_reply_button_background.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/notification_ripple_untinted_color">
+ <item>
+ <inset
+ android:insetLeft="0dp"
+ android:insetTop="8dp"
+ android:insetRight="0dp"
+ android:insetBottom="8dp">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/notification_2025_smart_reply_button_corner_radius" />
+ <stroke android:width="@dimen/smart_reply_button_stroke_width"
+ android:color="@color/smart_reply_button_stroke" />
+ <solid android:color="@color/smart_reply_button_background"/>
+ </shape>
+ </inset>
+ </item>
+</ripple>
diff --git a/packages/SystemUI/res/layout/clipboard_overlay.xml b/packages/SystemUI/res/layout/clipboard_overlay.xml
index 915563b1ae20..c7add163dffa 100644
--- a/packages/SystemUI/res/layout/clipboard_overlay.xml
+++ b/packages/SystemUI/res/layout/clipboard_overlay.xml
@@ -85,6 +85,7 @@
android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
android:elevation="4dp"
android:scrollbars="none"
+ android:importantForAccessibility="no"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintWidth_percent="1.0"
app:layout_constraintWidth_max="wrap"
@@ -176,6 +177,8 @@
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="4dp"
android:layout_marginBottom="2dp"
+ android:importantForAccessibility="yes"
+ android:contentDescription="@string/clipboard_overlay_window_name"
android:background="@drawable/clipboard_minimized_background_inset">
<ImageView
android:src="@drawable/ic_content_paste"
diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml
index 6f8b4cd3e4a9..9b629ace76af 100644
--- a/packages/SystemUI/res/layout/media_output_dialog.xml
+++ b/packages/SystemUI/res/layout/media_output_dialog.xml
@@ -15,8 +15,8 @@
~ limitations under the License.
-->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/media_output_dialog"
android:layout_width="@dimen/large_dialog_width"
android:layout_height="wrap_content"
@@ -35,24 +35,25 @@
android:orientation="horizontal">
<ImageView
android:id="@+id/header_icon"
- android:layout_width="72dp"
- android:layout_height="72dp"
+ android:layout_width="@dimen/media_output_dialog_header_album_icon_size"
+ android:layout_height="@dimen/media_output_dialog_header_album_icon_size"
+ android:layout_marginEnd="@dimen/media_output_dialog_header_icon_padding"
android:importantForAccessibility="no"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingStart="12dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:gravity="center_vertical"
+ android:gravity="top"
android:orientation="horizontal">
<ImageView
android:id="@+id/app_source_icon"
android:layout_width="20dp"
android:layout_height="20dp"
+ android:layout_marginBottom="7dp"
android:gravity="center_vertical"
android:importantForAccessibility="no"/>
@@ -103,12 +104,11 @@
android:layout_height="wrap_content" >
</ViewStub>
- <LinearLayout
+ <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/device_list"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:orientation="vertical">
+ android:layout_height="0dp"
+ android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_result"
@@ -116,8 +116,11 @@
android:paddingTop="8dp"
android:clipToPadding="false"
android:layout_width="match_parent"
- android:layout_height="wrap_content"/>
- </LinearLayout>
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintHeight_max="@dimen/media_output_dialog_list_max_height"/>
+ </androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
diff --git a/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml b/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml
new file mode 100644
index 000000000000..ed905885a76f
--- /dev/null
+++ b/packages/SystemUI/res/layout/notification_2025_smart_action_button.xml
@@ -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
+ -->
+
+<!-- android:paddingHorizontal is set dynamically in SmartReplyView. -->
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@android:style/Widget.Material.Button"
+ android:stateListAnimator="@null"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:minWidth="0dp"
+ android:minHeight="@dimen/notification_2025_smart_reply_button_min_height"
+ android:paddingVertical="@dimen/smart_reply_button_padding_vertical"
+ android:background="@drawable/notification_2025_smart_reply_button_background"
+ android:gravity="center"
+ android:fontFamily="google-sans-flex"
+ android:textSize="@dimen/smart_reply_button_font_size"
+ android:textColor="@color/smart_reply_button_text"
+ android:paddingStart="@dimen/smart_reply_button_action_padding_left"
+ android:paddingEnd="@dimen/smart_reply_button_padding_horizontal"
+ android:drawablePadding="@dimen/smart_action_button_icon_padding"
+ android:textStyle="normal"
+ android:ellipsize="none"/>
diff --git a/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml b/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml
new file mode 100644
index 000000000000..4f543e5099bf
--- /dev/null
+++ b/packages/SystemUI/res/layout/notification_2025_smart_reply_button.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2025 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<!-- android:paddingHorizontal is set dynamically in SmartReplyView. -->
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@android:style/Widget.Material.Button"
+ android:stateListAnimator="@null"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:minWidth="0dp"
+ android:minHeight="@dimen/notification_2025_smart_reply_button_min_height"
+ android:paddingVertical="@dimen/smart_reply_button_padding_vertical"
+ android:background="@drawable/notification_2025_smart_reply_button_background"
+ android:gravity="center"
+ android:fontFamily="google-sans-flex"
+ android:textSize="@dimen/smart_reply_button_font_size"
+ android:textColor="@color/smart_reply_button_text"
+ android:paddingStart="@dimen/smart_reply_button_padding_horizontal"
+ android:paddingEnd="@dimen/smart_reply_button_padding_horizontal"
+ android:textStyle="normal"
+ android:ellipsize="none"/>
diff --git a/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json
index b1d6a270bc67..3f03fcff7603 100644
--- a/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json
+++ b/packages/SystemUI/res/raw/fingerprint_dialogue_unlocked_to_checkmark_success_lottie.json
@@ -1 +1 @@
-{"v":"5.7.13","fr":60,"ip":0,"op":55,"w":80,"h":80,"nm":"unlocked_to_checkmark_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.143,32,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.761,0],[0,-2.7],[0,0]],"o":[[0,0],[0,-2.7],[-2.761,0],[0,0],[0,0]],"v":[[5,5],[5,-0.111],[0,-5],[-5,-0.111],[-5.01,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".colorAccentPrimary","cl":"colorAccentPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[37.999,44.999,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.42,0],[0,1.42],[1.42,0],[0,-1.42]],"o":[[1.42,0],[0,-1.42],[-1.42,0],[0,1.42]],"v":[[0,2.571],[2.571,0],[0,-2.571],[-2.571,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.5,40.75,0],"ix":2,"l":2},"a":{"a":0,"k":[12.5,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[112,112,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":77,"st":10,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".grey700","cl":"grey700","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]} \ No newline at end of file
+{"v":"5.7.13","fr":60,"ip":0,"op":55,"w":80,"h":80,"nm":"unlocked_to_checkmark_success","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.143,32,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[2.761,0],[0,-2.7],[0,0]],"o":[[0,0],[0,-2.7],[-2.761,0],[0,0],[0,0]],"v":[[5,5],[5,-0.111],[0,-5],[-5,-0.111],[-5.01,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[38,45,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[18,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":4},"w":{"a":0,"k":2.5,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onPrimary","cl":"onPrimary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[37.999,44.999,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.42,0],[0,1.42],[1.42,0],[0,-1.42]],"o":[[1.42,0],[0,-1.42],[-1.42,0],[0,1.42]],"v":[[0,2.571],[2.571,0],[0,-2.571],[-2.571,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.827450990677,0.890196084976,0.992156863213,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":10,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Vector","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":85,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40.5,40.75,0],"ix":2,"l":2},"a":{"a":0,"k":[12.5,-6.25,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[60,60,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":19,"s":[112,112,100]},{"t":30,"s":[100,100,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-10.556,-9.889],[7.444,6.555],[34.597,-20.486]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.7],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":10,"op":910,"st":10,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":".green200","cl":"green200","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[93.5,93.5,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.658823529412,0.854901960784,0.709803921569,1],"ix":3},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":10,"s":[0]},{"t":15,"s":[100]}],"ix":4},"w":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":10,"s":[0]},{"t":20,"s":[4]}],"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":10,"op":77,"st":10,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":".primary","cl":"primary","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[40,40,0],"ix":2,"l":2},"a":{"a":0,"k":[40.25,40.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[22.12,0],[0,-22.08],[-22.08,0],[0,22.08]],"o":[[-22.08,0],[0,22.08],[22.12,0],[0,-22.08]],"v":[[-0.04,-40],[-40,0],[-0.04,40],[40,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.278431385756,0.278431385756,0.278431385756,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":20,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[40.25,40.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]} \ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index d547bd0d05f3..d0ae307b6919 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1157,6 +1157,8 @@
<dimen name="smart_action_button_icon_size">18dp</dimen>
<dimen name="smart_action_button_icon_padding">8dp</dimen>
<dimen name="smart_action_button_outline_stroke_width">2dp</dimen>
+ <dimen name="notification_2025_smart_reply_button_corner_radius">18dp</dimen>
+ <dimen name="notification_2025_smart_reply_button_min_height">48dp</dimen>
<!-- Magic Action params. -->
<!-- Corner radius = half of min_height to create rounded sides. -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index c06c17a0844f..8c1fd65d96d4 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2357,8 +2357,8 @@
<string name="system_multitasking_lhs">Use split screen with app on the left</string>
<!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] -->
<string name="system_multitasking_full_screen">Use full screen</string>
- <!-- User visible title for the keyboard shortcut that switches to desktop view [CHAR LIMIT=70] -->
- <string name="system_multitasking_desktop_view">Use desktop view</string>
+ <!-- User visible title for the keyboard shortcut that switches to desktop windowing [CHAR LIMIT=70] -->
+ <string name="system_multitasking_desktop_view">Use desktop windowing</string>
<!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] -->
<string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string>
<!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index 1549b699eee6..763b1072f968 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -21,7 +21,6 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Resources
-import android.graphics.RectF
import android.os.Trace
import android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
import android.provider.Settings.Global.ZEN_MODE_OFF
@@ -60,6 +59,7 @@ import com.android.systemui.plugins.clocks.ClockEventListener
import com.android.systemui.plugins.clocks.ClockFaceController
import com.android.systemui.plugins.clocks.ClockMessageBuffers
import com.android.systemui.plugins.clocks.ClockTickRate
+import com.android.systemui.plugins.clocks.VRectF
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import com.android.systemui.plugins.clocks.ZenData.ZenMode
@@ -250,7 +250,7 @@ constructor(
private var largeClockOnSecondaryDisplay = false
val dozeAmount = MutableStateFlow(0f)
- val onClockBoundsChanged = MutableStateFlow<RectF?>(null)
+ val onClockBoundsChanged = MutableStateFlow<VRectF>(VRectF.ZERO)
private fun isDarkTheme(): Boolean {
val isLightTheme = TypedValue()
@@ -315,7 +315,7 @@ constructor(
private val clockListener =
object : ClockEventListener {
- override fun onBoundsChanged(bounds: RectF) {
+ override fun onBoundsChanged(bounds: VRectF) {
onClockBoundsChanged.value = bounds
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index ec97b8a96c1f..b8726101602c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -21,6 +21,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.ColorStateList;
import android.content.res.Resources;
+import android.hardware.input.InputManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
@@ -219,6 +220,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView>
private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor;
private final BouncerHapticPlayer mBouncerHapticPlayer;
private final UserActivityNotifier mUserActivityNotifier;
+ private final InputManager mInputManager;
@Inject
public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -235,7 +237,8 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView>
UiEventLogger uiEventLogger,
KeyguardKeyboardInteractor keyguardKeyboardInteractor,
BouncerHapticPlayer bouncerHapticPlayer,
- UserActivityNotifier userActivityNotifier) {
+ UserActivityNotifier userActivityNotifier,
+ InputManager inputManager) {
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mLockPatternUtils = lockPatternUtils;
mLatencyTracker = latencyTracker;
@@ -254,6 +257,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView>
mKeyguardKeyboardInteractor = keyguardKeyboardInteractor;
mBouncerHapticPlayer = bouncerHapticPlayer;
mUserActivityNotifier = userActivityNotifier;
+ mInputManager = inputManager;
}
/** Create a new {@link KeyguardInputViewController}. */
@@ -285,22 +289,23 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView>
emergencyButtonController, mFalsingCollector,
mDevicePostureController, mFeatureFlags, mSelectedUserInteractor,
mUiEventLogger, mKeyguardKeyboardInteractor, mBouncerHapticPlayer,
- mUserActivityNotifier);
+ mUserActivityNotifier, mInputManager);
} else if (keyguardInputView instanceof KeyguardSimPinView) {
return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mTelephonyManager, mFalsingCollector,
emergencyButtonController, mFeatureFlags, mSelectedUserInteractor,
- mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier);
+ mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier,
+ mInputManager);
} else if (keyguardInputView instanceof KeyguardSimPukView) {
return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView,
mKeyguardUpdateMonitor, securityMode, mLockPatternUtils,
keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker,
mTelephonyManager, mFalsingCollector,
emergencyButtonController, mFeatureFlags, mSelectedUserInteractor,
- mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier
- );
+ mKeyguardKeyboardInteractor, mBouncerHapticPlayer, mUserActivityNotifier,
+ mInputManager);
}
throw new RuntimeException("Unable to find controller for " + keyguardInputView);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
index 0e9d8fec9717..ec9aedfc7551 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -18,12 +18,14 @@ package com.android.keyguard;
import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
+import static com.android.internal.widget.flags.Flags.hideLastCharWithPhysicalInput;
import static com.android.systemui.Flags.pinInputFieldStyledFocusState;
import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;
+import android.hardware.input.InputManager;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -43,11 +45,13 @@ import com.android.systemui.res.R;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinBasedInputView>
- extends KeyguardAbsKeyInputViewController<T> {
+ extends KeyguardAbsKeyInputViewController<T> implements InputManager.InputDeviceListener {
private final FalsingCollector mFalsingCollector;
private final KeyguardKeyboardInteractor mKeyguardKeyboardInteractor;
protected PasswordTextView mPasswordEntry;
+ private Boolean mShowAnimations;
+ private InputManager mInputManager;
private final OnKeyListener mOnKeyListener = (v, keyCode, event) -> {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
@@ -79,7 +83,8 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB
SelectedUserInteractor selectedUserInteractor,
KeyguardKeyboardInteractor keyguardKeyboardInteractor,
BouncerHapticPlayer bouncerHapticPlayer,
- UserActivityNotifier userActivityNotifier) {
+ UserActivityNotifier userActivityNotifier,
+ InputManager inputManager) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker, falsingCollector,
emergencyButtonController, featureFlags, selectedUserInteractor,
@@ -87,6 +92,51 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB
mFalsingCollector = falsingCollector;
mKeyguardKeyboardInteractor = keyguardKeyboardInteractor;
mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId());
+ mInputManager = inputManager;
+ mShowAnimations = null;
+ }
+
+ private void updateAnimations(Boolean showAnimations) {
+ if (!hideLastCharWithPhysicalInput()) return;
+
+ if (showAnimations == null) {
+ showAnimations = !mLockPatternUtils
+ .isPinEnhancedPrivacyEnabled(mSelectedUserInteractor.getSelectedUserId());
+ }
+ if (mShowAnimations != null && showAnimations.equals(mShowAnimations)) return;
+ mShowAnimations = showAnimations;
+
+ for (NumPadKey button : mView.getButtons()) {
+ button.setAnimationEnabled(mShowAnimations);
+ }
+ mPasswordEntry.setShowPassword(mShowAnimations);
+ }
+
+ @Override
+ public void onInputDeviceAdded(int deviceId) {
+ if (!hideLastCharWithPhysicalInput()) return;
+
+ // If we were showing animations before maybe the new device is a keyboard.
+ if (mShowAnimations) {
+ updateAnimations(null);
+ }
+ }
+
+ @Override
+ public void onInputDeviceRemoved(int deviceId) {
+ if (!hideLastCharWithPhysicalInput()) return;
+
+ // If we were hiding animations because of a keyboard the keyboard may have been unplugged.
+ if (!mShowAnimations) {
+ updateAnimations(null);
+ }
+ }
+
+ @Override
+ public void onInputDeviceChanged(int deviceId) {
+ if (!hideLastCharWithPhysicalInput()) return;
+
+ updateAnimations(null);
}
@Override
@@ -95,7 +145,13 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB
boolean showAnimations = !mLockPatternUtils
.isPinEnhancedPrivacyEnabled(mSelectedUserInteractor.getSelectedUserId());
- mPasswordEntry.setShowPassword(showAnimations);
+ if (hideLastCharWithPhysicalInput()) {
+ mInputManager.registerInputDeviceListener(this, null);
+ updateAnimations(showAnimations);
+ } else {
+ mPasswordEntry.setShowPassword(showAnimations);
+ }
+
for (NumPadKey button : mView.getButtons()) {
button.setOnTouchListener((v, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
@@ -103,7 +159,9 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB
}
return false;
});
- button.setAnimationEnabled(showAnimations);
+ if (!hideLastCharWithPhysicalInput()) {
+ button.setAnimationEnabled(showAnimations);
+ }
button.setBouncerHapticHelper(mBouncerHapticPlayer);
}
mPasswordEntry.setOnKeyListener(mOnKeyListener);
@@ -191,6 +249,10 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB
protected void onViewDetached() {
super.onViewDetached();
+ if (hideLastCharWithPhysicalInput()) {
+ mInputManager.unregisterInputDeviceListener(this);
+ }
+
for (NumPadKey button : mView.getButtons()) {
button.setOnTouchListener(null);
button.setBouncerHapticHelper(null);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
index 9ae4cc6a4b4f..eefcab38ecd3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -18,6 +18,7 @@ package com.android.keyguard;
import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE;
+import android.hardware.input.InputManager;
import android.view.View;
import com.android.internal.logging.UiEvent;
@@ -63,11 +64,13 @@ public class KeyguardPinViewController
SelectedUserInteractor selectedUserInteractor, UiEventLogger uiEventLogger,
KeyguardKeyboardInteractor keyguardKeyboardInteractor,
BouncerHapticPlayer bouncerHapticPlayer,
- UserActivityNotifier userActivityNotifier) {
+ UserActivityNotifier userActivityNotifier,
+ InputManager inputManager) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker,
emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
- keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier);
+ keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier, inputManager
+ );
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mPostureController = postureController;
mLockPatternUtils = lockPatternUtils;
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
index 24f77d77dbe1..a5bb62c04d00 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java
@@ -29,6 +29,7 @@ import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
+import android.hardware.input.InputManager;
import android.telephony.PinResult;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
@@ -97,11 +98,13 @@ public class KeyguardSimPinViewController
SelectedUserInteractor selectedUserInteractor,
KeyguardKeyboardInteractor keyguardKeyboardInteractor,
BouncerHapticPlayer bouncerHapticPlayer,
- UserActivityNotifier userActivityNotifier) {
+ UserActivityNotifier userActivityNotifier,
+ InputManager inputManager) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker,
emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
- keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier);
+ keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier, inputManager
+ );
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mTelephonyManager = telephonyManager;
mSimImageView = mView.findViewById(R.id.keyguard_sim);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
index e17e8cc05f7e..adede3dc058d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
@@ -25,6 +25,7 @@ import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
+import android.hardware.input.InputManager;
import android.telephony.PinResult;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
@@ -95,11 +96,13 @@ public class KeyguardSimPukViewController
SelectedUserInteractor selectedUserInteractor,
KeyguardKeyboardInteractor keyguardKeyboardInteractor,
BouncerHapticPlayer bouncerHapticPlayer,
- UserActivityNotifier userActivityNotifier) {
+ UserActivityNotifier userActivityNotifier,
+ InputManager inputManager) {
super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback,
messageAreaControllerFactory, latencyTracker,
emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor,
- keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier);
+ keyguardKeyboardInteractor, bouncerHapticPlayer, userActivityNotifier, inputManager
+ );
mKeyguardUpdateMonitor = keyguardUpdateMonitor;
mTelephonyManager = telephonyManager;
mSimImageView = mView.findViewById(R.id.keyguard_sim);
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 19caead525c9..fb3bc620ee68 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -71,6 +71,7 @@ import com.android.systemui.bluetooth.qsdialog.SavedHearingDeviceItemFactory;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.qs.shared.QSSettingsPackageRepository;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -111,6 +112,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
private final HearingDevicesUiEventLogger mUiEventLogger;
private final boolean mShowPairNewDevice;
private final int mLaunchSourceId;
+ private final QSSettingsPackageRepository mQSSettingsPackageRepository;
private SystemUIDialog mDialog;
private HearingDevicesListAdapter mDeviceListAdapter;
@@ -166,7 +168,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
@Main Executor mainExecutor,
@Background Executor bgExecutor,
AudioManager audioManager,
- HearingDevicesUiEventLogger uiEventLogger) {
+ HearingDevicesUiEventLogger uiEventLogger,
+ QSSettingsPackageRepository qsSettingsPackageRepository) {
mShowPairNewDevice = showPairNewDevice;
mSystemUIDialogFactory = systemUIDialogFactory;
mActivityStarter = activityStarter;
@@ -178,6 +181,7 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
mProfileManager = localBluetoothManager.getProfileManager();
mUiEventLogger = uiEventLogger;
mLaunchSourceId = launchSourceId;
+ mQSSettingsPackageRepository = qsSettingsPackageRepository;
}
@Override
@@ -193,11 +197,11 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
public void onDeviceItemGearClicked(@NonNull DeviceItem deviceItem, @NonNull View view) {
mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_GEAR_CLICK, mLaunchSourceId);
dismissDialogIfExists();
- Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS);
Bundle bundle = new Bundle();
bundle.putString(KEY_BLUETOOTH_ADDRESS, deviceItem.getCachedBluetoothDevice().getAddress());
- intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ Intent intent = new Intent(ACTION_BLUETOOTH_DEVICE_DETAILS)
+ .setPackage(mQSSettingsPackageRepository.getSettingsPackageName())
+ .putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0,
mDialogTransitionAnimator.createActivityTransitionController(view));
}
@@ -396,8 +400,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
pairButton.setOnClickListener(v -> {
mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_PAIR, mLaunchSourceId);
dismissDialogIfExists();
- final Intent intent = new Intent(Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ final Intent intent = new Intent(Settings.ACTION_HEARING_DEVICE_PAIRING_SETTINGS)
+ .setPackage(mQSSettingsPackageRepository.getSettingsPackageName());
mActivityStarter.postStartActivityDismissingKeyguard(intent, /* delay= */ 0,
mDialogTransitionAnimator.createActivityTransitionController(dialog));
});
@@ -518,8 +522,9 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate,
com.android.internal.R.color.materialColorOnPrimaryContainer));
}
text.setText(item.getToolName());
- Intent intent = item.getToolIntent();
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ Intent intent = item.getToolIntent()
+ .setPackage(mQSSettingsPackageRepository.getSettingsPackageName());
+
view.setOnClickListener(v -> {
final String name = intent.getComponent() != null
? intent.getComponent().flattenToString()
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 5863a9385234..7d8752ef7222 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt
@@ -21,18 +21,14 @@ import com.android.systemui.plugins.qs.TileDetailsViewModel
class BluetoothDetailsViewModel(
private val onSettingsClick: () -> Unit,
val detailsContentViewModel: BluetoothDetailsContentViewModel,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
override fun clickOnSettingsButton() {
onSettingsClick()
}
- override fun getTitle(): String {
- // TODO: b/378513956 Update the placeholder text
- return "Bluetooth"
- }
+ // TODO: b/378513956 Update the placeholder text
+ override val title = "Bluetooth"
- override fun getSubTitle(): String {
- // TODO: b/378513956 Update the placeholder text
- return "Tap to connect or disconnect a device"
- }
+ // TODO: b/378513956 Update the placeholder text
+ override val subTitle = "Tap to connect or disconnect a device"
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt
index 42f1b738ec20..6c3535a42a6e 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingView.kt
@@ -27,17 +27,17 @@ import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import com.android.systemui.Flags.doubleTapToSleep
import com.android.systemui.log.TouchHandlingViewLogger
import com.android.systemui.shade.TouchLogger
-import kotlin.math.pow
-import kotlin.math.sqrt
import kotlinx.coroutines.DisposableHandle
/**
- * View designed to handle long-presses.
+ * View designed to handle long-presses and double taps.
*
- * The view will not handle any long pressed by default. To set it up, set up a listener and, when
- * ready to start consuming long-presses, set [setLongPressHandlingEnabled] to `true`.
+ * The view will not handle any gestures by default. To set it up, set up a listener and, when ready
+ * to start consuming gestures, set the gesture's enable function ([setLongPressHandlingEnabled],
+ * [setDoublePressHandlingEnabled]) to `true`.
*/
class TouchHandlingView(
context: Context,
@@ -62,6 +62,9 @@ class TouchHandlingView(
/** Notifies that the gesture was too short for a long press, it is actually a click. */
fun onSingleTapDetected(view: View, x: Int, y: Int) = Unit
+
+ /** Notifies that a double tap has been detected by the given view. */
+ fun onDoubleTapDetected(view: View) = Unit
}
var listener: Listener? = null
@@ -70,6 +73,7 @@ class TouchHandlingView(
private val interactionHandler: TouchHandlingViewInteractionHandler by lazy {
TouchHandlingViewInteractionHandler(
+ context = context,
postDelayed = { block, timeoutMs ->
val dispatchToken = Any()
@@ -84,6 +88,9 @@ class TouchHandlingView(
onSingleTapDetected = { x, y ->
listener?.onSingleTapDetected(this@TouchHandlingView, x = x, y = y)
},
+ onDoubleTapDetected = {
+ if (doubleTapToSleep()) listener?.onDoubleTapDetected(this@TouchHandlingView)
+ },
longPressDuration = longPressDuration,
allowedTouchSlop = allowedTouchSlop,
logger = logger,
@@ -100,13 +107,17 @@ class TouchHandlingView(
interactionHandler.isLongPressHandlingEnabled = isEnabled
}
+ fun setDoublePressHandlingEnabled(isEnabled: Boolean) {
+ interactionHandler.isDoubleTapHandlingEnabled = isEnabled
+ }
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
return TouchLogger.logDispatchTouch("long_press", event, super.dispatchTouchEvent(event))
}
@SuppressLint("ClickableViewAccessibility")
- override fun onTouchEvent(event: MotionEvent?): Boolean {
- return interactionHandler.onTouchEvent(event?.toModel())
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ return interactionHandler.onTouchEvent(event)
}
private fun setupAccessibilityDelegate() {
@@ -154,33 +165,3 @@ class TouchHandlingView(
}
}
}
-
-private fun MotionEvent.toModel(): TouchHandlingViewInteractionHandler.MotionEventModel {
- return when (actionMasked) {
- MotionEvent.ACTION_DOWN ->
- TouchHandlingViewInteractionHandler.MotionEventModel.Down(x = x.toInt(), y = y.toInt())
- MotionEvent.ACTION_MOVE ->
- TouchHandlingViewInteractionHandler.MotionEventModel.Move(
- distanceMoved = distanceMoved()
- )
- MotionEvent.ACTION_UP ->
- TouchHandlingViewInteractionHandler.MotionEventModel.Up(
- distanceMoved = distanceMoved(),
- gestureDuration = gestureDuration(),
- )
- MotionEvent.ACTION_CANCEL -> TouchHandlingViewInteractionHandler.MotionEventModel.Cancel
- else -> TouchHandlingViewInteractionHandler.MotionEventModel.Other
- }
-}
-
-private fun MotionEvent.distanceMoved(): Float {
- return if (historySize > 0) {
- sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2))
- } else {
- 0f
- }
-}
-
-private fun MotionEvent.gestureDuration(): Long {
- return eventTime - downTime
-}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt
index 5863fc644c8e..fe509d74edc0 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/TouchHandlingViewInteractionHandler.kt
@@ -17,12 +17,20 @@
package com.android.systemui.common.ui.view
+import android.content.Context
import android.graphics.Point
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.ViewConfiguration
import com.android.systemui.log.TouchHandlingViewLogger
+import kotlin.math.pow
+import kotlin.math.sqrt
+import kotlin.properties.Delegates
import kotlinx.coroutines.DisposableHandle
/** Encapsulates logic to handle complex touch interactions with a [TouchHandlingView]. */
class TouchHandlingViewInteractionHandler(
+ context: Context,
/**
* Callback to run the given [Runnable] with the given delay, returning a [DisposableHandle]
* allowing the delayed runnable to be canceled before it is run.
@@ -34,6 +42,8 @@ class TouchHandlingViewInteractionHandler(
private val onLongPressDetected: (x: Int, y: Int) -> Unit,
/** Callback reporting the a single tap gesture was detected at the given coordinates. */
private val onSingleTapDetected: (x: Int, y: Int) -> Unit,
+ /** Callback reporting that a double tap gesture was detected. */
+ private val onDoubleTapDetected: () -> Unit,
/** Time for the touch to be considered a long-press in ms */
var longPressDuration: () -> Long,
/**
@@ -58,48 +68,98 @@ class TouchHandlingViewInteractionHandler(
}
var isLongPressHandlingEnabled: Boolean = false
+ var isDoubleTapHandlingEnabled: Boolean = false
var scheduledLongPressHandle: DisposableHandle? = null
+ private var doubleTapAwaitingUp: Boolean = false
+ private var lastDoubleTapDownEventTime: Long? = null
+
/** Record coordinate for last DOWN event for single tap */
val lastEventDownCoordinate = Point(-1, -1)
- fun onTouchEvent(event: MotionEventModel?): Boolean {
- if (!isLongPressHandlingEnabled) {
- return false
- }
- return when (event) {
- is MotionEventModel.Down -> {
- scheduleLongPress(event.x, event.y)
- lastEventDownCoordinate.x = event.x
- lastEventDownCoordinate.y = event.y
- true
+ private val gestureDetector =
+ GestureDetector(
+ context,
+ object : GestureDetector.SimpleOnGestureListener() {
+ override fun onDoubleTap(event: MotionEvent): Boolean {
+ if (isDoubleTapHandlingEnabled) {
+ doubleTapAwaitingUp = true
+ lastDoubleTapDownEventTime = event.eventTime
+ return true
+ }
+ return false
+ }
+ },
+ )
+
+ fun onTouchEvent(event: MotionEvent): Boolean {
+ if (isDoubleTapHandlingEnabled) {
+ gestureDetector.onTouchEvent(event)
+ if (event.actionMasked == MotionEvent.ACTION_UP && doubleTapAwaitingUp) {
+ lastDoubleTapDownEventTime?.let { time ->
+ if (
+ event.eventTime - time < ViewConfiguration.getDoubleTapTimeout()
+ ) {
+ cancelScheduledLongPress()
+ onDoubleTapDetected()
+ }
+ }
+ doubleTapAwaitingUp = false
+ } else if (event.actionMasked == MotionEvent.ACTION_CANCEL && doubleTapAwaitingUp) {
+ doubleTapAwaitingUp = false
}
- is MotionEventModel.Move -> {
- if (event.distanceMoved > allowedTouchSlop) {
- logger?.cancelingLongPressDueToTouchSlop(event.distanceMoved, allowedTouchSlop)
+ }
+
+ if (isLongPressHandlingEnabled) {
+ val motionEventModel = event.toModel()
+
+ return when (motionEventModel) {
+ is MotionEventModel.Down -> {
+ scheduleLongPress(motionEventModel.x, motionEventModel.y)
+ lastEventDownCoordinate.x = motionEventModel.x
+ lastEventDownCoordinate.y = motionEventModel.y
+ true
+ }
+
+ is MotionEventModel.Move -> {
+ if (motionEventModel.distanceMoved > allowedTouchSlop) {
+ logger?.cancelingLongPressDueToTouchSlop(
+ motionEventModel.distanceMoved,
+ allowedTouchSlop,
+ )
+ cancelScheduledLongPress()
+ }
+ false
+ }
+
+ is MotionEventModel.Up -> {
+ logger?.onUpEvent(
+ motionEventModel.distanceMoved,
+ allowedTouchSlop,
+ motionEventModel.gestureDuration,
+ )
cancelScheduledLongPress()
+ if (
+ motionEventModel.distanceMoved <= allowedTouchSlop &&
+ motionEventModel.gestureDuration < longPressDuration()
+ ) {
+ logger?.dispatchingSingleTap()
+ dispatchSingleTap(lastEventDownCoordinate.x, lastEventDownCoordinate.y)
+ }
+ false
}
- false
- }
- is MotionEventModel.Up -> {
- logger?.onUpEvent(event.distanceMoved, allowedTouchSlop, event.gestureDuration)
- cancelScheduledLongPress()
- if (
- event.distanceMoved <= allowedTouchSlop &&
- event.gestureDuration < longPressDuration()
- ) {
- logger?.dispatchingSingleTap()
- dispatchSingleTap(lastEventDownCoordinate.x, lastEventDownCoordinate.y)
+
+ is MotionEventModel.Cancel -> {
+ logger?.motionEventCancelled()
+ cancelScheduledLongPress()
+ false
}
- false
- }
- is MotionEventModel.Cancel -> {
- logger?.motionEventCancelled()
- cancelScheduledLongPress()
- false
+
+ else -> false
}
- else -> false
}
+
+ return false
}
private fun scheduleLongPress(x: Int, y: Int) {
@@ -134,4 +194,30 @@ class TouchHandlingViewInteractionHandler(
onSingleTapDetected(x, y)
}
+
+ private fun MotionEvent.toModel(): MotionEventModel {
+ return when (actionMasked) {
+ MotionEvent.ACTION_DOWN -> MotionEventModel.Down(x = x.toInt(), y = y.toInt())
+ MotionEvent.ACTION_MOVE -> MotionEventModel.Move(distanceMoved = distanceMoved())
+ MotionEvent.ACTION_UP ->
+ MotionEventModel.Up(
+ distanceMoved = distanceMoved(),
+ gestureDuration = gestureDuration(),
+ )
+ MotionEvent.ACTION_CANCEL -> MotionEventModel.Cancel
+ else -> MotionEventModel.Other
+ }
+ }
+
+ private fun MotionEvent.distanceMoved(): Float {
+ return if (historySize > 0) {
+ sqrt((x - getHistoricalX(0)).pow(2) + (y - getHistoricalY(0)).pow(2))
+ } else {
+ 0f
+ }
+ }
+
+ private fun MotionEvent.gestureDuration(): Long {
+ return eventTime - downTime
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt
index 083191c8ecde..a56710ee3772 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/FakePerDisplayRepository.kt
@@ -32,9 +32,6 @@ class FakePerDisplayRepository<T> : PerDisplayRepository<T> {
return instances[displayId]
}
- override val displayIds: Set<Int>
- get() = instances.keys
-
override val debugName: String
get() = "FakePerDisplayRepository"
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt
index d27e33e53dbb..d1d013542fbf 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayRepository.kt
@@ -86,9 +86,6 @@ interface PerDisplayRepository<T> {
/** Gets the cached instance or create a new one for a given display. */
operator fun get(displayId: Int): T?
- /** List of display ids for which this repository has an instance. */
- val displayIds: Set<Int>
-
/** Debug name for this repository, mainly for tracing and logging. */
val debugName: String
}
@@ -122,9 +119,6 @@ constructor(
backgroundApplicationScope.launch("$debugName#start") { start() }
}
- override val displayIds: Set<Int>
- get() = perDisplayInstances.keys
-
private suspend fun start() {
dumpManager.registerNormalDumpable("PerDisplayRepository-${debugName}", this)
displayRepository.displayIds.collectLatest { displayIds ->
@@ -199,8 +193,6 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>(
private val lazyDefaultDisplayInstance by lazy {
instanceProvider.createInstance(Display.DEFAULT_DISPLAY)
}
- override val displayIds: Set<Int> = setOf(Display.DEFAULT_DISPLAY)
-
override fun get(displayId: Int): T? = lazyDefaultDisplayInstance
}
@@ -214,7 +206,5 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>(
*/
class SingleInstanceRepositoryImpl<T>(override val debugName: String, private val instance: T) :
PerDisplayRepository<T> {
- override val displayIds: Set<Int> = setOf(Display.DEFAULT_DISPLAY)
-
override fun get(displayId: Int): T? = instance
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
index 705eaa22aa9a..55534c4f1444 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTouchHandlingInteractor.kt
@@ -20,11 +20,14 @@ package com.android.systemui.keyguard.domain.interactor
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.os.PowerManager
+import android.provider.Settings
import android.view.accessibility.AccessibilityManager
import androidx.annotation.VisibleForTesting
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.internal.logging.UiEvent
import com.android.internal.logging.UiEventLogger
+import com.android.systemui.Flags.doubleTapToSleep
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -36,10 +39,13 @@ import com.android.systemui.res.R
import com.android.systemui.shade.PulsingGestureListener
import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
+import com.android.systemui.util.settings.repository.UserAwareSecureSettingsRepository
+import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -66,10 +72,13 @@ constructor(
private val accessibilityManager: AccessibilityManagerWrapper,
private val pulsingGestureListener: PulsingGestureListener,
private val faceAuthInteractor: DeviceEntryFaceAuthInteractor,
+ private val secureSettingsRepository: UserAwareSecureSettingsRepository,
+ private val powerManager: PowerManager,
+ private val systemClock: SystemClock,
) {
/** Whether the long-press handling feature should be enabled. */
val isLongPressHandlingEnabled: StateFlow<Boolean> =
- if (isFeatureEnabled()) {
+ if (isLongPressFeatureEnabled()) {
combine(
transitionInteractor.isFinishedIn(KeyguardState.LOCKSCREEN),
repository.isQuickSettingsVisible,
@@ -85,6 +94,30 @@ constructor(
initialValue = false,
)
+ /** Whether the double tap handling handling feature should be enabled. */
+ val isDoubleTapHandlingEnabled: StateFlow<Boolean> =
+ if (isDoubleTapFeatureEnabled()) {
+ combine(
+ transitionInteractor.transitionValue(KeyguardState.LOCKSCREEN),
+ repository.isQuickSettingsVisible,
+ isDoubleTapSettingEnabled(),
+ ) {
+ isFullyTransitionedToLockScreen,
+ isQuickSettingsVisible,
+ isDoubleTapSettingEnabled ->
+ isFullyTransitionedToLockScreen == 1f &&
+ !isQuickSettingsVisible &&
+ isDoubleTapSettingEnabled
+ }
+ } else {
+ flowOf(false)
+ }
+ .stateIn(
+ scope = scope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false,
+ )
+
private val _isMenuVisible = MutableStateFlow(false)
/** Model for whether the menu should be shown. */
val isMenuVisible: StateFlow<Boolean> =
@@ -116,7 +149,7 @@ constructor(
private var delayedHideMenuJob: Job? = null
init {
- if (isFeatureEnabled()) {
+ if (isLongPressFeatureEnabled()) {
broadcastDispatcher
.broadcastFlow(IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
.onEach { hideMenu() }
@@ -175,17 +208,30 @@ constructor(
/** Notifies that the lockscreen has been double clicked. */
fun onDoubleClick() {
- pulsingGestureListener.onDoubleTapEvent()
+ if (isDoubleTapHandlingEnabled.value) {
+ powerManager.goToSleep(systemClock.uptimeMillis())
+ } else {
+ pulsingGestureListener.onDoubleTapEvent()
+ }
+ }
+
+ private fun isDoubleTapSettingEnabled(): Flow<Boolean> {
+ return secureSettingsRepository.boolSetting(Settings.Secure.DOUBLE_TAP_TO_SLEEP)
}
private fun showSettings() {
_shouldOpenSettings.value = true
}
- private fun isFeatureEnabled(): Boolean {
+ private fun isLongPressFeatureEnabled(): Boolean {
return context.resources.getBoolean(R.bool.long_press_keyguard_customize_lockscreen_enabled)
}
+ private fun isDoubleTapFeatureEnabled(): Boolean {
+ return doubleTapToSleep() &&
+ context.resources.getBoolean(com.android.internal.R.bool.config_supportDoubleTapSleep)
+ }
+
/** Updates application state to ask to show the menu. */
private fun showMenu() {
_isMenuVisible.value = true
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
index 17e14c3e83da..70a827d5e45b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt
@@ -48,7 +48,7 @@ object DeviceEntryIconViewBinder {
/**
* Updates UI for:
* - device entry containing view (parent view for the below views)
- * - long-press handling view (transparent, no UI)
+ * - touch handling view (transparent, no UI)
* - foreground icon view (lock/unlock/fingerprint)
* - background view (optional)
*/
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt
index 741b149f29da..92b9da6790d1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewSmartspaceViewBinder.kt
@@ -31,6 +31,37 @@ import com.android.systemui.plugins.clocks.ClockPreviewConfig
object KeyguardPreviewSmartspaceViewBinder {
@JvmStatic
+ fun bind(parentView: View, viewModel: KeyguardPreviewSmartspaceViewModel) {
+ if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ val largeDateView =
+ parentView.findViewById<View>(
+ com.android.systemui.shared.R.id.date_smartspace_view_large
+ )
+ val smallDateView =
+ parentView.findViewById<View>(com.android.systemui.shared.R.id.date_smartspace_view)
+ parentView.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch("$TAG#viewModel.selectedClockSize") {
+ viewModel.previewingClockSize.collect {
+ when (it) {
+ ClockSizeSetting.DYNAMIC -> {
+ smallDateView?.visibility = View.GONE
+ largeDateView?.visibility = View.VISIBLE
+ }
+
+ ClockSizeSetting.SMALL -> {
+ smallDateView?.visibility = View.VISIBLE
+ largeDateView?.visibility = View.GONE
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @JvmStatic
fun bind(
smartspace: View,
viewModel: KeyguardPreviewSmartspaceViewModel,
@@ -44,6 +75,7 @@ object KeyguardPreviewSmartspaceViewBinder {
when (it) {
ClockSizeSetting.DYNAMIC ->
viewModel.getLargeClockSmartspaceTopPadding(clockPreviewConfig)
+
ClockSizeSetting.SMALL ->
viewModel.getSmallClockSmartspaceTopPadding(clockPreviewConfig)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index e81d5354ec6e..5ef2d6fd3256 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -29,6 +29,7 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.clocks.VRectF
import com.android.systemui.res.R
import com.android.systemui.shared.R as sharedR
import kotlinx.coroutines.DisposableHandle
@@ -135,7 +136,7 @@ object KeyguardSmartspaceViewBinder {
}
}
- if (clockBounds == null) return@collect
+ if (clockBounds == VRectF.ZERO) return@collect
if (isLargeClock) {
val largeDateHeight =
keyguardRootView
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt
index 195413a80f4b..485e1ce5b2f7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardTouchViewBinder.kt
@@ -75,6 +75,13 @@ object KeyguardTouchViewBinder {
onSingleTap(x, y)
}
+
+ override fun onDoubleTapDetected(view: View) {
+ if (falsingManager.isFalseDoubleTap()) {
+ return
+ }
+ viewModel.onDoubleClick()
+ }
}
view.repeatWhenAttached {
@@ -90,9 +97,20 @@ object KeyguardTouchViewBinder {
}
}
}
+ launch("$TAG#viewModel.isDoubleTapHandlingEnabled") {
+ viewModel.isDoubleTapHandlingEnabled.collect { isEnabled ->
+ view.setDoublePressHandlingEnabled(isEnabled)
+ view.contentDescription =
+ if (isEnabled) {
+ view.resources.getString(R.string.accessibility_desc_lock_screen)
+ } else {
+ null
+ }
+ }
+ }
}
}
}
- private const val TAG = "KeyguardLongPressViewBinder"
+ private const val TAG = "KeyguardTouchViewBinder"
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 242926b3e1d1..d749e3c11378 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -68,6 +68,7 @@ import com.android.systemui.monet.ColorScheme
import com.android.systemui.monet.Style
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.plugins.clocks.ClockPreviewConfig
+import com.android.systemui.plugins.clocks.ContextExt.getId
import com.android.systemui.plugins.clocks.ThemeConfig
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.res.R
@@ -126,6 +127,7 @@ constructor(
private val displayId = bundle.getInt(KEY_DISPLAY_ID, DEFAULT_DISPLAY)
private val display: Display? = displayManager.getDisplay(displayId)
+
/**
* Returns a key that should make the KeyguardPreviewRenderer unique and if two of them have the
* same key they will be treated as the same KeyguardPreviewRenderer. Primary this is used to
@@ -144,6 +146,8 @@ constructor(
get() = checkNotNull(host.surfacePackage)
private var smartSpaceView: View? = null
+ private var largeDateView: View? = null
+ private var smallDateView: View? = null
private val disposables = DisposableHandles()
private var isDestroyed = false
@@ -181,7 +185,7 @@ constructor(
ContextThemeWrapper(context.createDisplayContext(it), context.getTheme())
} ?: context
- val rootView = FrameLayout(previewContext)
+ val rootView = ConstraintLayout(previewContext)
setupKeyguardRootView(previewContext, rootView)
@@ -252,6 +256,24 @@ constructor(
fun onClockSizeSelected(clockSize: ClockSizeSetting) {
smartspaceViewModel.setOverrideClockSize(clockSize)
+ if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ when (clockSize) {
+ ClockSizeSetting.DYNAMIC -> {
+ largeDateView?.post {
+ smallDateView?.visibility = View.GONE
+ largeDateView?.visibility = View.VISIBLE
+ }
+ }
+
+ ClockSizeSetting.SMALL -> {
+ largeDateView?.post {
+ smallDateView?.visibility = View.VISIBLE
+ largeDateView?.visibility = View.GONE
+ }
+ }
+ }
+ smartSpaceView?.post { smartSpaceView?.visibility = View.GONE }
+ }
}
fun destroy() {
@@ -280,7 +302,7 @@ constructor(
*
* The end padding is as follows: Below clock padding end
*/
- private fun setUpSmartspace(previewContext: Context, parentView: ViewGroup) {
+ private fun setUpSmartspace(previewContext: Context, parentView: ConstraintLayout) {
if (
!lockscreenSmartspaceController.isEnabled ||
!lockscreenSmartspaceController.isDateWeatherDecoupled
@@ -292,40 +314,90 @@ constructor(
parentView.removeView(smartSpaceView)
}
- smartSpaceView =
- lockscreenSmartspaceController.buildAndConnectDateView(
- parent = parentView,
- isLargeClock = false,
- )
+ if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ val cs = ConstraintSet()
+ cs.clone(parentView)
+ cs.apply {
+ val largeClockViewId = previewContext.getId("lockscreen_clock_view_large")
+ val smallClockViewId = previewContext.getId("lockscreen_clock_view")
+ largeDateView =
+ lockscreenSmartspaceController
+ .buildAndConnectDateView(parentView, true)
+ ?.also { view ->
+ constrainWidth(view.id, ConstraintSet.WRAP_CONTENT)
+ constrainHeight(view.id, ConstraintSet.WRAP_CONTENT)
+ connect(view.id, START, largeClockViewId, START)
+ connect(view.id, ConstraintSet.END, largeClockViewId, ConstraintSet.END)
+ connect(
+ view.id,
+ TOP,
+ largeClockViewId,
+ ConstraintSet.BOTTOM,
+ smartspaceViewModel.getDateWeatherEndPadding(previewContext),
+ )
+ }
+ smallDateView =
+ lockscreenSmartspaceController
+ .buildAndConnectDateView(parentView, false)
+ ?.also { view ->
+ constrainWidth(view.id, ConstraintSet.WRAP_CONTENT)
+ constrainHeight(view.id, ConstraintSet.WRAP_CONTENT)
+ connect(
+ view.id,
+ START,
+ smallClockViewId,
+ ConstraintSet.END,
+ context.resources.getDimensionPixelSize(
+ R.dimen.smartspace_padding_horizontal
+ ),
+ )
+ connect(view.id, TOP, smallClockViewId, TOP)
+ connect(
+ view.id,
+ ConstraintSet.BOTTOM,
+ smallClockViewId,
+ ConstraintSet.BOTTOM,
+ )
+ }
+ parentView.addView(largeDateView)
+ parentView.addView(smallDateView)
+ }
+ cs.applyTo(parentView)
+ } else {
+ smartSpaceView =
+ lockscreenSmartspaceController.buildAndConnectDateView(
+ parent = parentView,
+ isLargeClock = false,
+ )
- val topPadding: Int =
- smartspaceViewModel.getLargeClockSmartspaceTopPadding(
- ClockPreviewConfig(
- previewContext,
- getPreviewShadeLayoutWide(display!!),
- SceneContainerFlag.isEnabled,
+ val topPadding: Int =
+ smartspaceViewModel.getLargeClockSmartspaceTopPadding(
+ ClockPreviewConfig(
+ previewContext,
+ getPreviewShadeLayoutWide(display!!),
+ SceneContainerFlag.isEnabled,
+ )
)
- )
- val startPadding: Int = smartspaceViewModel.getDateWeatherStartPadding(previewContext)
- val endPadding: Int = smartspaceViewModel.getDateWeatherEndPadding(previewContext)
-
- smartSpaceView?.let {
- it.setPaddingRelative(startPadding, topPadding, endPadding, 0)
- it.isClickable = false
- it.isInvisible = true
- parentView.addView(
- it,
- FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT,
- FrameLayout.LayoutParams.WRAP_CONTENT,
- ),
- )
+ val startPadding: Int = smartspaceViewModel.getDateWeatherStartPadding(previewContext)
+ val endPadding: Int = smartspaceViewModel.getDateWeatherEndPadding(previewContext)
+
+ smartSpaceView?.let {
+ it.setPaddingRelative(startPadding, topPadding, endPadding, 0)
+ it.isClickable = false
+ it.isInvisible = true
+ parentView.addView(
+ it,
+ FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.WRAP_CONTENT,
+ ),
+ )
+ }
+ smartSpaceView?.alpha = if (shouldHighlightSelectedAffordance) DIM_ALPHA else 1.0f
}
-
- smartSpaceView?.alpha = if (shouldHighlightSelectedAffordance) DIM_ALPHA else 1.0f
}
- private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) {
+ private fun setupKeyguardRootView(previewContext: Context, rootView: ConstraintLayout) {
val keyguardRootView = KeyguardRootView(previewContext, null)
rootView.addView(
keyguardRootView,
@@ -341,6 +413,13 @@ constructor(
if (!shouldHideClock) {
setUpClock(previewContext, rootView)
+ if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ setUpSmartspace(previewContext, keyguardRootView)
+ KeyguardPreviewSmartspaceViewBinder.bind(
+ keyguardRootView,
+ smartspaceViewModel,
+ )
+ }
KeyguardPreviewClockViewBinder.bind(
keyguardRootView,
clockViewModel,
@@ -354,19 +433,22 @@ constructor(
)
}
- setUpSmartspace(previewContext, rootView)
-
- smartSpaceView?.let {
- KeyguardPreviewSmartspaceViewBinder.bind(
- it,
- smartspaceViewModel,
- clockPreviewConfig =
- ClockPreviewConfig(
- previewContext,
- getPreviewShadeLayoutWide(display!!),
- SceneContainerFlag.isEnabled,
- ),
- )
+ if (!com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) {
+ setUpSmartspace(previewContext, keyguardRootView)
+ smartSpaceView?.let {
+ KeyguardPreviewSmartspaceViewBinder.bind(
+ it,
+ smartspaceViewModel,
+ clockPreviewConfig =
+ ClockPreviewConfig(
+ previewContext,
+ getPreviewShadeLayoutWide(display!!),
+ SceneContainerFlag.isEnabled,
+ lockId = null,
+ udfpsTop = null,
+ ),
+ )
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
index 1d2edc6d406b..d4e7af48adfe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardTouchHandlingViewModel.kt
@@ -33,6 +33,9 @@ constructor(
/** Whether the long-press handling feature should be enabled. */
val isLongPressHandlingEnabled: Flow<Boolean> = interactor.isLongPressHandlingEnabled
+ /** Whether the double tap handling feature should be enabled. */
+ val isDoubleTapHandlingEnabled: Flow<Boolean> = interactor.isDoubleTapHandlingEnabled
+
/** Notifies that the user has long-pressed on the lock screen.
*
* @param isA11yAction: Whether the action was performed as an a11y action
diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
index e7c2a454e16c..b71d8c995e51 100644
--- a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java
@@ -17,6 +17,7 @@
package com.android.systemui.media;
import android.annotation.Nullable;
+import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -126,6 +127,8 @@ public class RingtonePlayer implements CoreStartable {
Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid="
+ Binder.getCallingUid() + ")");
}
+ enforceUriUserId(uri);
+
Client client;
synchronized (mClients) {
client = mClients.get(token);
@@ -207,6 +210,7 @@ public class RingtonePlayer implements CoreStartable {
@Override
public String getTitle(Uri uri) {
+ enforceUriUserId(uri);
final UserHandle user = Binder.getCallingUserHandle();
return Ringtone.getTitle(getContextForUser(user), uri,
false /*followSettingsUri*/, false /*allowRemote*/);
@@ -214,6 +218,7 @@ public class RingtonePlayer implements CoreStartable {
@Override
public ParcelFileDescriptor openRingtone(Uri uri) {
+ enforceUriUserId(uri);
final UserHandle user = Binder.getCallingUserHandle();
final ContentResolver resolver = getContextForUser(user).getContentResolver();
@@ -241,6 +246,28 @@ public class RingtonePlayer implements CoreStartable {
}
};
+ /**
+ * Must be called from the Binder calling thread.
+ * Ensures caller is from the same userId as the content they're trying to access.
+ * @param uri the URI to check
+ * @throws SecurityException when in a non-system call and userId in uri differs from the
+ * caller's userId
+ */
+ private void enforceUriUserId(Uri uri) throws SecurityException {
+ final int uriUserId = ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId());
+ // for a non-system call, verify the URI to play belongs to the same user as the caller
+ if (UserHandle.isApp(Binder.getCallingUid()) && (UserHandle.myUserId() != uriUserId)) {
+ final String errorMessage = "Illegal access to uri=" + uri
+ + " content associated with user=" + uriUserId
+ + ", current userID: " + UserHandle.myUserId();
+ if (android.media.audio.Flags.ringtoneUserUriCheck()) {
+ throw new SecurityException(errorMessage);
+ } else {
+ Log.e(TAG, errorMessage, new Exception());
+ }
+ }
+ }
+
private Context getContextForUser(UserHandle user) {
try {
return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
index 49b53c2d78ae..dfb32e66dae5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt
@@ -37,6 +37,7 @@ import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
import com.android.settingslib.media.PhoneMediaDevice
import com.android.settingslib.media.flags.Flags
+import com.android.systemui.Flags.mediaControlsDeviceManagerBackgroundExecution
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.controls.shared.MediaControlDrawables
@@ -94,8 +95,39 @@ constructor(
data: MediaData,
immediately: Boolean,
receivedSmartspaceCardLatency: Int,
- isSsReactivated: Boolean
+ isSsReactivated: Boolean,
) {
+ if (mediaControlsDeviceManagerBackgroundExecution()) {
+ bgExecutor.execute { onMediaLoaded(key, oldKey, data) }
+ } else {
+ onMediaLoaded(key, oldKey, data)
+ }
+ }
+
+ override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
+ if (mediaControlsDeviceManagerBackgroundExecution()) {
+ bgExecutor.execute { onMediaRemoved(key, userInitiated) }
+ } else {
+ onMediaRemoved(key, userInitiated)
+ }
+ }
+
+ fun dump(pw: PrintWriter) {
+ with(pw) {
+ println("MediaDeviceManager state:")
+ entries.forEach { (key, entry) ->
+ println(" key=$key")
+ entry.dump(pw)
+ }
+ }
+ }
+
+ @MainThread
+ private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) {
+ listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) }
+ }
+
+ private fun onMediaLoaded(key: String, oldKey: String?, data: MediaData) {
if (oldKey != null && oldKey != key) {
val oldEntry = entries.remove(oldKey)
oldEntry?.stop()
@@ -104,9 +136,13 @@ constructor(
if (entry == null || entry.token != data.token) {
entry?.stop()
if (data.device != null) {
- // If we were already provided device info (e.g. from RCN), keep that and don't
- // listen for updates, but process once to push updates to listeners
- processDevice(key, oldKey, data.device)
+ // If we were already provided device info (e.g. from RCN), keep that and
+ // don't listen for updates, but process once to push updates to listeners
+ if (mediaControlsDeviceManagerBackgroundExecution()) {
+ fgExecutor.execute { processDevice(key, oldKey, data.device) }
+ } else {
+ processDevice(key, oldKey, data.device)
+ }
return
}
val controller = data.token?.let { controllerFactory.create(it) }
@@ -120,27 +156,18 @@ constructor(
}
}
- override fun onMediaDataRemoved(key: String, userInitiated: Boolean) {
+ private fun onMediaRemoved(key: String, userInitiated: Boolean) {
val token = entries.remove(key)
token?.stop()
- token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } }
- }
-
- fun dump(pw: PrintWriter) {
- with(pw) {
- println("MediaDeviceManager state:")
- entries.forEach { (key, entry) ->
- println(" key=$key")
- entry.dump(pw)
+ if (mediaControlsDeviceManagerBackgroundExecution()) {
+ fgExecutor.execute {
+ token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } }
}
+ } else {
+ token?.let { listeners.forEach { it.onKeyRemoved(key, userInitiated) } }
}
}
- @MainThread
- private fun processDevice(key: String, oldKey: String?, device: MediaDeviceData?) {
- listeners.forEach { it.onMediaDeviceChanged(key, oldKey, device) }
- }
-
interface Listener {
/** Called when the route has changed for a given notification. */
fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?)
@@ -260,7 +287,7 @@ constructor(
override fun onAboutToConnectDeviceAdded(
deviceAddress: String,
deviceName: String,
- deviceIcon: Drawable?
+ deviceIcon: Drawable?,
) {
aboutToConnectDeviceOverride =
AboutToConnectDevice(
@@ -270,8 +297,8 @@ constructor(
/* enabled */ enabled = true,
/* icon */ deviceIcon,
/* name */ deviceName,
- /* showBroadcastButton */ showBroadcastButton = false
- )
+ /* showBroadcastButton */ showBroadcastButton = false,
+ ),
)
updateCurrent()
}
@@ -292,7 +319,7 @@ constructor(
override fun onBroadcastMetadataChanged(
broadcastId: Int,
- metadata: BluetoothLeBroadcastMetadata
+ metadata: BluetoothLeBroadcastMetadata,
) {
logger.logBroadcastMetadataChanged(broadcastId, metadata.toString())
updateCurrent()
@@ -352,14 +379,14 @@ constructor(
// route.
connectedDevice?.copy(
name = it.name ?: connectedDevice.name,
- icon = icon
+ icon = icon,
)
}
?: MediaDeviceData(
enabled = false,
icon = MediaControlDrawables.getHomeDevices(context),
name = context.getString(R.string.media_seamless_other_device),
- showBroadcastButton = false
+ showBroadcastButton = false,
)
logger.logRemoteDevice(routingSession?.name, connectedDevice)
} else {
@@ -398,7 +425,7 @@ constructor(
device?.iconWithoutBackground,
name,
id = device?.id,
- showBroadcastButton = false
+ showBroadcastButton = false,
)
}
}
@@ -415,7 +442,7 @@ constructor(
icon = iconWithoutBackground,
name = name,
id = id,
- showBroadcastButton = false
+ showBroadcastButton = false,
)
private fun getLeAudioBroadcastDeviceData(): MediaDeviceData {
@@ -425,7 +452,7 @@ constructor(
icon = MediaControlDrawables.getLeAudioSharing(context),
name = context.getString(R.string.audio_sharing_description),
intent = null,
- showBroadcastButton = false
+ showBroadcastButton = false,
)
} else {
MediaDeviceData(
@@ -433,7 +460,7 @@ constructor(
icon = MediaControlDrawables.getAntenna(context),
name = broadcastDescription,
intent = null,
- showBroadcastButton = true
+ showBroadcastButton = true,
)
}
}
@@ -449,7 +476,7 @@ constructor(
device,
controller,
routingSession?.name,
- selectedRoutes?.firstOrNull()?.name
+ selectedRoutes?.firstOrNull()?.name,
)
if (controller == null) {
@@ -514,7 +541,7 @@ constructor(
MediaDataUtils.getAppLabel(
context,
localMediaManager.packageName,
- context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name)
+ context.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name),
)
val isCurrentBroadcastedApp = TextUtils.equals(mediaApp, currentBroadcastedApp)
if (isCurrentBroadcastedApp) {
@@ -538,5 +565,5 @@ constructor(
*/
private data class AboutToConnectDevice(
val fullMediaDevice: MediaDevice? = null,
- val backupMediaDeviceData: MediaDeviceData? = null
+ val backupMediaDeviceData: MediaDeviceData? = null,
)
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
index c58ba377fb68..ac1672db9375 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
@@ -351,8 +351,9 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl
@VisibleForTesting
void showCustomEndSessionDialog(MediaDevice device) {
MediaSessionReleaseDialog mediaSessionReleaseDialog = new MediaSessionReleaseDialog(
- mContext, () -> transferOutput(device), mController.getColorButtonBackground(),
- mController.getColorItemContent());
+ mContext, () -> transferOutput(device),
+ mController.getColorSchemeLegacy().getColorButtonBackground(),
+ mController.getColorSchemeLegacy().getColorItemContent());
mediaSessionReleaseDialog.show();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
index 290f62f595b1..6ab4a52dc919 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacy.java
@@ -50,9 +50,11 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.media.flags.Flags;
import com.android.settingslib.media.InputMediaDevice;
import com.android.settingslib.media.MediaDevice;
-import com.android.settingslib.utils.ThreadUtils;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.res.R;
+import java.util.concurrent.Executor;
/**
* A RecyclerView adapter for the legacy UI media output dialog device list.
*/
@@ -63,12 +65,18 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
private static final int UNMUTE_DEFAULT_VOLUME = 2;
@VisibleForTesting static final float DEVICE_DISABLED_ALPHA = 0.5f;
@VisibleForTesting static final float DEVICE_ACTIVE_ALPHA = 1f;
+ private final Executor mMainExecutor;
+ private final Executor mBackgroundExecutor;
View mHolderView;
- private boolean mIsInitVolumeFirstTime;
- public MediaOutputAdapterLegacy(MediaSwitchingController controller) {
+ public MediaOutputAdapterLegacy(
+ MediaSwitchingController controller,
+ @Main Executor mainExecutor,
+ @Background Executor backgroundExecutor
+ ) {
super(controller);
- mIsInitVolumeFirstTime = true;
+ mMainExecutor = mainExecutor;
+ mBackgroundExecutor = backgroundExecutor;
}
@Override
@@ -181,9 +189,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
mEndTouchArea.setVisibility(View.GONE);
mEndClickIcon.setVisibility(View.GONE);
mContainerLayout.setOnClickListener(null);
- mTitleText.setTextColor(mController.getColorItemContent());
- mSubTitleText.setTextColor(mController.getColorItemContent());
- mVolumeValueText.setTextColor(mController.getColorItemContent());
+ mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
+ mSubTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
+ mVolumeValueText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
mIconAreaLayout.setBackground(null);
updateIconAreaClickListener(null);
updateSeekBarProgressColor();
@@ -193,14 +201,14 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
/** Binds a ViewHolder for a "Connect a device" item. */
void onBindPairNewDevice() {
- mTitleText.setTextColor(mController.getColorItemContent());
+ mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
mCheckBox.setVisibility(View.GONE);
updateTitle(mContext.getText(R.string.media_output_dialog_pairing_new));
updateItemBackground(ConnectionState.DISCONNECTED);
final Drawable addDrawable = mContext.getDrawable(R.drawable.ic_add);
mTitleIcon.setImageDrawable(addDrawable);
- mTitleIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ mTitleIcon.setImageTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
mContainerLayout.setOnClickListener(mController::launchBluetoothPairing);
}
@@ -251,10 +259,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
updateSeekbarProgressBackground();
}
}
- boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
mSeekBar.setVisibility(showSeekBar ? View.VISIBLE : View.GONE);
if (showSeekBar) {
- initSeekbar(device, isCurrentSeekbarInvisible);
+ initSeekbar(device);
updateContainerContentA11yImportance(false /* isImportant */);
mSeekBar.setContentDescription(contentDescription);
} else {
@@ -264,9 +271,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
void updateGroupSeekBar(String contentDescription) {
updateSeekbarProgressBackground();
- boolean isCurrentSeekbarInvisible = mSeekBar.getVisibility() == View.GONE;
mSeekBar.setVisibility(View.VISIBLE);
- initGroupSeekbar(isCurrentSeekbarInvisible);
+ initGroupSeekbar();
updateContainerContentA11yImportance(false /* isImportant */);
mSeekBar.setContentDescription(contentDescription);
}
@@ -297,8 +303,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
protected void updateLoadingIndicator(ConnectionState connectionState) {
if (connectionState == ConnectionState.CONNECTING) {
mProgressBar.setVisibility(View.VISIBLE);
- mProgressBar.getIndeterminateDrawable().setTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ mProgressBar.getIndeterminateDrawable().setTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
} else {
mProgressBar.setVisibility(View.GONE);
}
@@ -318,8 +324,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
// Connected or connecting state has a darker background.
int backgroundColor = isConnected || isConnecting
- ? mController.getColorConnectedItemBackground()
- : mController.getColorItemBackground();
+ ? mController.getColorSchemeLegacy().getColorConnectedItemBackground()
+ : mController.getColorSchemeLegacy().getColorItemBackground();
mItemLayout.setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
}
@@ -332,13 +338,13 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
}
private void updateSeekBarProgressColor() {
- mSeekBar.setProgressTintList(
- ColorStateList.valueOf(mController.getColorSeekbarProgress()));
+ mSeekBar.setProgressTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorSeekbarProgress()));
final Drawable contrastDotDrawable =
((LayerDrawable) mSeekBar.getProgressDrawable()).findDrawableByLayerId(
R.id.contrast_dot);
- contrastDotDrawable.setTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ contrastDotDrawable.setTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
}
void updateSeekbarProgressBackground() {
@@ -354,31 +360,21 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
mActiveRadius, 0, 0});
}
- private void initializeSeekbarVolume(
- @Nullable MediaDevice device, int currentVolume,
- boolean isCurrentSeekbarInvisible) {
+ private void initializeSeekbarVolume(@Nullable MediaDevice device, int currentVolume) {
if (!isDragging()) {
if (mSeekBar.getVolume() != currentVolume && (mLatestUpdateVolume == -1
|| currentVolume == mLatestUpdateVolume)) {
// Update only if volume of device and value of volume bar doesn't match.
// Check if response volume match with the latest request, to ignore obsolete
// response
- if (isCurrentSeekbarInvisible && !mIsInitVolumeFirstTime) {
+ if (!mVolumeAnimator.isStarted()) {
if (currentVolume == 0) {
updateMutedVolumeIcon(device);
} else {
updateUnmutedVolumeIcon(device);
}
- } else {
- if (!mVolumeAnimator.isStarted()) {
- if (currentVolume == 0) {
- updateMutedVolumeIcon(device);
- } else {
- updateUnmutedVolumeIcon(device);
- }
- mSeekBar.setVolume(currentVolume);
- mLatestUpdateVolume = -1;
- }
+ mSeekBar.setVolume(currentVolume);
+ mLatestUpdateVolume = -1;
}
} else if (currentVolume == 0) {
mSeekBar.resetVolume();
@@ -388,12 +384,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
mLatestUpdateVolume = -1;
}
}
- if (mIsInitVolumeFirstTime) {
- mIsInitVolumeFirstTime = false;
- }
}
- void initSeekbar(@NonNull MediaDevice device, boolean isCurrentSeekbarInvisible) {
+ void initSeekbar(@NonNull MediaDevice device) {
SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() {
@Override
public int getVolume() {
@@ -422,7 +415,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
}
mSeekBar.setMaxVolume(device.getMaxVolume());
final int currentVolume = device.getCurrentVolume();
- initializeSeekbarVolume(device, currentVolume, isCurrentSeekbarInvisible);
+ initializeSeekbarVolume(device, currentVolume);
mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener(
device, volumeControl) {
@@ -435,7 +428,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
}
// Initializes the seekbar for a group of devices.
- void initGroupSeekbar(boolean isCurrentSeekbarInvisible) {
+ void initGroupSeekbar() {
SeekBarVolumeControl volumeControl = new SeekBarVolumeControl() {
@Override
public int getVolume() {
@@ -462,7 +455,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
mSeekBar.setMaxVolume(mController.getSessionVolumeMax());
final int currentVolume = mController.getSessionVolume();
- initializeSeekbarVolume(null, currentVolume, isCurrentSeekbarInvisible);
+ initializeSeekbarVolume(null, currentVolume);
mSeekBar.setOnSeekBarChangeListener(new MediaSeekBarChangedListener(
null, volumeControl) {
@Override
@@ -503,9 +496,10 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
boolean isInputMediaDevice = device instanceof InputMediaDevice;
int id = getDrawableId(isInputMediaDevice, isMutedVolumeIcon);
mTitleIcon.setImageDrawable(mContext.getDrawable(id));
- mTitleIcon.setImageTintList(ColorStateList.valueOf(mController.getColorItemContent()));
- mIconAreaLayout.setBackgroundTintList(
- ColorStateList.valueOf(mController.getColorSeekbarProgress()));
+ mTitleIcon.setImageTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
+ mIconAreaLayout.setBackgroundTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorSeekbarProgress()));
}
@VisibleForTesting
@@ -534,8 +528,8 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
mStatusIcon.setVisibility(View.GONE);
} else {
mStatusIcon.setImageDrawable(deviceStatusIcon);
- mStatusIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ mStatusIcon.setImageTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
if (deviceStatusIcon instanceof AnimatedVectorDrawable) {
((AnimatedVectorDrawable) deviceStatusIcon).start();
}
@@ -585,9 +579,10 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
private void updateEndAreaWithIcon(View.OnClickListener clickListener,
@DrawableRes int iconDrawableId,
@StringRes int accessibilityStringId) {
- updateEndAreaColor(mController.getColorSeekbarProgress());
+ updateEndAreaColor(mController.getColorSchemeLegacy().getColorSeekbarProgress());
mEndClickIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
mEndClickIcon.setOnClickListener(clickListener);
Drawable drawable = mContext.getDrawable(iconDrawableId);
mEndClickIcon.setImageDrawable(drawable);
@@ -600,8 +595,9 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
private void updateEndAreaForGroupCheckBox(@NonNull MediaDevice device,
@NonNull GroupStatus groupStatus) {
boolean isEnabled = isGroupCheckboxEnabled(groupStatus);
- updateEndAreaColor(groupStatus.selected() ? mController.getColorSeekbarProgress()
- : mController.getColorItemBackground());
+ updateEndAreaColor(groupStatus.selected()
+ ? mController.getColorSchemeLegacy().getColorSeekbarProgress()
+ : mController.getColorSchemeLegacy().getColorItemBackground());
mCheckBox.setContentDescription(mContext.getString(
groupStatus.selected() ? R.string.accessibility_remove_device_from_group
: R.string.accessibility_add_device_to_group));
@@ -611,7 +607,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
isEnabled ? (buttonView, isChecked) -> onGroupActionTriggered(
!groupStatus.selected(), device) : null);
mCheckBox.setEnabled(isEnabled);
- setCheckBoxColor(mCheckBox, mController.getColorItemContent());
+ setCheckBoxColor(mCheckBox, mController.getColorSchemeLegacy().getColorItemContent());
}
private void setCheckBoxColor(CheckBox checkBox, int color) {
@@ -714,15 +710,15 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
}
protected void setUpDeviceIcon(@NonNull MediaDevice device) {
- ThreadUtils.postOnBackgroundThread(() -> {
+ mBackgroundExecutor.execute(() -> {
Icon icon = mController.getDeviceIconCompat(device).toIcon(mContext);
- ThreadUtils.postOnMainThread(() -> {
+ mMainExecutor.execute(() -> {
if (!TextUtils.equals(mDeviceId, device.getId())) {
return;
}
mTitleIcon.setImageIcon(icon);
- mTitleIcon.setImageTintList(
- ColorStateList.valueOf(mController.getColorItemContent()));
+ mTitleIcon.setImageTintList(ColorStateList.valueOf(
+ mController.getColorSchemeLegacy().getColorItemContent()));
});
});
}
@@ -807,7 +803,7 @@ public class MediaOutputAdapterLegacy extends MediaOutputAdapterBase {
}
void onBind(String groupDividerTitle) {
- mTitleText.setTextColor(mController.getColorItemContent());
+ mTitleText.setTextColor(mController.getColorSchemeLegacy().getColorItemContent());
mTitleText.setText(groupDividerTitle);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index d791361d555f..49d09cf64c8e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -40,7 +40,6 @@ import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
@@ -93,13 +92,10 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
private ImageView mAppResourceIcon;
private ImageView mBroadcastIcon;
private RecyclerView mDevicesRecyclerView;
- private LinearLayout mDeviceListLayout;
+ private ViewGroup mDeviceListLayout;
private LinearLayout mMediaMetadataSectionLayout;
private Button mDoneButton;
private Button mStopButton;
- private int mListMaxHeight;
- private int mItemHeight;
- private int mListPaddingTop;
private WallpaperColors mWallpaperColors;
private boolean mShouldLaunchLeBroadcastDialog;
private boolean mIsLeBroadcastCallbackRegistered;
@@ -109,17 +105,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
protected Executor mExecutor;
- private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> {
- ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams();
- int totalItemsHeight = mAdapter.getItemCount() * mItemHeight
- + mListPaddingTop;
- int correctHeight = Math.min(totalItemsHeight, mListMaxHeight);
- // Set max height for list
- if (correctHeight != params.height) {
- params.height = correctHeight;
- mDeviceListLayout.setLayoutParams(params);
- }
- };
private final BluetoothLeBroadcast.Callback mBroadcastCallback =
new BluetoothLeBroadcast.Callback() {
@@ -220,12 +205,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mBroadcastSender = broadcastSender;
mMediaSwitchingController = mediaSwitchingController;
mLayoutManager = new LayoutManagerWrapper(mContext);
- mListMaxHeight = context.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_list_max_height);
- mItemHeight = context.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_list_item_height);
- mListPaddingTop = mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_list_padding_top);
mExecutor = Executors.newSingleThreadExecutor();
mIncludePlaybackAndAppMetadata = includePlaybackAndAppMetadata;
}
@@ -258,8 +237,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mAppResourceIcon = mDialogView.requireViewById(R.id.app_source_icon);
mBroadcastIcon = mDialogView.requireViewById(R.id.broadcast_icon);
- mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener(
- mDeviceListLayoutListener);
// Init device list
mLayoutManager.setAutoMeasureEnabled(true);
mDevicesRecyclerView.setLayoutManager(mLayoutManager);
@@ -342,7 +319,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(icon.getBitmap());
colorSetUpdated = !wallpaperColors.equals(mWallpaperColors);
if (colorSetUpdated) {
- mMediaSwitchingController.setCurrentColorScheme(wallpaperColors, isDarkThemeOn);
+ mMediaSwitchingController.updateCurrentColorScheme(wallpaperColors,
+ isDarkThemeOn);
updateButtonBackgroundColorFilter();
updateDialogBackgroundColor();
}
@@ -359,7 +337,8 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mAppResourceIcon.setVisibility(View.GONE);
} else if (appSourceIcon != null) {
Icon appIcon = appSourceIcon.toIcon(mContext);
- mAppResourceIcon.setColorFilter(mMediaSwitchingController.getColorItemContent());
+ mAppResourceIcon.setColorFilter(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorItemContent());
mAppResourceIcon.setImageIcon(appIcon);
} else {
Drawable appIconDrawable = mMediaSwitchingController.getAppSourceIconFromPackage();
@@ -369,12 +348,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mAppResourceIcon.setVisibility(View.GONE);
}
}
- if (mHeaderIcon.getVisibility() == View.VISIBLE) {
- final int size = getHeaderIconSize();
- final int padding = mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_header_icon_padding);
- mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, size));
- }
if (!mIncludePlaybackAndAppMetadata) {
mHeaderTitle.setVisibility(View.GONE);
@@ -419,18 +392,19 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
private void updateButtonBackgroundColorFilter() {
ColorFilter buttonColorFilter =
new PorterDuffColorFilter(
- mMediaSwitchingController.getColorButtonBackground(),
+ mMediaSwitchingController.getColorSchemeLegacy().getColorButtonBackground(),
PorterDuff.Mode.SRC_IN);
mDoneButton.getBackground().setColorFilter(buttonColorFilter);
mStopButton.getBackground().setColorFilter(buttonColorFilter);
- mDoneButton.setTextColor(mMediaSwitchingController.getColorPositiveButtonText());
+ mDoneButton.setTextColor(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorPositiveButtonText());
}
private void updateDialogBackgroundColor() {
- getDialogView()
- .getBackground()
- .setTint(mMediaSwitchingController.getColorDialogBackground());
- mDeviceListLayout.setBackgroundColor(mMediaSwitchingController.getColorDialogBackground());
+ getDialogView().getBackground().setTint(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground());
+ mDeviceListLayout.setBackgroundColor(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground());
}
public void handleLeBroadcastStarted() {
@@ -520,8 +494,6 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
abstract IconCompat getHeaderIcon();
- abstract int getHeaderIconSize();
-
abstract CharSequence getHeaderText();
abstract CharSequence getHeaderSubtitle();
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
index 9ade9e275ca1..791a61cc73ec 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
@@ -52,6 +52,8 @@ import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.google.zxing.WriterException;
+import java.util.concurrent.Executor;
+
/**
* Dialog for media output broadcast.
*/
@@ -239,13 +241,16 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog {
Context context,
boolean aboveStatusbar,
BroadcastSender broadcastSender,
- MediaSwitchingController mediaSwitchingController) {
+ MediaSwitchingController mediaSwitchingController,
+ Executor mainExecutor,
+ Executor backgroundExecutor) {
super(
context,
broadcastSender,
mediaSwitchingController, /* includePlaybackAndAppMetadata */
true);
- mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor,
+ backgroundExecutor);
// TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class
// that extends MediaOutputBaseDialog
if (!aboveStatusbar) {
@@ -295,12 +300,6 @@ public class MediaOutputBroadcastDialog extends MediaOutputBaseDialog {
}
@Override
- int getHeaderIconSize() {
- return mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_header_album_icon_size);
- }
-
- @Override
CharSequence getHeaderText() {
return mMediaSwitchingController.getHeaderTitle();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
index 2e7e66f5b384..81c85a6ad22d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogManager.kt
@@ -20,6 +20,9 @@ import android.content.Context
import android.view.View
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import java.util.concurrent.Executor
import javax.inject.Inject
/** Manager to create and show a [MediaOutputBroadcastDialog]. */
@@ -29,7 +32,9 @@ constructor(
private val context: Context,
private val broadcastSender: BroadcastSender,
private val dialogTransitionAnimator: DialogTransitionAnimator,
- private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory
+ private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory,
+ @Main private val mainExecutor: Executor,
+ @Background private val backgroundExecutor: Executor,
) {
var mediaOutputBroadcastDialog: MediaOutputBroadcastDialog? = null
@@ -47,7 +52,14 @@ constructor(
/* token */ null,
)
val dialog =
- MediaOutputBroadcastDialog(context, aboveStatusBar, broadcastSender, controller)
+ MediaOutputBroadcastDialog(
+ context,
+ aboveStatusBar,
+ broadcastSender,
+ controller,
+ mainExecutor,
+ backgroundExecutor,
+ )
mediaOutputBroadcastDialog = dialog
// Show the dialog.
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt
new file mode 100644
index 000000000000..7f0fa463811b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorSchemeLegacy.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog
+
+import android.content.Context
+import com.android.settingslib.Utils
+import com.android.systemui.monet.ColorScheme
+import com.android.systemui.res.R
+
+abstract class MediaOutputColorSchemeLegacy {
+ companion object Factory {
+ @JvmStatic
+ fun fromSystemColors(context: Context): MediaOutputColorSchemeLegacy {
+ return MediaOutputColorSchemeLegacySystem(context)
+ }
+
+ @JvmStatic
+ fun fromDynamicColors(
+ colorScheme: ColorScheme,
+ isDarkTheme: Boolean,
+ ): MediaOutputColorSchemeLegacy {
+ return MediaOutputColorSchemeLegacyDynamic(colorScheme, isDarkTheme)
+ }
+ }
+
+ abstract fun getColorConnectedItemBackground(): Int
+
+ abstract fun getColorPositiveButtonText(): Int
+
+ abstract fun getColorDialogBackground(): Int
+
+ abstract fun getColorItemContent(): Int
+
+ abstract fun getColorSeekbarProgress(): Int
+
+ abstract fun getColorButtonBackground(): Int
+
+ abstract fun getColorItemBackground(): Int
+}
+
+class MediaOutputColorSchemeLegacySystem(private val mContext: Context) :
+ MediaOutputColorSchemeLegacy() {
+
+ override fun getColorConnectedItemBackground() =
+ Utils.getColorStateListDefaultColor(
+ mContext,
+ R.color.media_dialog_connected_item_background,
+ )
+
+ override fun getColorPositiveButtonText() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_solid_button_text)
+
+ override fun getColorDialogBackground() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_background)
+
+ override fun getColorItemContent() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_item_main_content)
+
+ override fun getColorSeekbarProgress() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_seekbar_progress)
+
+ override fun getColorButtonBackground() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_button_background)
+
+ override fun getColorItemBackground() =
+ Utils.getColorStateListDefaultColor(mContext, R.color.media_dialog_item_background)
+}
+
+class MediaOutputColorSchemeLegacyDynamic(colorScheme: ColorScheme, isDarkTheme: Boolean) :
+ MediaOutputColorSchemeLegacy() {
+ private var mColorItemContent: Int
+ private var mColorSeekbarProgress: Int
+ private var mColorButtonBackground: Int
+ private var mColorItemBackground: Int
+ private var mColorConnectedItemBackground: Int
+ private var mColorPositiveButtonText: Int
+ private var mColorDialogBackground: Int
+
+ init {
+ if (isDarkTheme) {
+ mColorItemContent = colorScheme.accent1.s100 // A1-100
+ mColorSeekbarProgress = colorScheme.accent2.s600 // A2-600
+ mColorButtonBackground = colorScheme.accent1.s300 // A1-300
+ mColorItemBackground = colorScheme.neutral2.s800 // N2-800
+ mColorConnectedItemBackground = colorScheme.accent2.s800 // A2-800
+ mColorPositiveButtonText = colorScheme.accent2.s800 // A2-800
+ mColorDialogBackground = colorScheme.neutral1.s900 // N1-900
+ } else {
+ mColorItemContent = colorScheme.accent1.s800 // A1-800
+ mColorSeekbarProgress = colorScheme.accent1.s300 // A1-300
+ mColorButtonBackground = colorScheme.accent1.s600 // A1-600
+ mColorItemBackground = colorScheme.accent2.s50 // A2-50
+ mColorConnectedItemBackground = colorScheme.accent1.s100 // A1-100
+ mColorPositiveButtonText = colorScheme.neutral1.s50 // N1-50
+ mColorDialogBackground = colorScheme.backgroundColor
+ }
+ }
+
+ override fun getColorConnectedItemBackground() = mColorConnectedItemBackground
+
+ override fun getColorPositiveButtonText() = mColorPositiveButtonText
+
+ override fun getColorDialogBackground() = mColorDialogBackground
+
+ override fun getColorItemContent() = mColorItemContent
+
+ override fun getColorSeekbarProgress() = mColorSeekbarProgress
+
+ override fun getColorButtonBackground() = mColorButtonBackground
+
+ override fun getColorItemBackground() = mColorItemBackground
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index 2e602be4556e..163ff248b9df 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -34,6 +34,8 @@ import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.res.R;
+import java.util.concurrent.Executor;
+
/**
* Dialog for media output transferring.
*/
@@ -49,11 +51,14 @@ public class MediaOutputDialog extends MediaOutputBaseDialog {
MediaSwitchingController mediaSwitchingController,
DialogTransitionAnimator dialogTransitionAnimator,
UiEventLogger uiEventLogger,
+ Executor mainExecutor,
+ Executor backgroundExecutor,
boolean includePlaybackAndAppMetadata) {
super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata);
mDialogTransitionAnimator = dialogTransitionAnimator;
mUiEventLogger = uiEventLogger;
- mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController);
+ mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor,
+ backgroundExecutor);
if (!aboveStatusbar) {
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}
@@ -76,12 +81,6 @@ public class MediaOutputDialog extends MediaOutputBaseDialog {
}
@Override
- int getHeaderIconSize() {
- return mContext.getResources().getDimensionPixelSize(
- R.dimen.media_output_dialog_header_album_icon_size);
- }
-
- @Override
CharSequence getHeaderText() {
return mMediaSwitchingController.getHeaderTitle();
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
index 4e9451a838ad..d3a81a53b6d3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogManager.kt
@@ -25,6 +25,9 @@ import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.broadcast.BroadcastSender
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import java.util.concurrent.Executor
import javax.inject.Inject
/** Manager to create and show a [MediaOutputDialog]. */
@@ -37,6 +40,9 @@ constructor(
private val dialogTransitionAnimator: DialogTransitionAnimator,
private val mediaSwitchingControllerFactory: MediaSwitchingController.Factory,
) {
+ @Inject @Main lateinit var mainExecutor: Executor
+ @Inject @Background lateinit var backgroundExecutor: Executor
+
companion object {
const val INTERACTION_JANK_TAG = "media_output"
var mediaOutputDialog: MediaOutputDialog? = null
@@ -51,7 +57,7 @@ constructor(
aboveStatusBar: Boolean,
view: View? = null,
userHandle: UserHandle? = null,
- token: MediaSession.Token? = null
+ token: MediaSession.Token? = null,
) {
createAndShowWithController(
packageName,
@@ -62,8 +68,8 @@ constructor(
it,
DialogCuj(
InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG
- )
+ INTERACTION_JANK_TAG,
+ ),
)
},
userHandle = userHandle,
@@ -128,15 +134,14 @@ constructor(
controller,
dialogTransitionAnimator,
uiEventLogger,
- includePlaybackAndAppMetadata
+ mainExecutor,
+ backgroundExecutor,
+ includePlaybackAndAppMetadata,
)
// Show the dialog.
if (dialogTransitionAnimatorController != null) {
- dialogTransitionAnimator.show(
- mediaOutputDialog,
- dialogTransitionAnimatorController,
- )
+ dialogTransitionAnimator.show(mediaOutputDialog, dialogTransitionAnimatorController)
} else {
mediaOutputDialog.show()
}
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 02dce406bbee..f79693138e24 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -83,6 +83,8 @@ import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.animation.ActivityTransitionAnimator;
import com.android.systemui.animation.DialogTransitionAnimator;
import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.dialog.MediaItem.MediaItemType;
import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
@@ -114,6 +116,8 @@ import java.util.concurrent.Executor;
import java.util.function.Function;
import java.util.stream.Collectors;
+import javax.inject.Inject;
+
/**
* Controller for a dialog that allows users to switch media output and input devices, control
* volume, connect to new devices, etc.
@@ -141,7 +145,7 @@ public class MediaSwitchingController
@VisibleForTesting
final List<MediaDevice> mGroupMediaDevices = new CopyOnWriteArrayList<>();
final List<MediaDevice> mCachedMediaDevices = new CopyOnWriteArrayList<>();
- private final List<MediaItem> mOutputMediaItemList = new CopyOnWriteArrayList<>();
+ private final OutputMediaItemListProxy mOutputMediaItemListProxy;
private final List<MediaItem> mInputMediaItemList = new CopyOnWriteArrayList<>();
private final AudioManager mAudioManager;
private final PowerExemptionManager mPowerExemptionManager;
@@ -149,7 +153,8 @@ public class MediaSwitchingController
private final NearbyMediaDevicesManager mNearbyMediaDevicesManager;
private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>();
private final MediaSession.Token mToken;
-
+ @Inject @Main Executor mMainExecutor;
+ @Inject @Background Executor mBackgroundExecutor;
@VisibleForTesting
boolean mIsRefreshing = false;
@VisibleForTesting
@@ -163,17 +168,10 @@ public class MediaSwitchingController
@VisibleForTesting
MediaOutputMetricLogger mMetricLogger;
private int mCurrentState;
-
- private int mColorItemContent;
- private int mColorSeekbarProgress;
- private int mColorButtonBackground;
- private int mColorItemBackground;
- private int mColorConnectedItemBackground;
- private int mColorPositiveButtonText;
- private int mColorDialogBackground;
private FeatureFlags mFeatureFlags;
private UserTracker mUserTracker;
private VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor;
+ @NonNull private MediaOutputColorSchemeLegacy mMediaOutputColorSchemeLegacy;
public enum BroadcastNotifyDialog {
ACTION_FIRST_LAUNCH,
@@ -228,22 +226,10 @@ public class MediaSwitchingController
InfoMediaManager.createInstance(mContext, packageName, userHandle, lbm, token);
mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
mMetricLogger = new MediaOutputMetricLogger(mContext, mPackageName);
+ mOutputMediaItemListProxy = new OutputMediaItemListProxy();
mDialogTransitionAnimator = dialogTransitionAnimator;
mNearbyMediaDevicesManager = nearbyMediaDevicesManager;
- mColorItemContent = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_item_main_content);
- mColorSeekbarProgress = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_seekbar_progress);
- mColorButtonBackground = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_button_background);
- mColorItemBackground = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_item_background);
- mColorConnectedItemBackground = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_connected_item_background);
- mColorPositiveButtonText = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_solid_button_text);
- mColorDialogBackground = Utils.getColorStateListDefaultColor(mContext,
- R.color.media_dialog_background);
+ mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext);
if (enableInputRouting()) {
mInputRouteManager = new InputRouteManager(mContext, audioManager);
@@ -260,7 +246,7 @@ public class MediaSwitchingController
protected void start(@NonNull Callback cb) {
synchronized (mMediaDevicesLock) {
mCachedMediaDevices.clear();
- mOutputMediaItemList.clear();
+ mOutputMediaItemListProxy.clear();
}
mNearbyDeviceInfoMap.clear();
if (mNearbyMediaDevicesManager != null) {
@@ -306,7 +292,7 @@ public class MediaSwitchingController
mLocalMediaManager.stopScan();
synchronized (mMediaDevicesLock) {
mCachedMediaDevices.clear();
- mOutputMediaItemList.clear();
+ mOutputMediaItemListProxy.clear();
}
if (mNearbyMediaDevicesManager != null) {
mNearbyMediaDevicesManager.unregisterNearbyDevicesCallback(this);
@@ -348,7 +334,7 @@ public class MediaSwitchingController
@Override
public void onDeviceListUpdate(List<MediaDevice> devices) {
- boolean isListEmpty = mOutputMediaItemList.isEmpty();
+ boolean isListEmpty = mOutputMediaItemListProxy.isEmpty();
if (isListEmpty || !mIsRefreshing) {
buildMediaItems(devices);
mCallback.onDeviceListChanged();
@@ -362,11 +348,12 @@ public class MediaSwitchingController
}
@Override
- public void onSelectedDeviceStateChanged(MediaDevice device,
- @LocalMediaManager.MediaDeviceState int state) {
+ public void onSelectedDeviceStateChanged(
+ MediaDevice device, @LocalMediaManager.MediaDeviceState int state) {
mCallback.onRouteChanged();
mMetricLogger.logOutputItemSuccess(
- device.toString(), new ArrayList<>(mOutputMediaItemList));
+ device.toString(),
+ new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList()));
}
@Override
@@ -377,7 +364,8 @@ public class MediaSwitchingController
@Override
public void onRequestFailed(int reason) {
mCallback.onRouteChanged();
- mMetricLogger.logOutputItemFailure(new ArrayList<>(mOutputMediaItemList), reason);
+ mMetricLogger.logOutputItemFailure(
+ new ArrayList<>(mOutputMediaItemListProxy.getOutputMediaItemList()), reason);
}
/**
@@ -396,7 +384,7 @@ public class MediaSwitchingController
}
try {
synchronized (mMediaDevicesLock) {
- mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
+ mOutputMediaItemListProxy.removeMutingExpectedDevices();
}
mAudioManager.cancelMuteAwaitConnection(mAudioManager.getMutingExpectedDevice());
} catch (Exception e) {
@@ -568,26 +556,15 @@ public class MediaSwitchingController
return null;
}
- void setCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) {
- ColorScheme mCurrentColorScheme = new ColorScheme(wallpaperColors,
+ void updateCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) {
+ ColorScheme currentColorScheme = new ColorScheme(wallpaperColors,
isDarkTheme);
- if (isDarkTheme) {
- mColorItemContent = mCurrentColorScheme.getAccent1().getS100(); // A1-100
- mColorSeekbarProgress = mCurrentColorScheme.getAccent2().getS600(); // A2-600
- mColorButtonBackground = mCurrentColorScheme.getAccent1().getS300(); // A1-300
- mColorItemBackground = mCurrentColorScheme.getNeutral2().getS800(); // N2-800
- mColorConnectedItemBackground = mCurrentColorScheme.getAccent2().getS800(); // A2-800
- mColorPositiveButtonText = mCurrentColorScheme.getAccent2().getS800(); // A2-800
- mColorDialogBackground = mCurrentColorScheme.getNeutral1().getS900(); // N1-900
- } else {
- mColorItemContent = mCurrentColorScheme.getAccent1().getS800(); // A1-800
- mColorSeekbarProgress = mCurrentColorScheme.getAccent1().getS300(); // A1-300
- mColorButtonBackground = mCurrentColorScheme.getAccent1().getS600(); // A1-600
- mColorItemBackground = mCurrentColorScheme.getAccent2().getS50(); // A2-50
- mColorConnectedItemBackground = mCurrentColorScheme.getAccent1().getS100(); // A1-100
- mColorPositiveButtonText = mCurrentColorScheme.getNeutral1().getS50(); // N1-50
- mColorDialogBackground = mCurrentColorScheme.getBackgroundColor();
- }
+ mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromDynamicColors(
+ currentColorScheme, isDarkTheme);
+ }
+
+ MediaOutputColorSchemeLegacy getColorSchemeLegacy() {
+ return mMediaOutputColorSchemeLegacy;
}
public void refreshDataSetIfNeeded() {
@@ -598,44 +575,16 @@ public class MediaSwitchingController
}
}
- public int getColorConnectedItemBackground() {
- return mColorConnectedItemBackground;
- }
-
- public int getColorPositiveButtonText() {
- return mColorPositiveButtonText;
- }
-
- public int getColorDialogBackground() {
- return mColorDialogBackground;
- }
-
- public int getColorItemContent() {
- return mColorItemContent;
- }
-
- public int getColorSeekbarProgress() {
- return mColorSeekbarProgress;
- }
-
- public int getColorButtonBackground() {
- return mColorButtonBackground;
- }
-
- public int getColorItemBackground() {
- return mColorItemBackground;
- }
-
private void buildMediaItems(List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
- List<MediaItem> updatedMediaItems = buildMediaItems(mOutputMediaItemList, devices);
- mOutputMediaItemList.clear();
- mOutputMediaItemList.addAll(updatedMediaItems);
+ List<MediaItem> updatedMediaItems =
+ buildMediaItems(mOutputMediaItemListProxy.getOutputMediaItemList(), devices);
+ mOutputMediaItemListProxy.clearAndAddAll(updatedMediaItems);
}
}
- protected List<MediaItem> buildMediaItems(List<MediaItem> oldMediaItems,
- List<MediaDevice> devices) {
+ protected List<MediaItem> buildMediaItems(
+ List<MediaItem> oldMediaItems, List<MediaDevice> devices) {
synchronized (mMediaDevicesLock) {
if (!mLocalMediaManager.isPreferenceRouteListingExist()) {
attachRangeInfo(devices);
@@ -743,7 +692,8 @@ public class MediaSwitchingController
* list.
*/
@GuardedBy("mMediaDevicesLock")
- private List<MediaItem> categorizeMediaItemsLocked(MediaDevice connectedMediaDevice,
+ private List<MediaItem> categorizeMediaItemsLocked(
+ MediaDevice connectedMediaDevice,
List<MediaDevice> devices,
boolean needToHandleMutingExpectedDevice) {
List<MediaItem> finalMediaItems = new ArrayList<>();
@@ -802,6 +752,14 @@ public class MediaSwitchingController
}
private void attachConnectNewDeviceItemIfNeeded(List<MediaItem> mediaItems) {
+ MediaItem connectNewDeviceItem = getConnectNewDeviceItem();
+ if (connectNewDeviceItem != null) {
+ mediaItems.add(connectNewDeviceItem);
+ }
+ }
+
+ @Nullable
+ private MediaItem getConnectNewDeviceItem() {
boolean isSelectedDeviceNotAGroup = getSelectedMediaDevice().size() == 1;
if (enableInputRouting()) {
// When input routing is enabled, there are expected to be at least 2 total selected
@@ -810,9 +768,9 @@ public class MediaSwitchingController
}
// Attach "Connect a device" item only when current output is not remote and not a group
- if (!isCurrentConnectedDeviceRemote() && isSelectedDeviceNotAGroup) {
- mediaItems.add(MediaItem.createPairNewDeviceMediaItem());
- }
+ return (!isCurrentConnectedDeviceRemote() && isSelectedDeviceNotAGroup)
+ ? MediaItem.createPairNewDeviceMediaItem()
+ : null;
}
private void attachRangeInfo(List<MediaDevice> devices) {
@@ -901,13 +859,13 @@ public class MediaSwitchingController
mediaItems.add(
MediaItem.createGroupDividerMediaItem(
mContext.getString(R.string.media_output_group_title)));
- mediaItems.addAll(mOutputMediaItemList);
+ mediaItems.addAll(mOutputMediaItemListProxy.getOutputMediaItemList());
}
public List<MediaItem> getMediaItemList() {
// If input routing is not enabled, only return output media items.
if (!enableInputRouting()) {
- return mOutputMediaItemList;
+ return mOutputMediaItemListProxy.getOutputMediaItemList();
}
// If input routing is enabled, return both output and input media items.
@@ -1013,7 +971,7 @@ public class MediaSwitchingController
public boolean isAnyDeviceTransferring() {
synchronized (mMediaDevicesLock) {
- for (MediaItem mediaItem : mOutputMediaItemList) {
+ for (MediaItem mediaItem : mOutputMediaItemListProxy.getOutputMediaItemList()) {
if (mediaItem.getMediaDevice().isPresent()
&& mediaItem.getMediaDevice().get().getState()
== LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
@@ -1053,8 +1011,11 @@ public class MediaSwitchingController
startActivity(launchIntent, controller);
}
- void launchLeBroadcastNotifyDialog(View mediaOutputDialog, BroadcastSender broadcastSender,
- BroadcastNotifyDialog action, final DialogInterface.OnClickListener listener) {
+ void launchLeBroadcastNotifyDialog(
+ View mediaOutputDialog,
+ BroadcastSender broadcastSender,
+ BroadcastNotifyDialog action,
+ final DialogInterface.OnClickListener listener) {
final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
switch (action) {
case ACTION_FIRST_LAUNCH:
@@ -1097,7 +1058,7 @@ public class MediaSwitchingController
mVolumePanelGlobalStateInteractor,
mUserTracker);
MediaOutputBroadcastDialog dialog = new MediaOutputBroadcastDialog(mContext, true,
- broadcastSender, controller);
+ broadcastSender, controller, mMainExecutor, mBackgroundExecutor);
dialog.show();
}
@@ -1284,8 +1245,8 @@ public class MediaSwitchingController
return !sourceList.isEmpty();
}
- boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant(BluetoothDevice sink,
- BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) {
+ boolean addSourceIntoSinkDeviceWithBluetoothLeAssistant(
+ BluetoothDevice sink, BluetoothLeBroadcastMetadata metadata, boolean isGroupOp) {
LocalBluetoothLeBroadcastAssistant assistant =
mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastAssistantProfile();
if (assistant == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
new file mode 100644
index 000000000000..1c9c0b102cb7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/OutputMediaItemListProxy.java
@@ -0,0 +1,55 @@
+/*
+ * 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.systemui.media.dialog;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/** A proxy of holding the list of Output Switcher's output media items. */
+public class OutputMediaItemListProxy {
+ private final List<MediaItem> mOutputMediaItemList;
+
+ public OutputMediaItemListProxy() {
+ mOutputMediaItemList = new CopyOnWriteArrayList<>();
+ }
+
+ /** Returns the list of output media items. */
+ public List<MediaItem> getOutputMediaItemList() {
+ return mOutputMediaItemList;
+ }
+
+ /** Updates the list of output media items with the given list. */
+ public void clearAndAddAll(List<MediaItem> updatedMediaItems) {
+ mOutputMediaItemList.clear();
+ mOutputMediaItemList.addAll(updatedMediaItems);
+ }
+
+ /** Removes the media items with muting expected devices. */
+ public void removeMutingExpectedDevices() {
+ mOutputMediaItemList.removeIf((MediaItem::isMutingExpectedDevice));
+ }
+
+ /** Clears the output media item list. */
+ public void clear() {
+ mOutputMediaItemList.clear();
+ }
+
+ /** Returns whether the output media item list is empty. */
+ public boolean isEmpty() {
+ return mOutputMediaItemList.isEmpty();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
index 9c6568057d6f..9eb55a8eff2e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
@@ -37,6 +37,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.hoverable
+import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -44,7 +45,6 @@ import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -65,6 +65,7 @@ import androidx.compose.material3.SliderColors
import androidx.compose.material3.SliderDefaults.colors
import androidx.compose.material3.SliderState
import androidx.compose.material3.Text
+import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
@@ -376,29 +377,32 @@ private fun ContentScope.CardForegroundContent(
) {
Column(
modifier =
- modifier
- .combinedClickable(
- onClick = viewModel.onClick,
- onLongClick = viewModel.onLongClick,
- onClickLabel = viewModel.onClickLabel,
- )
- .padding(16.dp)
+ modifier.combinedClickable(
+ onClick = viewModel.onClick,
+ onLongClick = viewModel.onLongClick,
+ onClickLabel = viewModel.onClickLabel,
+ )
) {
// Always add the first/top row, regardless of presentation style.
- Row(verticalAlignment = Alignment.CenterVertically) {
+ Box(modifier = Modifier.fillMaxWidth()) {
// Icon.
Icon(
icon = viewModel.icon,
tint = colorScheme.primary,
- modifier = Modifier.size(24.dp).clip(CircleShape),
+ modifier =
+ Modifier.align(Alignment.TopStart)
+ .padding(top = 16.dp, start = 16.dp)
+ .size(24.dp)
+ .clip(CircleShape),
)
- Spacer(modifier = Modifier.weight(1f))
- viewModel.outputSwitcherChips.fastForEach { chip ->
- OutputSwitcherChip(
- viewModel = chip,
- colorScheme = colorScheme,
- modifier = Modifier.padding(start = 8.dp),
- )
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.align(Alignment.TopEnd),
+ ) {
+ viewModel.outputSwitcherChips.fastForEach { chip ->
+ OutputSwitcherChip(viewModel = chip, colorScheme = colorScheme)
+ }
}
}
@@ -415,7 +419,7 @@ private fun ContentScope.CardForegroundContent(
// Second row.
Row(
verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(top = 16.dp),
+ modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp),
) {
Metadata(
title = viewModel.title,
@@ -441,7 +445,7 @@ private fun ContentScope.CardForegroundContent(
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(top = 24.dp),
+ modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp, bottom = 16.dp),
) {
Navigation(
viewModel = viewModel.navigation,
@@ -464,7 +468,7 @@ private fun ContentScope.CardForegroundContent(
// Bottom row.
Row(
verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.padding(top = 36.dp),
+ modifier = Modifier.padding(start = 16.dp, top = 36.dp, end = 16.dp, bottom = 16.dp),
) {
Metadata(
title = viewModel.title,
@@ -914,20 +918,48 @@ private fun OutputSwitcherChip(
colorScheme: AnimatedColorScheme,
modifier: Modifier = Modifier,
) {
- PlatformButton(
- onClick = viewModel.onClick,
- colors = ButtonDefaults.buttonColors(containerColor = colorScheme.primary),
- contentPadding = PaddingValues(start = 8.dp, end = 12.dp, top = 4.dp, bottom = 4.dp),
- modifier = modifier.height(24.dp),
+ // For accessibility reasons, the touch area for the chip needs to be at least 48dp in height.
+ // At the same time, the rounded corner chip should only be as tall as it needs to be to contain
+ // its contents and look like a nice design; also, the ripple effect should only be shown within
+ // the bounds of the chip.
+ //
+ // This is achieved by sharing this InteractionSource between the outer and inner composables.
+ //
+ // The outer composable hosts that clickable that writes user events into the InteractionSource.
+ // The inner composable consumes the user events from the InteractionSource and feeds them into
+ // its indication.
+ val clickInteractionSource = remember { MutableInteractionSource() }
+ Box(
+ modifier =
+ modifier
+ .height(48.dp)
+ .clickable(interactionSource = clickInteractionSource, indication = null) {
+ viewModel.onClick()
+ }
+ .padding(top = 16.dp, end = 16.dp, bottom = 8.dp)
) {
- Icon(icon = viewModel.icon, tint = colorScheme.onPrimary, modifier = Modifier.size(16.dp))
- viewModel.text?.let {
- Spacer(Modifier.size(4.dp))
- Text(
- text = viewModel.text,
- style = MaterialTheme.typography.bodySmall,
- color = colorScheme.onPrimary,
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ modifier =
+ Modifier.clip(RoundedCornerShape(12.dp))
+ .background(colorScheme.primary)
+ .indication(clickInteractionSource, ripple())
+ .padding(start = 8.dp, end = 12.dp, top = 4.dp, bottom = 4.dp),
+ ) {
+ Icon(
+ icon = viewModel.icon,
+ tint = colorScheme.onPrimary,
+ modifier = Modifier.size(16.dp),
)
+
+ viewModel.text?.let {
+ Text(
+ text = viewModel.text,
+ style = MaterialTheme.typography.bodySmall,
+ color = colorScheme.onPrimary,
+ )
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index d8fc52bcc55a..8dc27bf4ac3e 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -162,10 +162,6 @@ constructor(
): Boolean {
return this@NoteTaskInitializer.handleKeyGestureEvent(event)
}
-
- override fun isKeyGestureSupported(gestureType: Int): Boolean {
- return this@NoteTaskInitializer.isKeyGestureSupported(gestureType)
- }
}
/**
@@ -225,10 +221,6 @@ constructor(
return true
}
- private fun isKeyGestureSupported(gestureType: Int): Boolean {
- return gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES
- }
-
companion object {
val MULTI_PRESS_TIMEOUT = ViewConfiguration.getMultiPressTimeout().toLong()
val LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout().toLong()
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 ef57978584b9..8920c86282da 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -747,22 +747,24 @@ constructor(
val BrightnessSlider =
@Composable {
AlwaysDarkMode {
- BrightnessSliderContainer(
- viewModel = containerViewModel.brightnessSliderViewModel,
- containerColors =
- ContainerColors(
- Color.Transparent,
- ContainerColors.defaultContainerColor,
- ),
- modifier =
- Modifier.systemGestureExclusionInShade(
- enabled = {
- layoutState.transitionState is
- TransitionState.Idle
- }
- )
- .fillMaxWidth(),
- )
+ Box(
+ Modifier.systemGestureExclusionInShade(
+ enabled = {
+ layoutState.transitionState is TransitionState.Idle
+ }
+ )
+ ) {
+ BrightnessSliderContainer(
+ viewModel =
+ containerViewModel.brightnessSliderViewModel,
+ containerColors =
+ ContainerColors(
+ Color.Transparent,
+ ContainerColors.defaultContainerColor,
+ ),
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
}
}
val TileGrid =
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 701f44e9981c..d40ecc9565ae 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
@@ -61,6 +61,9 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode
DisposableEffect(Unit) { onDispose { detailsViewModel.closeDetailedView() } }
+ val title = tileDetailedViewModel.title
+ val subTitle = tileDetailedViewModel.subTitle
+
Column(
modifier =
modifier
@@ -90,7 +93,7 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode
)
}
Text(
- text = tileDetailedViewModel.getTitle(),
+ text = title,
modifier = Modifier.align(Alignment.CenterVertically),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge,
@@ -110,7 +113,7 @@ fun TileDetails(modifier: Modifier = Modifier, detailsViewModel: DetailsViewMode
}
}
Text(
- text = tileDetailedViewModel.getSubTitle(),
+ text = subTitle,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleSmall,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
index 153238fc91c9..a66b51f6fe50 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt
@@ -59,12 +59,14 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.toSize
import androidx.compose.ui.zIndex
import com.android.compose.modifiers.size
import com.android.compose.modifiers.thenIf
import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BADGE_ANGLE_RAD
+import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeIconSize
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeSize
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeXOffset
import com.android.systemui.qs.panels.ui.compose.selection.SelectionDefaults.BadgeYOffset
@@ -149,16 +151,14 @@ fun InteractiveTileContainer(
onClick = onClick,
)
) {
+ val size = with(LocalDensity.current) { BadgeIconSize.toDp() }
Icon(
Icons.Default.Remove,
contentDescription = null,
modifier =
- Modifier.size(
- width = { decorationSize.width.roundToInt() },
- height = { decorationSize.height.roundToInt() },
- )
- .align(Alignment.Center)
- .graphicsLayer { this.alpha = badgeIconAlpha },
+ Modifier.size(size).align(Alignment.Center).graphicsLayer {
+ this.alpha = badgeIconAlpha
+ },
)
}
}
@@ -219,12 +219,13 @@ fun StaticTileBadge(
}
) {
val secondaryColor = MaterialTheme.colorScheme.secondary
+ val size = with(LocalDensity.current) { BadgeIconSize.toDp() }
Icon(
icon,
contentDescription = contentDescription,
modifier =
- Modifier.size(BadgeSize).align(Alignment.Center).drawBehind {
- drawCircle(secondaryColor)
+ Modifier.size(size).align(Alignment.Center).drawBehind {
+ drawCircle(secondaryColor, radius = BadgeSize.toPx() / 2)
},
)
}
@@ -338,6 +339,7 @@ private fun offsetForAngle(angle: Float, radius: Float, center: Offset): Offset
private object SelectionDefaults {
val SelectedBorderWidth = 2.dp
val BadgeSize = 24.dp
+ val BadgeIconSize = 16.sp
val BadgeXOffset = -4.dp
val BadgeYOffset = 4.dp
val ResizingPillWidth = 8.dp
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt
index 99f52c28a137..3ae90d2f976b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt
@@ -16,14 +16,20 @@
package com.android.systemui.qs.panels.ui.compose.toolbar
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.CornerSize
+import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.development.ui.compose.BuildNumber
import com.android.systemui.qs.footer.ui.compose.IconButton
import com.android.systemui.qs.panels.ui.viewmodel.toolbar.ToolbarViewModel
+import com.android.systemui.qs.ui.compose.borderOnFocus
@Composable
fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) {
@@ -44,7 +50,18 @@ fun Toolbar(viewModel: ToolbarViewModel, modifier: Modifier = Modifier) {
Modifier.sysuiResTag("settings_button_container"),
)
- Spacer(modifier = Modifier.weight(1f))
+ Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.Center) {
+ BuildNumber(
+ viewModelFactory = viewModel.buildNumberViewModelFactory,
+ textColor = MaterialTheme.colorScheme.onSurface,
+ modifier =
+ Modifier.borderOnFocus(
+ color = MaterialTheme.colorScheme.secondary,
+ cornerSize = CornerSize(1.dp),
+ )
+ .wrapContentSize(),
+ )
+ }
IconButton(
{ viewModel.powerButtonViewModel },
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt
index e54bfa29d2db..10d7871b8ea2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModel.kt
@@ -24,6 +24,7 @@ import androidx.compose.runtime.setValue
import com.android.systemui.animation.Expandable
import com.android.systemui.classifier.domain.interactor.FalsingInteractor
import com.android.systemui.classifier.domain.interactor.runIfNotFalseTap
+import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel
import com.android.systemui.globalactions.GlobalActionsDialogLite
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
@@ -46,6 +47,7 @@ class ToolbarViewModel
@AssistedInject
constructor(
editModeButtonViewModelFactory: EditModeButtonViewModel.Factory,
+ val buildNumberViewModelFactory: BuildNumberViewModel.Factory,
private val footerActionsInteractor: FooterActionsInteractor,
private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>,
private val falsingInteractor: FalsingInteractor,
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
index 7d396c58630e..8ffba1e5f3dd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContent.kt
@@ -19,26 +19,14 @@ 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(),
@@ -46,11 +34,11 @@ fun InternetDetailsContent(viewModel: InternetDetailsViewModel) {
// 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)
+ viewModel.internetDetailsContentManager.bind(view, coroutineScope)
view
// TODO: b/377388104 - Polish the internet details view UI
},
- onRelease = { internetDetailsContentManager.unBind() },
+ onRelease = { viewModel.internetDetailsContentManager.unBind() },
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
index 659488bdd0d3..d8e1755e6cca 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManager.kt
@@ -43,6 +43,9 @@ import android.widget.Switch
import android.widget.TextView
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
@@ -79,8 +82,6 @@ constructor(
private val internetDetailsContentController: InternetDetailsContentController,
@Assisted(CAN_CONFIG_MOBILE_DATA) private val canConfigMobileData: Boolean,
@Assisted(CAN_CONFIG_WIFI) private val canConfigWifi: Boolean,
- @Assisted private val coroutineScope: CoroutineScope,
- @Assisted private var context: Context,
private val uiEventLogger: UiEventLogger,
@Main private val handler: Handler,
@Background private val backgroundExecutor: Executor,
@@ -121,26 +122,29 @@ constructor(
private lateinit var shareWifiButton: Button
private lateinit var airplaneModeButton: Button
private var alertDialog: AlertDialog? = null
-
- private val canChangeWifiState =
- WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context)
+ private var canChangeWifiState = false
private var wifiNetworkHeight = 0
private var backgroundOn: Drawable? = null
private var backgroundOff: Drawable? = null
private var clickJob: Job? = null
private var defaultDataSubId = internetDetailsContentController.defaultDataSubscriptionId
- @VisibleForTesting
- internal var adapter = InternetAdapter(internetDetailsContentController, coroutineScope)
+ @VisibleForTesting internal lateinit var adapter: InternetAdapter
@VisibleForTesting internal var wifiEntriesCount: Int = 0
@VisibleForTesting internal var hasMoreWifiEntries: Boolean = false
+ private lateinit var context: Context
+ private lateinit var coroutineScope: CoroutineScope
+
+ var title by mutableStateOf("")
+ private set
+
+ var subTitle by mutableStateOf("")
+ private set
@AssistedFactory
interface Factory {
fun create(
@Assisted(CAN_CONFIG_MOBILE_DATA) canConfigMobileData: Boolean,
@Assisted(CAN_CONFIG_WIFI) canConfigWifi: Boolean,
- coroutineScope: CoroutineScope,
- context: Context,
): InternetDetailsContentManager
}
@@ -152,12 +156,16 @@ constructor(
*
* @param contentView The view to which the content manager should be bound.
*/
- fun bind(contentView: View) {
+ fun bind(contentView: View, coroutineScope: CoroutineScope) {
if (DEBUG) {
Log.d(TAG, "Bind InternetDetailsContentManager")
}
this.contentView = contentView
+ context = contentView.context
+ this.coroutineScope = coroutineScope
+ adapter = InternetAdapter(internetDetailsContentController, coroutineScope)
+ canChangeWifiState = WifiEnterpriseRestrictionUtils.isChangeWifiStateAllowed(context)
initializeLifecycle()
initializeViews()
@@ -323,11 +331,11 @@ constructor(
}
}
- fun getTitleText(): String {
+ private fun getTitleText(): String {
return internetDetailsContentController.getDialogTitleText().toString()
}
- fun getSubtitleText(): String {
+ private fun getSubtitleText(): String {
return internetDetailsContentController.getSubtitleText(isProgressBarVisible).toString()
}
@@ -336,6 +344,13 @@ constructor(
Log.d(TAG, "updateDetailsUI ")
}
+ if (!::context.isInitialized) {
+ return
+ }
+
+ title = getTitleText()
+ subTitle = getSubtitleText()
+
airplaneModeButton.visibility =
if (internetContent.isAirplaneModeEnabled) View.VISIBLE else View.GONE
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 6709fd2bb508..fb63bea4fb9f 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
@@ -28,38 +28,27 @@ class InternetDetailsViewModel
@AssistedInject
constructor(
private val accessPointController: AccessPointController,
- val contentManagerFactory: InternetDetailsContentManager.Factory,
+ private val contentManagerFactory: InternetDetailsContentManager.Factory,
private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
-) : TileDetailsViewModel() {
- override fun clickOnSettingsButton() {
- qsTileIntentUserActionHandler.handle(
- /* expandable= */ null,
- Intent(Settings.ACTION_WIFI_SETTINGS),
+) : TileDetailsViewModel {
+ val internetDetailsContentManager by lazy {
+ contentManagerFactory.create(
+ canConfigMobileData = accessPointController.canConfigMobileData(),
+ canConfigWifi = accessPointController.canConfigWifi(),
)
}
- override fun getTitle(): String {
- // TODO: b/377388104 make title and sub title mutable states of string
- // by internetDetailsContentManager.getTitleText()
- // TODO: test title change between airplane mode and not airplane mode
- // TODO: b/377388104 Update the placeholder text
- return "Internet"
- }
+ override val title: String
+ get() = internetDetailsContentManager.title
- override fun getSubTitle(): String {
- // TODO: b/377388104 make title and sub title mutable states of string
- // by internetDetailsContentManager.getSubtitleText()
- // TODO: test subtitle change between airplane mode and not airplane mode
- // TODO: b/377388104 Update the placeholder text
- return "Tab a network to connect"
- }
+ override val subTitle: String
+ get() = internetDetailsContentManager.subTitle
- fun getCanConfigMobileData(): Boolean {
- return accessPointController.canConfigMobileData()
- }
-
- fun getCanConfigWifi(): Boolean {
- return accessPointController.canConfigWifi()
+ override fun clickOnSettingsButton() {
+ qsTileIntentUserActionHandler.handle(
+ /* expandable= */ null,
+ Intent(Settings.ACTION_WIFI_SETTINGS),
+ )
}
@AssistedFactory
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 9a39c3c095ef..4f7e03bd3bc3 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
@@ -23,18 +23,14 @@ import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogView
class ModesDetailsViewModel(
private val onSettingsClick: () -> Unit,
val viewModel: ModesDialogViewModel,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
override fun clickOnSettingsButton() {
onSettingsClick()
}
- override fun getTitle(): String {
- // TODO(b/388321032): Replace this string with a string in a translatable xml file.
- return "Modes"
- }
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file.
+ override val title = "Modes"
- override fun getSubTitle(): String {
- // TODO(b/388321032): Replace this string with a string in a translatable xml file.
- return "Silences interruptions from people and apps in different circumstances"
- }
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file.
+ override val subTitle = "Silences interruptions from people and apps in different circumstances"
}
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 c84ddb6fdb36..59f209edb546 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
@@ -23,19 +23,15 @@ import com.android.systemui.screenrecord.RecordingController
class ScreenRecordDetailsViewModel(
val recordingController: RecordingController,
val onStartRecordingClicked: Runnable,
-) : TileDetailsViewModel() {
+) : TileDetailsViewModel {
override fun clickOnSettingsButton() {
// No settings button in this tile.
}
- override fun getTitle(): String {
- // TODO(b/388321032): Replace this string with a string in a translatable xml file,
- return "Screen recording"
- }
+ // TODO(b/388321032): Replace this string with a string in a translatable xml file,
+ override val title = "Screen recording"
- override fun getSubTitle(): String {
- // No sub-title in this tile.
- return ""
- }
+ // No sub-title in this tile.
+ override val subTitle = ""
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt
index 5e7172ee3ba7..268efcef9062 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/interactor/HearingDevicesTileUserActionInteractor.kt
@@ -21,6 +21,7 @@ import android.provider.Settings
import com.android.systemui.accessibility.hearingaid.HearingDevicesDialogManager
import com.android.systemui.accessibility.hearingaid.HearingDevicesUiEventLogger.Companion.LAUNCH_SOURCE_QS_TILE
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.shared.QSSettingsPackageRepository
import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler
import com.android.systemui.qs.tiles.base.interactor.QSTileInput
import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
@@ -37,6 +38,7 @@ constructor(
@Main private val mainContext: CoroutineContext,
private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler,
private val hearingDevicesDialogManager: HearingDevicesDialogManager,
+ private val settingsPackageRepository: QSSettingsPackageRepository,
) : QSTileUserActionInteractor<HearingDevicesTileModel> {
override suspend fun handleInput(input: QSTileInput<HearingDevicesTileModel>) =
@@ -53,7 +55,8 @@ constructor(
is QSTileUserAction.LongClick -> {
qsTileIntentUserActionHandler.handle(
action.expandable,
- Intent(Settings.ACTION_HEARING_DEVICES_SETTINGS),
+ Intent(Settings.ACTION_HEARING_DEVICES_SETTINGS)
+ .setPackage(settingsPackageRepository.getSettingsPackageName()),
)
}
is QSTileUserAction.ToggleClick -> {}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 3be2f1b7b957..362b5db012e1 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -17,6 +17,7 @@
package com.android.systemui.shade
import android.content.Context
+import android.content.res.Configuration
import android.graphics.Rect
import android.os.PowerManager
import android.os.SystemClock
@@ -25,11 +26,13 @@ import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
+import android.view.WindowInsets
import android.widget.FrameLayout
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
import androidx.compose.ui.platform.ComposeView
+import androidx.core.view.updateMargins
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleObserver
@@ -101,7 +104,10 @@ constructor(
) : LifecycleOwner {
private val logger = Logger(logBuffer, TAG)
- private class CommunalWrapper(context: Context) : FrameLayout(context) {
+ private class CommunalWrapper(
+ context: Context,
+ private val communalSettingsInteractor: CommunalSettingsInteractor,
+ ) : FrameLayout(context) {
private val consumers: MutableSet<Consumer<Boolean>> = ArraySet()
override fun requestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {
@@ -121,6 +127,24 @@ constructor(
consumers.clear()
}
}
+
+ override fun onApplyWindowInsets(windowInsets: WindowInsets): WindowInsets {
+ if (
+ !communalSettingsInteractor.isV2FlagEnabled() ||
+ resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
+ ) {
+ return super.onApplyWindowInsets(windowInsets)
+ }
+ val type = WindowInsets.Type.displayCutout()
+ val insets = windowInsets.getInsets(type)
+
+ // Reset horizontal margins added by window insets, so hub can be edge to edge.
+ if (insets.left > 0 || insets.right > 0) {
+ val lp = layoutParams as LayoutParams
+ lp.updateMargins(0, lp.topMargin, 0, lp.bottomMargin)
+ }
+ return WindowInsets.CONSUMED
+ }
}
/** The container view for the hub. This will not be initialized until [initView] is called. */
@@ -443,7 +467,8 @@ constructor(
collectFlow(containerView, keyguardInteractor.isDreaming, { isDreaming = it })
collectFlow(containerView, communalViewModel.swipeToHubEnabled, { swipeToHubEnabled = it })
- communalContainerWrapper = CommunalWrapper(containerView.context)
+ communalContainerWrapper =
+ CommunalWrapper(containerView.context, communalSettingsInteractor)
communalContainerWrapper?.addView(communalContainerView)
logger.d("Hub container initialized")
return communalContainerWrapper!!
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index fa17b4fad592..dafb1a559d59 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -498,17 +498,18 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW
}
private boolean isExpanded(NotificationShadeWindowState state) {
+ boolean visForBlur = !Flags.disableShadeVisibleWithBlur() && state.backgroundBlurRadius > 0;
boolean isExpanded = !state.forceWindowCollapsed && (state.isKeyguardShowingAndNotOccluded()
|| state.panelVisible || state.keyguardFadingAway || state.bouncerShowing
|| state.headsUpNotificationShowing
|| state.scrimsVisibility != ScrimController.TRANSPARENT)
- || state.backgroundBlurRadius > 0
+ || visForBlur
|| state.launchingActivityFromNotification;
mLogger.logIsExpanded(isExpanded, state.forceWindowCollapsed,
state.isKeyguardShowingAndNotOccluded(), state.panelVisible,
state.keyguardFadingAway, state.bouncerShowing, state.headsUpNotificationShowing,
state.scrimsVisibility != ScrimController.TRANSPARENT,
- state.backgroundBlurRadius > 0, state.launchingActivityFromNotification);
+ visForBlur, state.launchingActivityFromNotification);
return isExpanded;
}
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 20b44d73e097..5609326362fc 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
@@ -26,6 +26,7 @@ import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import com.android.app.tracing.coroutines.launchTraced as launch
+import com.android.compose.animation.scene.OverlayKey
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
@@ -86,6 +87,22 @@ constructor(
(ViewGroup, StatusBarLocation) -> BatteryMeterViewController =
batteryMeterViewControllerFactory::create
+ val showClock: Boolean by
+ hydrator.hydratedStateOf(
+ traceName = "showClock",
+ initialValue =
+ shouldShowClock(
+ isShadeLayoutWide = shadeInteractor.isShadeLayoutWide.value,
+ overlays = sceneInteractor.currentOverlays.value,
+ ),
+ source =
+ combine(
+ shadeInteractor.isShadeLayoutWide,
+ sceneInteractor.currentOverlays,
+ ::shouldShowClock,
+ ),
+ )
+
val notificationsChipHighlight: HeaderChipHighlight by
hydrator.hydratedStateOf(
traceName = "notificationsChipHighlight",
@@ -114,13 +131,6 @@ constructor(
},
)
- val isShadeLayoutWide: Boolean by
- hydrator.hydratedStateOf(
- traceName = "isShadeLayoutWide",
- initialValue = shadeInteractor.isShadeLayoutWide.value,
- source = shadeInteractor.isShadeLayoutWide,
- )
-
/** True if there is exactly one mobile connection. */
val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier
@@ -271,6 +281,11 @@ constructor(
}
}
+ private fun shouldShowClock(isShadeLayoutWide: Boolean, overlays: Set<OverlayKey>): Boolean {
+ // Notifications shade on narrow layout renders its own clock. Hide the header clock.
+ return isShadeLayoutWide || Overlays.NotificationsShade !in overlays
+ }
+
private fun getFormatFromPattern(pattern: String?): DateFormat {
val format = DateFormat.getInstanceForSkeleton(pattern, Locale.getDefault())
format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
index c2e355d07e9c..03c191e40ccf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
@@ -22,6 +22,7 @@ import android.app.Flags;
import android.app.Notification;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
+import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.TypedValue;
@@ -31,6 +32,8 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.VisibleForTesting;
+
import com.android.internal.R;
import com.android.internal.widget.CachingIconView;
import com.android.internal.widget.ConversationLayout;
@@ -39,6 +42,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.NotificationContentView;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import java.util.ArrayList;
import java.util.HashSet;
@@ -214,7 +218,7 @@ public class NotificationGroupingUtil {
}
// in case no view is visible we make sure the time is visible
int timeVisibility = !hasVisibleText
- || row.getEntry().getSbn().getNotification().showsTime()
+ || showsTime(row)
? View.VISIBLE : View.GONE;
time.setVisibility(timeVisibility);
View left = null;
@@ -243,6 +247,17 @@ public class NotificationGroupingUtil {
}
}
+ @VisibleForTesting
+ boolean showsTime(ExpandableNotificationRow row) {
+ StatusBarNotification sbn;
+ if (NotificationBundleUi.isEnabled()) {
+ sbn = row.getEntryAdapter() != null ? row.getEntryAdapter().getSbn() : null;
+ } else {
+ sbn = row.getEntry().getSbn();
+ }
+ return (sbn != null && sbn.getNotification().showsTime());
+ }
+
/**
* Reset the modifications to this row for removing it from the group.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index 0d34bdc7e477..041ed6504634 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -59,11 +59,13 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor;
import com.android.systemui.statusbar.dagger.CentralSurfacesDependenciesModule;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.RemoteInputControllerLogger;
+import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.statusbar.phone.ExpandHeadsUpOnInlineReply;
import com.android.systemui.statusbar.policy.RemoteInputUriController;
import com.android.systemui.statusbar.policy.RemoteInputView;
@@ -134,20 +136,21 @@ public class NotificationRemoteInputManager implements CoreStartable {
view.getTag(com.android.internal.R.id.notification_action_index_tag);
final ExpandableNotificationRow row = getNotificationRowForParent(view.getParent());
- final NotificationEntry entry = getNotificationForParent(view.getParent());
- mLogger.logInitialClick(
- row != null ? row.getLoggingKey() : null, actionIndex, pendingIntent);
+ if (row == null) {
+ return false;
+ }
+ mLogger.logInitialClick(row.getLoggingKey(), actionIndex, pendingIntent);
if (handleRemoteInput(view, pendingIntent)) {
- mLogger.logRemoteInputWasHandled(
- row != null ? row.getLoggingKey() : null, actionIndex);
+ mLogger.logRemoteInputWasHandled(row.getLoggingKey(), actionIndex);
return true;
}
if (DEBUG) {
Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
}
- logActionClick(view, entry, pendingIntent);
+ Notification.Action action = getActionFromView(view, row, pendingIntent);
+ logActionClick(view, row.getKey(), action);
// The intent we are sending is for the application, which
// won't have permission to immediately start an activity after
// the user switches to home. We know it is safe to do at this
@@ -156,33 +159,47 @@ public class NotificationRemoteInputManager implements CoreStartable {
ActivityManager.getService().resumeAppSwitches();
} catch (RemoteException e) {
}
- Notification.Action action = getActionFromView(view, entry, pendingIntent);
return mCallback.handleRemoteViewClick(view, pendingIntent,
action == null ? false : action.isAuthenticationRequired(), actionIndex, () -> {
Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
mLogger.logStartingIntentWithDefaultHandler(
- row != null ? row.getLoggingKey() : null, pendingIntent, actionIndex);
+ row.getLoggingKey(), pendingIntent, actionIndex);
boolean started = RemoteViews.startPendingIntent(view, pendingIntent, options);
- if (started) releaseNotificationIfKeptForRemoteInputHistory(entry);
+ if (started) {
+ if (NotificationBundleUi.isEnabled()) {
+ releaseNotificationIfKeptForRemoteInputHistory(row.getEntryAdapter());
+ } else {
+ releaseNotificationIfKeptForRemoteInputHistory(row.getEntry());
+ }
+ }
return started;
});
}
private @Nullable Notification.Action getActionFromView(View view,
- NotificationEntry entry, PendingIntent actionIntent) {
+ ExpandableNotificationRow row, PendingIntent actionIntent) {
Integer actionIndex = (Integer)
view.getTag(com.android.internal.R.id.notification_action_index_tag);
if (actionIndex == null) {
return null;
}
- if (entry == null) {
+ StatusBarNotification statusBarNotification = null;
+ if (NotificationBundleUi.isEnabled()) {
+ if (row.getEntryAdapter() != null) {
+ statusBarNotification = row.getEntryAdapter().getSbn();
+ }
+ } else {
+ if (row.getEntry() != null) {
+ statusBarNotification = row.getEntry().getSbn();
+ }
+ }
+ if (statusBarNotification == null) {
Log.w(TAG, "Couldn't determine notification for click.");
return null;
}
// Notification may be updated before this function is executed, and thus play safe
// here and verify that the action object is still the one that where the click happens.
- StatusBarNotification statusBarNotification = entry.getSbn();
Notification.Action[] actions = statusBarNotification.getNotification().actions;
if (actions == null || actionIndex >= actions.length) {
Log.w(TAG, "statusBarNotification.getNotification().actions is null or invalid");
@@ -199,14 +216,12 @@ public class NotificationRemoteInputManager implements CoreStartable {
private void logActionClick(
View view,
- NotificationEntry entry,
- PendingIntent actionIntent) {
- Notification.Action action = getActionFromView(view, entry, actionIntent);
+ String key,
+ Notification.Action action) {
if (action == null) {
return;
}
ViewParent parent = view.getParent();
- String key = entry.getSbn().getKey();
int buttonIndex = -1;
// If this is a default template, determine the index of the button.
if (view.getId() == com.android.internal.R.id.action0 &&
@@ -214,20 +229,10 @@ public class NotificationRemoteInputManager implements CoreStartable {
ViewGroup actionGroup = (ViewGroup) parent;
buttonIndex = actionGroup.indexOfChild(view);
}
- final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true);
+ final NotificationVisibility nv = mVisibilityProvider.obtain(key, true);
mClickNotifier.onNotificationActionClick(key, buttonIndex, action, nv, false);
}
- private NotificationEntry getNotificationForParent(ViewParent parent) {
- while (parent != null) {
- if (parent instanceof ExpandableNotificationRow) {
- return ((ExpandableNotificationRow) parent).getEntry();
- }
- parent = parent.getParent();
- }
- return null;
- }
-
private @Nullable ExpandableNotificationRow getNotificationRowForParent(ViewParent parent) {
while (parent != null) {
if (parent instanceof ExpandableNotificationRow) {
@@ -394,11 +399,21 @@ public class NotificationRemoteInputManager implements CoreStartable {
}
}
+ /**
+ * Use {@link com.android.systemui.statusbar.notification.row.NotificationActionClickManager}
+ * instead
+ */
public void addActionPressListener(Consumer<NotificationEntry> listener) {
+ NotificationBundleUi.assertInLegacyMode();
mActionPressListeners.addIfAbsent(listener);
}
+ /**
+ * Use {@link com.android.systemui.statusbar.notification.row.NotificationActionClickManager}
+ * instead
+ */
public void removeActionPressListener(Consumer<NotificationEntry> listener) {
+ NotificationBundleUi.assertInLegacyMode();
mActionPressListeners.remove(listener);
}
@@ -681,12 +696,30 @@ public class NotificationRemoteInputManager implements CoreStartable {
* (after unlock, if applicable), and will then wait a short time to allow the app to update the
* notification in response to the action.
*/
+ private void releaseNotificationIfKeptForRemoteInputHistory(EntryAdapter entryAdapter) {
+ if (entryAdapter == null) {
+ return;
+ }
+ if (mRemoteInputListener != null) {
+ mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(
+ entryAdapter.getKey());
+ }
+ entryAdapter.onNotificationActionClicked();
+ }
+
+ /**
+ * Checks if the notification is being kept due to the user sending an inline reply, and if
+ * so, releases that hold. This is called anytime an action on the notification is dispatched
+ * (after unlock, if applicable), and will then wait a short time to allow the app to update the
+ * notification in response to the action.
+ */
private void releaseNotificationIfKeptForRemoteInputHistory(NotificationEntry entry) {
+ NotificationBundleUi.assertInLegacyMode();
if (entry == null) {
return;
}
if (mRemoteInputListener != null) {
- mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry);
+ mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry.getKey());
}
for (Consumer<NotificationEntry> listener : mActionPressListeners) {
listener.accept(entry);
@@ -866,7 +899,7 @@ public class NotificationRemoteInputManager implements CoreStartable {
boolean isNotificationKeptForRemoteInputHistory(@NonNull String key);
/** Called on user interaction to end lifetime extension for history */
- void releaseNotificationIfKeptForRemoteInputHistory(@NonNull NotificationEntry entry);
+ void releaseNotificationIfKeptForRemoteInputHistory(@NonNull String entryKey);
/** Called when the RemoteInputController is attached to the manager */
void setRemoteInputController(@NonNull RemoteInputController remoteInputController);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt
new file mode 100644
index 000000000000..176419454c21
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsReturnAnimations.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips
+
+import com.android.systemui.Flags
+import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization
+
+/** Helper for reading or using the status_bar_chips_return_animations flag state. */
+object StatusBarChipsReturnAnimations {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_STATUS_BAR_CHIPS_RETURN_ANIMATIONS
+
+ /** Is the feature enabled. */
+ @JvmStatic
+ inline val isEnabled
+ get() = StatusBarChipsModernization.isEnabled && Flags.statusBarChipsReturnAnimations()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
index 16bf43bbf272..7e7031200988 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
@@ -32,6 +32,7 @@ import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad
import com.android.systemui.statusbar.chips.StatusBarChipsLog
+import com.android.systemui.statusbar.chips.StatusBarChipsReturnAnimations
import com.android.systemui.statusbar.chips.call.domain.interactor.CallChipInteractor
import com.android.systemui.statusbar.chips.ui.model.ColorsModel
import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
@@ -43,8 +44,10 @@ import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCall
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -60,25 +63,60 @@ constructor(
private val activityStarter: ActivityStarter,
@StatusBarChipsLog private val logger: LogBuffer,
) : OngoingActivityChipViewModel {
- override val chip: StateFlow<OngoingActivityChipModel> =
- interactor.ongoingCallState
- .map { state ->
- when (state) {
- is OngoingCallModel.NoCall -> OngoingActivityChipModel.Inactive()
- is OngoingCallModel.InCall ->
- if (state.isAppVisible) {
- OngoingActivityChipModel.Inactive()
- } else {
- prepareChip(state, systemClock)
- }
+ private val chipWithReturnAnimation: StateFlow<OngoingActivityChipModel> =
+ if (StatusBarChipsReturnAnimations.isEnabled) {
+ interactor.ongoingCallState
+ .map { state ->
+ when (state) {
+ is OngoingCallModel.NoCall -> OngoingActivityChipModel.Inactive()
+ is OngoingCallModel.InCall ->
+ prepareChip(state, systemClock, isHidden = state.isAppVisible)
+ }
}
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Inactive())
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ OngoingActivityChipModel.Inactive(),
+ )
+ } else {
+ MutableStateFlow(OngoingActivityChipModel.Inactive()).asStateFlow()
+ }
+
+ private val chipLegacy: StateFlow<OngoingActivityChipModel> =
+ if (!StatusBarChipsReturnAnimations.isEnabled) {
+ interactor.ongoingCallState
+ .map { state ->
+ when (state) {
+ is OngoingCallModel.NoCall -> OngoingActivityChipModel.Inactive()
+ is OngoingCallModel.InCall ->
+ if (state.isAppVisible) {
+ OngoingActivityChipModel.Inactive()
+ } else {
+ prepareChip(state, systemClock, isHidden = false)
+ }
+ }
+ }
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ OngoingActivityChipModel.Inactive(),
+ )
+ } else {
+ MutableStateFlow(OngoingActivityChipModel.Inactive()).asStateFlow()
+ }
+
+ override val chip: StateFlow<OngoingActivityChipModel> =
+ if (StatusBarChipsReturnAnimations.isEnabled) {
+ chipWithReturnAnimation
+ } else {
+ chipLegacy
+ }
/** Builds an [OngoingActivityChipModel.Active] from all the relevant information. */
private fun prepareChip(
state: OngoingCallModel.InCall,
systemClock: SystemClock,
+ isHidden: Boolean,
): OngoingActivityChipModel.Active {
val key = state.notificationKey
val contentDescription = getContentDescription(state.appName)
@@ -110,6 +148,7 @@ constructor(
colors = colors,
onClickListenerLegacy = getOnClickListener(state.intent),
clickBehavior = getClickBehavior(state.intent),
+ isHidden = isHidden,
)
} else {
val startTimeInElapsedRealtime =
@@ -121,6 +160,7 @@ constructor(
startTimeMs = startTimeInElapsedRealtime,
onClickListenerLegacy = getOnClickListener(state.intent),
clickBehavior = getClickBehavior(state.intent),
+ isHidden = isHidden,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
index 41353b9921bd..f6535730cf77 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntry.java
@@ -33,7 +33,8 @@ import androidx.annotation.VisibleForTesting;
import com.android.systemui.statusbar.notification.icon.IconPack;
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import kotlinx.coroutines.flow.MutableStateFlow;
@@ -51,25 +52,31 @@ public class BundleEntry extends PipelineEntry {
// TODO (b/389839319): implement the row
private ExpandableNotificationRow mRow;
+ private final List<ListEntry> mChildren = new ArrayList<>();
+
+ private final List<ListEntry> mUnmodifiableChildren = Collections.unmodifiableList(mChildren);
+
public BundleEntry(String key) {
super(key);
}
- @Nullable
- @Override
- public NotificationEntry getRepresentativeEntry() {
- return null;
+ void addChild(ListEntry child) {
+ mChildren.add(child);
}
- @Nullable
- @Override
- public NotifSection getSection() {
- return null;
+ @NonNull
+ public List<ListEntry> getChildren() {
+ return mUnmodifiableChildren;
}
+ /**
+ * @return Null because bundles do not have an associated NotificationEntry.
+ */
+
+ @Nullable
@Override
- public int getSectionIndex() {
- return 0;
+ public NotificationEntry getRepresentativeEntry() {
+ return null;
}
@Nullable
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt
index 64db9df8270c..26c302bf6409 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/BundleEntryAdapter.kt
@@ -20,6 +20,7 @@ import android.app.Notification
import android.content.Context
import android.os.Build
import android.service.notification.StatusBarNotification
+import android.util.Log
import com.android.systemui.statusbar.notification.icon.IconPack
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import kotlinx.coroutines.flow.StateFlow
@@ -118,5 +119,13 @@ class BundleEntryAdapter(val entry: BundleEntry) : EntryAdapter {
override fun onNotificationBubbleIconClicked() {
// do nothing. these cannot be a bubble
+ Log.wtf(TAG, "onNotificationBubbleIconClicked() called")
+ }
+
+ override fun onNotificationActionClicked() {
+ // do nothing. these have no actions
+ Log.wtf(TAG, "onNotificationActionClicked() called")
}
}
+
+private const val TAG = "BundleEntryAdapter"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
index 0e75b6050678..3118ce56ac69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapter.java
@@ -140,5 +140,10 @@ public interface EntryAdapter {
* Process a click on a notification bubble icon
*/
void onNotificationBubbleIconClicked();
+
+ /**
+ * Processes a click on a notification action
+ */
+ void onNotificationActionClicked();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt
index 779c25a3b402..a5169865c3c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/EntryAdapterFactoryImpl.kt
@@ -16,11 +16,11 @@
package com.android.systemui.statusbar.notification.collection
-import androidx.annotation.VisibleForTesting
import com.android.internal.logging.MetricsLogger
import com.android.systemui.statusbar.notification.NotificationActivityStarter
import com.android.systemui.statusbar.notification.collection.coordinator.VisualStabilityCoordinator
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.row.NotificationActionClickManager
import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider
import javax.inject.Inject
@@ -33,6 +33,7 @@ constructor(
private val peopleNotificationIdentifier: PeopleNotificationIdentifier,
private val iconStyleProvider: NotificationIconStyleProvider,
private val visualStabilityCoordinator: VisualStabilityCoordinator,
+ private val notificationActionClickManager: NotificationActionClickManager,
) : EntryAdapterFactory {
override fun create(entry: PipelineEntry): EntryAdapter {
return if (entry is NotificationEntry) {
@@ -42,15 +43,11 @@ constructor(
peopleNotificationIdentifier,
iconStyleProvider,
visualStabilityCoordinator,
+ notificationActionClickManager,
entry,
)
} else {
BundleEntryAdapter((entry as BundleEntry))
}
}
-
- @VisibleForTesting
- fun getNotificationActivityStarter() : NotificationActivityStarter {
- return notificationActivityStarter
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
index 4a1b9568c714..04dc7d5ed3ff 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
@@ -31,8 +31,8 @@ data class ListAttachState private constructor(
var parent: PipelineEntry?,
/**
- * The section that this ListEntry was sorted into. If the child of the group, this will be the
- * parent's section. Null if not attached to the list.
+ * The section that this PipelineEntry was sorted into. If the child of the group, this will be
+ * the parent's section. Null if not attached to the list.
*/
var section: NotifSection?,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
index 697d0a06cf9d..caa7abb1aa7a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListEntry.java
@@ -66,10 +66,6 @@ public abstract class ListEntry extends PipelineEntry {
return mPreviousAttachState.getParent();
}
- public int getSectionIndex() {
- return mAttachState.getSection() != null ? mAttachState.getSection().getIndex() : -1;
- }
-
/**
* Stores the current attach state into {@link #getPreviousAttachState()}} and then starts a
* fresh attach state (all entries will be null/default-initialized).
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt
index f662a040fae6..0fc0e9c5eab8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifPipeline.kt
@@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifBundler
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
@@ -169,6 +170,14 @@ class NotifPipeline @Inject constructor(
}
/**
+ * NotifBundler that is used to determine whether a notification should be bundled according to
+ * classification.
+ */
+ fun setNotifBundler(bundler: NotifBundler) {
+ mShadeListBuilder.setBundler(bundler)
+ }
+
+ /**
* StabilityManager that is used to determine whether to suppress group and section changes.
* This should only be set once.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt
index 0ff2dd7c7f43..1168c647c26a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntryAdapter.kt
@@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.Visual
import com.android.systemui.statusbar.notification.icon.IconPack
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.row.NotificationActionClickManager
import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider
import kotlinx.coroutines.flow.StateFlow
@@ -33,6 +34,7 @@ class NotificationEntryAdapter(
private val peopleNotificationIdentifier: PeopleNotificationIdentifier,
private val iconStyleProvider: NotificationIconStyleProvider,
private val visualStabilityCoordinator: VisualStabilityCoordinator,
+ private val notificationActionClickManager: NotificationActionClickManager,
private val entry: NotificationEntry,
) : EntryAdapter {
@@ -142,4 +144,8 @@ class NotificationEntryAdapter(
override fun onNotificationBubbleIconClicked() {
notificationActivityStarter.onNotificationBubbleIconClicked(entry)
}
+
+ override fun onNotificationActionClicked() {
+ notificationActionClickManager.onNotificationActionClicked(entry)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
index 84de77bac352..872cd68e1b21 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/PipelineEntry.java
@@ -70,7 +70,9 @@ public abstract class PipelineEntry {
/**
* @return Index of section assigned to this entry.
*/
- public abstract int getSectionIndex();
+ public int getSectionIndex() {
+ return mAttachState.getSection() != null ? mAttachState.getSection().getIndex() : -1;
+ }
/**
* @return Parent PipelineEntry
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index bb84ab8f421a..238ba8d9f490 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -48,6 +48,7 @@ import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.statusbar.NotificationInteractionTracker;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
+import com.android.systemui.statusbar.notification.collection.coordinator.BundleCoordinator;
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener;
@@ -58,8 +59,10 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.SemiSt
import com.android.systemui.statusbar.notification.collection.listbuilder.SemiStableSort.StableOrder;
import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderHelper;
import com.android.systemui.statusbar.notification.collection.listbuilder.ShadeListBuilderLogger;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifBundler;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.DefaultNotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifBundler;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
@@ -78,6 +81,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -122,7 +126,8 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
private final List<NotifComparator> mNotifComparators = new ArrayList<>();
private final List<NotifSection> mNotifSections = new ArrayList<>();
private NotifStabilityManager mNotifStabilityManager;
-
+ private NotifBundler mNotifBundler;
+ private Map<String, BundleEntry> mIdToBundleEntry = new HashMap<>();
private final NamedListenerSet<OnBeforeTransformGroupsListener>
mOnBeforeTransformGroupsListeners = new NamedListenerSet<>();
private final NamedListenerSet<OnBeforeSortListener>
@@ -273,6 +278,21 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
}
}
+ void setBundler(NotifBundler bundler) {
+ Assert.isMainThread();
+ mPipelineState.requireState(STATE_IDLE);
+
+ mNotifBundler = bundler;
+ if (mNotifBundler == null) {
+ throw new IllegalStateException(TAG + ".setBundler: null");
+ }
+
+ mIdToBundleEntry.clear();
+ for (String id: mNotifBundler.getBundleIds()) {
+ mIdToBundleEntry.put(id, new BundleEntry(id));
+ }
+ }
+
void setNotifStabilityManager(@NonNull NotifStabilityManager notifStabilityManager) {
Assert.isMainThread();
mPipelineState.requireState(STATE_IDLE);
@@ -297,6 +317,14 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
return mNotifStabilityManager;
}
+ @NonNull
+ private NotifBundler getNotifBundler() {
+ if (mNotifBundler == null) {
+ return DefaultNotifBundler.INSTANCE;
+ }
+ return mNotifBundler;
+ }
+
void setComparators(List<NotifComparator> comparators) {
Assert.isMainThread();
mPipelineState.requireState(STATE_IDLE);
@@ -651,7 +679,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
j--;
}
}
- } else {
+ } else if (tle instanceof NotificationEntry) {
// maybe put top-level-entries back into their previous groups
if (maybeSuppressGroupChange(tle.getRepresentativeEntry(), topLevelList)) {
// entry was put back into its previous group, so we remove it from the list of
@@ -659,7 +687,7 @@ public class ShadeListBuilder implements Dumpable, PipelineDumpable {
topLevelList.remove(i);
i--;
}
- }
+ } // Promoters ignore bundles so we don't have to demote any here.
}
Trace.endSection();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt
index e6d5f4120a20..8833ff1ce20c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/BundleCoordinator.kt
@@ -20,15 +20,19 @@ import android.app.NotificationChannel.NEWS_ID
import android.app.NotificationChannel.PROMOTIONS_ID
import android.app.NotificationChannel.RECS_ID
import android.app.NotificationChannel.SOCIAL_MEDIA_ID
-import com.android.systemui.statusbar.notification.collection.PipelineEntry
+import android.app.NotificationChannel.SYSTEM_RESERVED_IDS
import com.android.systemui.statusbar.notification.collection.NotifPipeline
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.PipelineEntry
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifBundler
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.render.NodeController
import com.android.systemui.statusbar.notification.dagger.NewsHeader
import com.android.systemui.statusbar.notification.dagger.PromoHeader
import com.android.systemui.statusbar.notification.dagger.RecsHeader
import com.android.systemui.statusbar.notification.dagger.SocialHeader
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.notification.stack.BUCKET_NEWS
import com.android.systemui.statusbar.notification.stack.BUCKET_PROMO
import com.android.systemui.statusbar.notification.stack.BUCKET_RECS
@@ -90,6 +94,20 @@ class BundleCoordinator @Inject constructor(
}
}
+ val bundler =
+ object : NotifBundler("NotifBundler") {
+
+ // Use list instead of set to keep fixed order
+ override val bundleIds: List<String> = SYSTEM_RESERVED_IDS
+
+ override fun getBundleIdOrNull(entry: NotificationEntry?): String? {
+ return entry?.representativeEntry?.channel?.id?.takeIf { it in this.bundleIds }
+ }
+ }
+
override fun attach(pipeline: NotifPipeline) {
+ if (NotificationBundleUi.isEnabled) {
+ pipeline.setNotifBundler(bundler)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index fdb8cd871dd9..a0eab43f854b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -26,6 +26,7 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager
import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor
import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips
import com.android.systemui.statusbar.notification.NotifPipelineFlags
+import com.android.systemui.statusbar.notification.collection.BundleEntry
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.NotifCollection
import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -47,7 +48,9 @@ import com.android.systemui.statusbar.notification.headsup.PinnedStatus
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider
import com.android.systemui.statusbar.notification.logKey
+import com.android.systemui.statusbar.notification.row.NotificationActionClickManager
import com.android.systemui.statusbar.notification.shared.GroupHunAnimationFix
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.time.SystemClock
@@ -82,6 +85,7 @@ constructor(
private val mHeadsUpViewBinder: HeadsUpViewBinder,
private val mVisualInterruptionDecisionProvider: VisualInterruptionDecisionProvider,
private val mRemoteInputManager: NotificationRemoteInputManager,
+ private val notificationActionClickManager: NotificationActionClickManager,
private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider,
private val mFlags: NotifPipelineFlags,
private val statusBarNotificationChipsInteractor: StatusBarNotificationChipsInteractor,
@@ -107,7 +111,11 @@ constructor(
pipeline.addOnBeforeFinalizeFilterListener(::onBeforeFinalizeFilter)
pipeline.addPromoter(mNotifPromoter)
pipeline.addNotificationLifetimeExtender(mLifetimeExtender)
- mRemoteInputManager.addActionPressListener(mActionPressListener)
+ if (NotificationBundleUi.isEnabled) {
+ notificationActionClickManager.addActionClickListener(mActionPressListener)
+ } else {
+ mRemoteInputManager.addActionPressListener(mActionPressListener)
+ }
if (StatusBarNotifChips.isEnabled) {
applicationScope.launch {
@@ -423,6 +431,7 @@ constructor(
map[child.key] = GroupLocation.Child
}
}
+ is BundleEntry -> map[topLevelEntry.key] = GroupLocation.Bundle
else -> error("unhandled type $topLevelEntry")
}
}
@@ -781,7 +790,7 @@ constructor(
*/
private val mActionPressListener =
Consumer<NotificationEntry> { entry ->
- mHeadsUpManager.setUserActionMayIndirectlyRemove(entry)
+ mHeadsUpManager.setUserActionMayIndirectlyRemove(entry.key)
mExecutor.execute { endNotifLifetimeExtensionIfExtended(entry) }
}
@@ -950,6 +959,7 @@ private enum class GroupLocation {
Isolated,
Summary,
Child,
+ Bundle,
}
private fun Map<String, GroupLocation>.getLocation(key: String): GroupLocation =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
index 56deb18df9ab..d542e67e665a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/LockScreenMinimalismCoordinator.kt
@@ -26,6 +26,7 @@ import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.collection.BundleEntry
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.PipelineEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -193,6 +194,7 @@ constructor(
when (it) {
is NotificationEntry -> listOfNotNull(it)
is GroupEntry -> it.children
+ is BundleEntry -> emptyList()
else -> error("unhandled type of $it")
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
index df0cde51e8ba..818ef6b335c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt
@@ -24,6 +24,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.dagger
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
import com.android.systemui.statusbar.notification.promoted.AutomaticPromotionCoordinator
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi
import com.android.systemui.statusbar.notification.shared.NotificationMinimalism
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.notification.shared.PriorityPeopleSection
@@ -113,11 +114,12 @@ constructor(
mCoordinators.add(remoteInputCoordinator)
mCoordinators.add(dismissibilityCoordinator)
mCoordinators.add(automaticPromotionCoordinator)
-
+ if (NotificationBundleUi.isEnabled) {
+ mCoordinators.add(bundleCoordinator)
+ }
if (NotificationsLiveDataStoreRefactor.isEnabled) {
mCoordinators.add(statsLoggerCoordinator)
}
-
// Manually add Ordered Sections
if (NotificationMinimalism.isEnabled) {
mOrderedSections.add(lockScreenMinimalismCoordinator.topOngoingSectioner) // Top Ongoing
@@ -135,7 +137,7 @@ constructor(
mOrderedSections.add(conversationCoordinator.peopleSilentSectioner) // People Silent
}
mOrderedSections.add(rankingCoordinator.alertingSectioner) // Alerting
- if (NotificationClassificationFlag.isEnabled) {
+ if (NotificationClassificationFlag.isEnabled && !NotificationBundleUi.isEnabled) {
mOrderedSections.add(bundleCoordinator.newsSectioner)
mOrderedSections.add(bundleCoordinator.socialSectioner)
mOrderedSections.add(bundleCoordinator.recsSectioner)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index 20c6736b74c8..b54f21b23bba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -34,6 +34,7 @@ import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
+import com.android.systemui.statusbar.notification.collection.BundleEntry;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.PipelineEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -54,6 +55,7 @@ import com.android.systemui.statusbar.notification.row.icon.AppIconProvider;
import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
+import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -306,7 +308,9 @@ public class PreparationCoordinator implements Coordinator {
private void inflateAllRequiredViews(List<PipelineEntry> entries) {
for (int i = 0, size = entries.size(); i < size; i++) {
PipelineEntry entry = entries.get(i);
- if (entry instanceof GroupEntry) {
+ if (NotificationBundleUi.isEnabled() && entry instanceof BundleEntry) {
+ // TODO(b/399738511) Inflate bundle views.
+ } else if (entry instanceof GroupEntry) {
GroupEntry groupEntry = (GroupEntry) entry;
inflateRequiredGroupViews(groupEntry);
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
index d1063d95a305..1f32b945ce7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RankingCoordinator.java
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.statusbar.notification.collection.BundleEntry;
import com.android.systemui.statusbar.notification.collection.PipelineEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -116,6 +117,9 @@ public class RankingCoordinator implements Coordinator {
NotificationPriorityBucketKt.BUCKET_SILENT) {
@Override
public boolean isInSection(PipelineEntry entry) {
+ if (entry instanceof BundleEntry) {
+ return true;
+ }
return !mHighPriorityProvider.isHighPriority(entry)
&& entry.getRepresentativeEntry() != null
&& !entry.getRepresentativeEntry().isAmbient();
@@ -132,7 +136,12 @@ public class RankingCoordinator implements Coordinator {
public void onEntriesUpdated(@NonNull List<PipelineEntry> entries) {
mHasSilentEntries = false;
for (int i = 0; i < entries.size(); i++) {
- if (entries.get(i).getRepresentativeEntry().getSbn().isClearable()) {
+ NotificationEntry notifEntry = entries.get(i).getRepresentativeEntry();
+ if (notifEntry == null) {
+ // TODO(b/395698521) Handle BundleEntry
+ continue;
+ }
+ if (notifEntry.getSbn().isClearable()) {
mHasSilentEntries = true;
break;
}
@@ -147,6 +156,7 @@ public class RankingCoordinator implements Coordinator {
@Override
public boolean isInSection(PipelineEntry entry) {
return !mHighPriorityProvider.isHighPriority(entry)
+ && entry.getRepresentativeEntry() != null
&& entry.getRepresentativeEntry().isAmbient();
}
@@ -161,7 +171,12 @@ public class RankingCoordinator implements Coordinator {
public void onEntriesUpdated(@NonNull List<PipelineEntry> entries) {
mHasMinimizedEntries = false;
for (int i = 0; i < entries.size(); i++) {
- if (entries.get(i).getRepresentativeEntry().getSbn().isClearable()) {
+ NotificationEntry notifEntry = entries.get(i).getRepresentativeEntry();
+ if (notifEntry == null) {
+ // TODO(b/395698521) Handle BundleEntry
+ continue;
+ }
+ if (notifEntry.getSbn().isClearable()) {
mHasMinimizedEntries = true;
break;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
index e7c767f42c12..27c0dcccfe43 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
@@ -221,20 +221,20 @@ constructor(
mSmartReplyHistoryExtender.isExtending(key)
} else false
- override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) {
- if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})")
+ override fun releaseNotificationIfKeptForRemoteInputHistory(entryKey: String) {
+ if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entryKey})")
if (!lifetimeExtensionRefactor()) {
mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(
- entry.key,
+ entryKey,
REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
)
mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(
- entry.key,
+ entryKey,
REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
)
}
mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(
- entry.key,
+ entryKey,
REMOTE_INPUT_EXTENDER_RELEASE_DELAY,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index c2f0806a9cd6..6b32c6a18ec0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.notification.collection.inflation;
import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE;
-import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
@@ -276,7 +275,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder {
if (LockscreenOtpRedaction.isSingleLineViewEnabled()) {
if (inflaterParams.isChildInGroup()
- && redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) {
+ && redactionType != REDACTION_TYPE_NONE) {
params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE);
} else {
params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.kt
new file mode 100644
index 000000000000..14a9113f7eb4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/pluggable/NotifBundler.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.collection.listbuilder.pluggable
+
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+
+/** Pluggable for bundling notifications according to classification. */
+abstract class NotifBundler protected constructor(name: String?) : Pluggable<NotifBundler?>(name) {
+ abstract val bundleIds: List<String>
+
+ abstract fun getBundleIdOrNull(entry: NotificationEntry?): String?
+}
+
+/** The default, no-op instance of NotifBundler which does not bundle anything. */
+object DefaultNotifBundler : NotifBundler("DefaultNotifBundler") {
+ override val bundleIds: List<String>
+ get() = listOf()
+
+ override fun getBundleIdOrNull(entry: NotificationEntry?): String? {
+ return null
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index ef3da9498f70..1e5aa01714be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -83,6 +83,7 @@ import com.android.systemui.statusbar.notification.logging.dagger.NotificationsL
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractorImpl;
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
+import com.android.systemui.statusbar.notification.row.NotificationActionClickManager;
import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactory;
import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactoryLooperImpl;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -346,4 +347,5 @@ public interface NotificationsModule {
/** Provides an instance of {@link EntryAdapterFactory} */
@Binds
EntryAdapterFactory provideEntryAdapterFactory(EntryAdapterFactoryImpl impl);
+
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt
index 9728fcfcd6ba..25ae50c34659 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManager.kt
@@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.headsup
import android.graphics.Region
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.notification.collection.EntryAdapter
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import dagger.Binds
@@ -155,9 +154,9 @@ interface HeadsUpManager : Dumpable {
fun setAnimationStateHandler(handler: AnimationStateHandler)
/**
- * Set an entry to be expanded and therefore stick in the heads up area if it's pinned until
- * it's collapsed again.
- */
+ * Set an entry to be expanded and therefore stick in the heads up area if it's pinned until
+ * it's collapsed again.
+ */
fun setExpanded(key: String, row: ExpandableNotificationRow, expanded: Boolean)
/**
@@ -199,12 +198,12 @@ interface HeadsUpManager : Dumpable {
* Notes that the user took an action on an entry that might indirectly cause the system or the
* app to remove the notification.
*
- * @param entry the entry that might be indirectly removed by the user's action
+ * @param entry the key of the entry that might be indirectly removed by the user's action
* @see
* com.android.systemui.statusbar.notification.collection.coordinator.HeadsUpCoordinator.mActionPressListener
* @see .canRemoveImmediately
*/
- fun setUserActionMayIndirectlyRemove(entry: NotificationEntry)
+ fun setUserActionMayIndirectlyRemove(entryKey: String)
/**
* Decides whether a click is invalid for a notification, i.e. it has not been shown long enough
@@ -332,7 +331,7 @@ class HeadsUpManagerEmptyImpl @Inject constructor() : HeadsUpManager {
override fun setUser(user: Int) {}
- override fun setUserActionMayIndirectlyRemove(entry: NotificationEntry) {}
+ override fun setUserActionMayIndirectlyRemove(entryKey: String) {}
override fun shouldSwallowClick(key: String): Boolean = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
index ca94655318b9..ca83666079ec 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java
@@ -1126,8 +1126,8 @@ public class HeadsUpManagerImpl
* @see HeadsUpCoordinator.mActionPressListener
* @see #canRemoveImmediately(String)
*/
- public void setUserActionMayIndirectlyRemove(@NonNull NotificationEntry entry) {
- HeadsUpEntry headsUpEntry = getHeadsUpEntry(entry.getKey());
+ public void setUserActionMayIndirectlyRemove(@NonNull String entryKey) {
+ HeadsUpEntry headsUpEntry = getHeadsUpEntry(entryKey);
if (headsUpEntry != null) {
headsUpEntry.mUserActionMayIndirectlyRemove = true;
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index c877beeda152..8da2f768bf71 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -68,6 +68,7 @@ import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.ViewStub;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.widget.Chronometer;
@@ -114,7 +115,6 @@ import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.collection.EntryAdapter;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.NotificationEntryAdapter;
import com.android.systemui.statusbar.notification.collection.PipelineEntry;
import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider;
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
@@ -123,6 +123,7 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
import com.android.systemui.statusbar.notification.logging.NotificationCounters;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction;
@@ -185,7 +186,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private boolean mShowSnooze = false;
private boolean mIsFaded;
- private boolean mIsPromotedOngoing = false;
private boolean mHasStatusBarChipDuringHeadsUpAnimation = false;
@Nullable
@@ -871,7 +871,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private void updateLimitsForView(NotificationContentView layout) {
final int maxExpandedHeight;
- if (isPromotedOngoing()) {
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
maxExpandedHeight = mMaxExpandedHeightForPromotedOngoing;
} else {
maxExpandedHeight = mMaxExpandedHeight;
@@ -1350,7 +1350,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (mIsSummaryWithChildren) {
return mChildrenContainer.getIntrinsicHeight();
}
- if (isPromotedOngoing()) {
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
return getMaxExpandHeight();
}
if (mExpandedWhenPinned) {
@@ -2939,7 +2939,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
if (mIsSummaryWithChildren && !shouldShowPublic()) {
return !mChildrenExpanded;
}
- if (isPromotedOngoing()) {
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
return false;
}
return mEnableNonGroupedNotificationExpand && mExpandable;
@@ -2950,17 +2950,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mPrivateLayout.updateExpandButtons(isExpandable());
}
- /**
- * Set this notification to be promoted ongoing
- */
- public void setPromotedOngoing(boolean promotedOngoing) {
- if (PromotedNotificationUiForceExpanded.isUnexpectedlyInLegacyMode()) {
- return;
- }
-
- mIsPromotedOngoing = promotedOngoing;
- setExpandable(!mIsPromotedOngoing);
- }
/**
* Sets whether the status bar is showing a chip corresponding to this notification.
@@ -3061,10 +3050,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public void setUserLocked(boolean userLocked) {
- if (isPromotedOngoing()) return;
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) return;
mUserLocked = userLocked;
mPrivateLayout.setUserExpanding(userLocked);
+ if (android.app.Flags.expandingPublicView()) {
+ mPublicLayout.setUserExpanding(userLocked);
+ }
// This is intentionally not guarded with mIsSummaryWithChildren since we might have had
// children but not anymore.
if (mChildrenContainer != null) {
@@ -3247,7 +3239,16 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public boolean isPromotedOngoing() {
- return PromotedNotificationUiForceExpanded.isEnabled() && mIsPromotedOngoing;
+ if (!PromotedNotificationUi.isEnabled()) {
+ return false;
+ }
+
+ final NotificationEntry entry = mEntry;
+ if (entry == null) {
+ return false;
+ }
+
+ return entry.isPromotedOngoing();
}
private boolean isPromotedNotificationExpanded(boolean allowOnKeyguard) {
@@ -3309,7 +3310,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
public boolean isExpanded(boolean allowOnKeyguard) {
- if (isPromotedOngoing()) {
+ if (PromotedNotificationUiForceExpanded.isEnabled() && isPromotedOngoing()) {
return isPromotedNotificationExpanded(allowOnKeyguard);
}
@@ -4038,10 +4039,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
private void notifyAccessibilityContentExpansionChanged() {
- AccessibilityEvent event = AccessibilityEvent.obtain(TYPE_WINDOW_CONTENT_CHANGED);
- onPopulateAccessibilityEvent(event);
- event.setContentChangeTypes(CONTENT_CHANGE_TYPE_EXPANDED);
- sendAccessibilityEventUnchecked(event);
+ if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(TYPE_WINDOW_CONTENT_CHANGED);
+ event.setContentChangeTypes(CONTENT_CHANGE_TYPE_EXPANDED);
+ sendAccessibilityEventUnchecked(event);
+ }
}
public void setOnExpansionChangedListener(@Nullable OnExpansionChangedListener listener) {
@@ -4396,9 +4399,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
+ (!shouldShowPublic() && mIsSummaryWithChildren));
pw.print(", mShowNoBackground: " + mShowNoBackground);
pw.print(", clipBounds: " + getClipBounds());
- if (PromotedNotificationUiForceExpanded.isEnabled()) {
- pw.print(", isPromotedOngoing: " + isPromotedOngoing());
- }
+ pw.print(", isPromotedOngoing: " + isPromotedOngoing());
if (notificationRowAccessibilityExpanded()) {
pw.print(", isShowingExpanded: " + isShowingExpanded());
pw.print(", isAccessibilityExpandable: " + isAccessibilityExpandable());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt
new file mode 100644
index 000000000000..2b451406eaad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManager.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.util.ListenerSet
+import java.util.function.Consumer
+import javax.inject.Inject
+
+/**
+ * Pipeline components can register consumers here to be informed when a notification action is
+ * clicked
+ */
+@SysUISingleton
+class NotificationActionClickManager @Inject constructor() {
+ private val actionClickListeners = ListenerSet<Consumer<NotificationEntry>>()
+
+ fun addActionClickListener(listener: Consumer<NotificationEntry>) {
+ actionClickListeners.addIfAbsent(listener)
+ }
+
+ fun removeActionClickListener(listener: Consumer<NotificationEntry>) {
+ actionClickListeners.remove(listener)
+ }
+
+ fun onNotificationActionClicked(entry: NotificationEntry) {
+ for (listener in actionClickListeners) {
+ listener.accept(entry)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 51569c1596ef..3ffc052b5acc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -59,7 +59,6 @@ import com.android.systemui.statusbar.notification.NmSummarizationUiFlag;
import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor;
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
import com.android.systemui.statusbar.notification.row.shared.AsyncGroupHeaderViewInflation;
import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
@@ -1006,10 +1005,6 @@ public class NotificationContentInflater implements NotificationRowContentBinder
entry.setPromotedNotificationContentModel(result.mPromotedContent);
}
- if (PromotedNotificationUiForceExpanded.isEnabled()) {
- row.setPromotedOngoing(entry.isOngoingPromoted());
- }
-
boolean setRepliesAndActions = true;
if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) {
if (result.inflatedContentView != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
index 482b315aa14d..b1c145e08777 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt
@@ -53,7 +53,6 @@ import com.android.systemui.statusbar.notification.NmSummarizationUiFlag
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.logKey
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor
-import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED
import com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED
@@ -1293,6 +1292,7 @@ constructor(
runningInflations,
e,
row,
+ entry,
callback,
logger,
"applying view synchronously",
@@ -1318,6 +1318,7 @@ constructor(
runningInflations,
InflationException(invalidReason),
row,
+ entry,
callback,
logger,
"applied invalid view",
@@ -1377,6 +1378,7 @@ constructor(
runningInflations,
e,
row,
+ entry,
callback,
logger,
"applying view",
@@ -1480,6 +1482,7 @@ constructor(
runningInflations: HashMap<Int, CancellationSignal>,
e: Exception,
notification: ExpandableNotificationRow?,
+ entry: NotificationEntry,
callback: InflationCallback?,
logger: NotificationRowContentBinderLogger,
logContext: String,
@@ -1487,7 +1490,7 @@ constructor(
Assert.isMainThread()
logger.logAsyncTaskException(notification?.loggingKey, logContext, e)
runningInflations.values.forEach(Consumer { obj: CancellationSignal -> obj.cancel() })
- callback?.handleInflationException(notification?.entry, e)
+ callback?.handleInflationException(entry, e)
}
/**
@@ -1520,10 +1523,6 @@ constructor(
entry.promotedNotificationContentModel = result.promotedContent
}
- if (PromotedNotificationUiForceExpanded.isEnabled) {
- row.setPromotedOngoing(entry.isOngoingPromoted())
- }
-
result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) }
setContentViewsFromRemoteViews(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
index d7dd7ec08583..53728c7da62d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt
@@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.shared
import android.app.PendingIntent
import android.graphics.drawable.Icon
+import android.util.Log
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel
import com.android.systemui.statusbar.notification.stack.PriorityBucket
@@ -85,6 +86,15 @@ data class ActiveNotificationModel(
*/
val promotedContent: PromotedNotificationContentModel?,
) : ActiveNotificationEntryModel() {
+ init {
+ if (!PromotedNotificationContentModel.featureFlagEnabled()) {
+ if (promotedContent != null) {
+ // TODO(b/401018545): convert to Log.wtf and fix tests (see: ag/32114199)
+ Log.e(TAG, "passing non-null promoted content without feature flag enabled")
+ }
+ }
+ }
+
companion object {
private const val TAG = "ActiveNotificationEntryModel"
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
index d3af1e5b65fe..6d959be1c5f4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyStateInflater.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.policy
import android.app.ActivityOptions
+import android.app.Flags.notificationsRedesignTemplates
import android.app.Notification
import android.app.Notification.Action.SEMANTIC_ACTION_MARK_CONVERSATION_AS_PRIORITY
import android.app.PendingIntent
@@ -53,7 +54,6 @@ import com.android.systemui.statusbar.SmartReplyController
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.headsup.HeadsUpManager
import com.android.systemui.statusbar.notification.logging.NotificationLogger
-import com.android.systemui.statusbar.notification.row.MagicActionBackgroundDrawable
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.statusbar.policy.InflatedSmartReplyState.SuppressedActions
import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions
@@ -397,16 +397,21 @@ constructor(
delayOnClickListener: Boolean,
packageContext: Context,
): Button {
- val isMagicAction = Flags.notificationMagicActionsTreatment() &&
+ val isMagicAction =
+ Flags.notificationMagicActionsTreatment() &&
smartActions.fromAssistant &&
action.extras.getBoolean(Notification.Action.EXTRA_IS_MAGIC, false)
- val layoutRes = if (isMagicAction) {
- R.layout.magic_action_button
- } else {
- R.layout.smart_action_button
- }
- return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false)
- as Button)
+ val layoutRes =
+ if (isMagicAction) {
+ R.layout.magic_action_button
+ } else {
+ if (notificationsRedesignTemplates()) {
+ R.layout.notification_2025_smart_action_button
+ } else {
+ R.layout.smart_action_button
+ }
+ }
+ return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) as Button)
.apply {
text = action.title
@@ -435,7 +440,6 @@ constructor(
// Mark this as an Action button
(layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.ACTION
}
-
}
private fun onSmartActionClick(
@@ -499,9 +503,11 @@ constructor(
replyIndex: Int,
choice: CharSequence,
delayOnClickListener: Boolean,
- ): Button =
- (LayoutInflater.from(parent.context).inflate(R.layout.smart_reply_button, parent, false)
- as Button)
+ ): Button {
+ val layoutRes =
+ if (notificationsRedesignTemplates()) R.layout.notification_2025_smart_reply_button
+ else R.layout.smart_reply_button
+ return (LayoutInflater.from(parent.context).inflate(layoutRes, parent, false) as Button)
.apply {
text = choice
val onClickListener =
@@ -531,6 +537,7 @@ constructor(
// Mark this as a Reply button
(layoutParams as SmartReplyView.LayoutParams).mButtonType = SmartButtonType.REPLY
}
+ }
private fun onSmartReplyClick(
entry: NotificationEntry,
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt
index 7dc6fa8e9f70..c11e4c507914 100644
--- a/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/TopLevelWindowEffects.kt
@@ -25,8 +25,10 @@ import com.android.app.viewcapture.ViewCaptureAwareWindowManager
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
import com.android.systemui.topwindoweffects.domain.interactor.SqueezeEffectInteractor
import com.android.systemui.topwindoweffects.ui.compose.EffectsWindowRoot
+import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@@ -37,7 +39,9 @@ class TopLevelWindowEffects @Inject constructor(
@Application private val context: Context,
@Application private val applicationScope: CoroutineScope,
private val windowManager: ViewCaptureAwareWindowManager,
- private val squeezeEffectInteractor: SqueezeEffectInteractor
+ private val squeezeEffectInteractor: SqueezeEffectInteractor,
+ private val keyEventInteractor: KeyEventInteractor,
+ private val viewModelFactory: SqueezeEffectViewModel.Factory
) : CoreStartable {
override fun start() {
@@ -45,12 +49,25 @@ class TopLevelWindowEffects @Inject constructor(
var root: EffectsWindowRoot? = null
squeezeEffectInteractor.isSqueezeEffectEnabled.collectLatest { enabled ->
// TODO: move window ops to a separate UI thread
- if (enabled && root == null) {
- root = EffectsWindowRoot(context)
- root?.let { windowManager.addView(it, getWindowManagerLayoutParams()) }
- } else if (root?.isAttachedToWindow == true) {
- windowManager.removeView(root)
- root = null
+ if (enabled) {
+ keyEventInteractor.isPowerButtonDown.collectLatest { down ->
+ // TODO: ignore new window creation when ignoring short power press duration
+ if (down && root == null) {
+ root = EffectsWindowRoot(
+ context = context,
+ viewModelFactory = viewModelFactory,
+ onEffectFinished = {
+ if (root?.isAttachedToWindow == true) {
+ windowManager.removeView(root)
+ root = null
+ }
+ }
+ )
+ root?.let {
+ windowManager.addView(it, getWindowManagerLayoutParams())
+ }
+ }
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt
index 0826917c5cb5..61448f45c0e2 100644
--- a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/EffectsWindowRoot.kt
@@ -16,22 +16,19 @@
package com.android.systemui.topwindoweffects.ui.compose
+import android.annotation.SuppressLint
import android.content.Context
-import android.util.AttributeSet
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.AbstractComposeView
import com.android.systemui.compose.ComposeInitializer
+import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel
-class EffectsWindowRoot : AbstractComposeView {
- constructor(context: Context) : super(context)
-
- constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
-
- constructor(
- context: Context,
- attrs: AttributeSet,
- defStyleAttr: Int,
- ) : super(context, attrs, defStyleAttr)
+@SuppressLint("ViewConstructor")
+class EffectsWindowRoot(
+ context: Context,
+ private val onEffectFinished: () -> Unit,
+ private val viewModelFactory: SqueezeEffectViewModel.Factory
+) : AbstractComposeView(context) {
override fun onAttachedToWindow() {
ComposeInitializer.onAttachedToWindow(this)
@@ -45,6 +42,9 @@ class EffectsWindowRoot : AbstractComposeView {
@Composable
override fun Content() {
- SqueezeEffect()
+ SqueezeEffect(
+ viewModelFactory = viewModelFactory,
+ onEffectFinished = onEffectFinished
+ )
}
} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt
index 124724000aaf..9809b0592dcf 100644
--- a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/compose/SqueezeEffect.kt
@@ -16,9 +16,124 @@
package com.android.systemui.topwindoweffects.ui.compose
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.withTransform
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.VectorPainter
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.unit.dp
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.res.R
+import com.android.systemui.topwindoweffects.ui.viewmodel.SqueezeEffectViewModel
+
+private val SqueezeEffectMaxThickness = 12.dp
+private val SqueezeColor = Color.Black
@Composable
-fun SqueezeEffect() {
- // TODO: add squeeze effect
+fun SqueezeEffect(
+ viewModelFactory: SqueezeEffectViewModel.Factory,
+ onEffectFinished: () -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val viewModel = rememberViewModel(traceName = "SqueezeEffect") { viewModelFactory.create() }
+ val down = viewModel.isPowerButtonPressed
+ val longPressed = viewModel.isPowerButtonLongPressed
+ // TODO: Choose the correct resource based on primary / secondary display
+ val top = rememberVectorPainter(ImageVector.vectorResource(R.drawable.rounded_corner_top))
+ val bottom = rememberVectorPainter(ImageVector.vectorResource(R.drawable.rounded_corner_bottom))
+
+ val squeezeProgress by animateFloatAsState(
+ targetValue =
+ if (down && !longPressed) {
+ 1f
+ } else {
+ 0f
+ },
+ animationSpec = tween(durationMillis = 400),
+ finishedListener = { onEffectFinished() }
+ )
+
+ Canvas(modifier = modifier.fillMaxSize()) {
+ if (squeezeProgress <= 0) {
+ return@Canvas
+ }
+
+ val squeezeThickness = SqueezeEffectMaxThickness.toPx() * squeezeProgress
+
+ drawRect(color = SqueezeColor, size = Size(size.width, squeezeThickness))
+
+ drawRect(
+ color = SqueezeColor,
+ topLeft = Offset(0f, size.height - squeezeThickness),
+ size = Size(size.width, squeezeThickness)
+ )
+
+ drawRect(color = SqueezeColor, size = Size(squeezeThickness, size.height))
+
+ drawRect(
+ color = SqueezeColor,
+ topLeft = Offset(size.width - squeezeThickness, 0f),
+ size = Size(squeezeThickness, size.height)
+ )
+
+ drawTransform(
+ dx = squeezeThickness,
+ dy = squeezeThickness,
+ rotation = 0f,
+ corner = top
+ )
+
+ drawTransform(
+ dx = size.width - squeezeThickness,
+ dy = squeezeThickness,
+ rotation = 90f,
+ corner = top
+ )
+
+ drawTransform(
+ dx = squeezeThickness,
+ dy = size.height - squeezeThickness,
+ rotation = 270f,
+ corner = bottom
+ )
+
+ drawTransform(
+ dx = size.width - squeezeThickness,
+ dy = size.height - squeezeThickness,
+ rotation = 180f,
+ corner = bottom
+ )
+ }
+}
+
+private fun DrawScope.drawTransform(
+ dx: Float,
+ dy: Float,
+ rotation: Float = 0f,
+ corner: VectorPainter,
+) {
+ withTransform(transformBlock = {
+ transform(matrix = Matrix().apply {
+ translate(dx, dy)
+ if (rotation != 0f) {
+ rotateZ(rotation)
+ }
+ })
+ }) {
+ with(corner) {
+ draw(size = intrinsicSize)
+ }
+ }
} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt
new file mode 100644
index 000000000000..1cab327740ce
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/topwindoweffects/ui/viewmodel/SqueezeEffectViewModel.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.ui.viewmodel
+
+import androidx.compose.runtime.getValue
+import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.Hydrator
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+class SqueezeEffectViewModel
+@AssistedInject
+constructor(
+ keyEventInteractor: KeyEventInteractor
+) : ExclusiveActivatable() {
+ private val hydrator = Hydrator("SqueezeEffectViewModel.hydrator")
+
+ val isPowerButtonPressed: Boolean by hydrator.hydratedStateOf(
+ traceName = "isPowerButtonPressed",
+ initialValue = false,
+ source = keyEventInteractor.isPowerButtonDown
+ )
+
+ val isPowerButtonLongPressed: Boolean by hydrator.hydratedStateOf(
+ traceName = "isPowerButtonLongPressed",
+ initialValue = false,
+ source = keyEventInteractor.isPowerButtonLongPressed
+ )
+
+ override suspend fun onActivated(): Nothing {
+ hydrator.activate()
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): SqueezeEffectViewModel
+ }
+} \ No newline at end of file
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 b274f1c2c6df..d9c59d37b031 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
@@ -88,11 +88,6 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() {
companion object {
private val INTENT = Intent("some.intent.action")
- private val DRAWABLE =
- mock<Icon> {
- whenever(this.contentDescription)
- .thenReturn(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID))
- }
private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
@Parameters(
@@ -337,7 +332,17 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() {
homeControls.setState(
lockScreenState =
- KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = DRAWABLE)
+ KeyguardQuickAffordanceConfig.LockScreenState.Visible(
+ icon =
+ mock<Icon> {
+ whenever(contentDescription)
+ .thenReturn(
+ ContentDescription.Resource(
+ res = CONTENT_DESCRIPTION_RESOURCE_ID
+ )
+ )
+ }
+ )
)
homeControls.onTriggeredResult =
if (startActivity) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
index 175e8d4331a1..e048d462ef8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt
@@ -33,8 +33,8 @@ import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.RequiresFlagsDisabled
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.platform.test.flag.junit.FlagsParameterization
import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.settingslib.bluetooth.LocalBluetoothManager
@@ -77,6 +77,8 @@ import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
import org.mockito.kotlin.eq
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
private const val KEY = "TEST_KEY"
private const val KEY_OLD = "TEST_KEY_OLD"
@@ -89,12 +91,24 @@ private const val BROADCAST_APP_NAME = "BROADCAST_APP_NAME"
private const val NORMAL_APP_NAME = "NORMAL_APP_NAME"
@SmallTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
@TestableLooper.RunWithLooper
-public class MediaDeviceManagerTest : SysuiTestCase() {
+public class MediaDeviceManagerTest(flags: FlagsParameterization) : SysuiTestCase() {
- private companion object {
+ companion object {
val OTHER_DEVICE_ICON_STUB = TestStubDrawable()
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.progressionOf(
+ com.android.systemui.Flags.FLAG_MEDIA_CONTROLS_DEVICE_MANAGER_BACKGROUND_EXECUTION
+ )
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
}
@get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
@@ -187,6 +201,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
@Test
fun loadMediaData() {
manager.onMediaDataLoaded(KEY, null, mediaData)
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
verify(lmmFactory).create(PACKAGE)
}
@@ -195,6 +211,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
manager.onMediaDataLoaded(KEY, null, mediaData)
manager.onMediaDataRemoved(KEY, false)
fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
verify(lmm).unregisterCallback(any())
verify(muteAwaitManager).stopListening()
}
@@ -406,6 +423,8 @@ public class MediaDeviceManagerTest : SysuiTestCase() {
manager.onMediaDataLoaded(KEY, null, mediaData)
// WHEN the notification is removed
manager.onMediaDataRemoved(KEY, true)
+ fakeBgExecutor.runAllReady()
+ fakeFgExecutor.runAllReady()
// THEN the listener receives key removed event
verify(listener).onKeyRemoved(eq(KEY), eq(true))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index 205ccea657df..9ad2235965bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -406,11 +406,6 @@ public class MediaOutputBaseDialogTest extends SysuiTestCase {
}
@Override
- int getHeaderIconSize() {
- return 10;
- }
-
- @Override
CharSequence getHeaderText() {
return mHeaderTitle;
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
index f0902e35b837..f1bf7c0bcf13 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialogTest.java
@@ -50,6 +50,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.media.BluetoothMediaDevice;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestCaseExtKt;
import com.android.systemui.animation.DialogTransitionAnimator;
@@ -152,9 +153,9 @@ public class MediaOutputBroadcastDialogTest extends SysuiTestCase {
volumePanelGlobalStateInteractor,
mUserTracker);
mMediaSwitchingController.mLocalMediaManager = mLocalMediaManager;
- mMediaOutputBroadcastDialog =
- new MediaOutputBroadcastDialog(
- mContext, false, mBroadcastSender, mMediaSwitchingController);
+ mMediaOutputBroadcastDialog = new MediaOutputBroadcastDialog(mContext, false,
+ mBroadcastSender, mMediaSwitchingController, mContext.getMainExecutor(),
+ ThreadUtils.getBackgroundExecutor());
mMediaOutputBroadcastDialog.show();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index d3ecb3d8c944..420fd6e33abc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -53,6 +53,7 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.settingslib.flags.Flags;
import com.android.settingslib.media.LocalMediaManager;
import com.android.settingslib.media.MediaDevice;
+import com.android.settingslib.utils.ThreadUtils;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.SysuiTestCaseExtKt;
import com.android.systemui.animation.DialogTransitionAnimator;
@@ -455,6 +456,8 @@ public class MediaOutputDialogTest extends SysuiTestCase {
controller,
mDialogTransitionAnimator,
mUiEventLogger,
+ mContext.getMainExecutor(),
+ ThreadUtils.getBackgroundExecutor(),
true);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
index c20a801cd5e3..a8bfbd18d0c5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDetailsContentManagerTest.kt
@@ -103,6 +103,8 @@ class InternetDetailsContentManagerTest : SysuiTestCase() {
whenever(internetWifiEntry.hasInternetAccess()).thenReturn(true)
whenever(wifiEntries.size).thenReturn(1)
whenever(internetDetailsContentController.getDialogTitleText()).thenReturn(TITLE)
+ whenever(internetDetailsContentController.getSubtitleText(ArgumentMatchers.anyBoolean()))
+ .thenReturn("")
whenever(internetDetailsContentController.getMobileNetworkTitle(ArgumentMatchers.anyInt()))
.thenReturn(MOBILE_NETWORK_TITLE)
whenever(
@@ -128,15 +130,13 @@ class InternetDetailsContentManagerTest : SysuiTestCase() {
internetDetailsContentController,
canConfigMobileData = true,
canConfigWifi = true,
- coroutineScope = scope,
- context = mContext,
uiEventLogger = mock<UiEventLogger>(),
handler = handler,
backgroundExecutor = bgExecutor,
keyguard = keyguard,
)
- internetDetailsContentManager.bind(contentView)
+ internetDetailsContentManager.bind(contentView, scope)
internetDetailsContentManager.adapter = internetAdapter
internetDetailsContentManager.connectedWifiEntry = internetWifiEntry
internetDetailsContentManager.wifiEntriesCount = wifiEntries.size
@@ -777,6 +777,26 @@ class InternetDetailsContentManagerTest : SysuiTestCase() {
}
}
+ @Test
+ fun updateTitleAndSubtitle() {
+ assertThat(internetDetailsContentManager.title).isEqualTo("Internet")
+ assertThat(internetDetailsContentManager.subTitle).isEqualTo("")
+
+ whenever(internetDetailsContentController.getDialogTitleText()).thenReturn("New title")
+ whenever(internetDetailsContentController.getSubtitleText(ArgumentMatchers.anyBoolean()))
+ .thenReturn("New subtitle")
+
+ internetDetailsContentManager.updateContent(true)
+ bgExecutor.runAllReady()
+
+ internetDetailsContentManager.internetContentData.observe(
+ internetDetailsContentManager.lifecycleOwner!!
+ ) {
+ assertThat(internetDetailsContentManager.title).isEqualTo("New title")
+ assertThat(internetDetailsContentManager.subTitle).isEqualTo("New subtitle")
+ }
+ }
+
companion object {
private const val TITLE = "Internet"
private const val MOBILE_NETWORK_TITLE = "Mobile Title"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java
new file mode 100644
index 000000000000..c231be181977
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/ringtone/RingtonePlayerTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.ringtone;
+
+import static org.junit.Assert.assertThrows;
+
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.UserHandle;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RingtonePlayerTest extends SysuiTestCase {
+
+ private AudioManager mAudioManager;
+
+ private static final String TAG = "RingtonePlayerTest";
+
+ @Before
+ public void setup() throws Exception {
+ mAudioManager = getContext().getSystemService(AudioManager.class);
+ }
+
+ @Test
+ public void testRingtonePlayerUriUserCheck() {
+ android.media.IRingtonePlayer irp = mAudioManager.getRingtonePlayer();
+ final AudioAttributes aa = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build();
+ // get a UserId that doesn't belong to mine
+ final int otherUserId = UserHandle.myUserId() == 0 ? 10 : 0;
+ // build a URI that I shouldn't have access to
+ final Uri uri = new Uri.Builder()
+ .scheme("content").authority(otherUserId + "@media")
+ .appendPath("external").appendPath("downloads")
+ .appendPath("bogusPathThatDoesNotMatter.mp3")
+ .build();
+ if (android.media.audio.Flags.ringtoneUserUriCheck()) {
+ assertThrows(SecurityException.class, () ->
+ irp.play(new Binder(), uri, aa, 1.0f /*volume*/, false /*looping*/)
+ );
+
+ assertThrows(SecurityException.class, () ->
+ irp.getTitle(uri));
+ }
+ }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index cd2ea7d25699..4315c0f638ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -73,6 +73,7 @@ import com.android.systemui.statusbar.notification.FeedbackIcon;
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
+import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi;
import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiForceExpanded;
import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
@@ -936,11 +937,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_sensitivePromotedNotification_notExpanded() throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setSensitive(/* sensitive= */true, /* hideSensitive= */false);
row.setHideSensitiveForIntrinsicHeight(/* hideSensitive= */true);
@@ -949,11 +952,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationNotOnKeyguard_expanded() throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(false);
// THEN
@@ -961,11 +966,13 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationAllowOnKeyguard_expanded() throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(true);
// THEN
@@ -973,12 +980,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationIgnoreLockscreenConstraints_expanded()
throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(true);
row.setIgnoreLockscreenConstraints(true);
@@ -987,12 +996,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationSaveSpaceOnLockScreen_notExpanded()
throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(true);
row.setSaveSpaceOnLockscreen(true);
@@ -1001,12 +1012,14 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
}
@Test
- @EnableFlags(PromotedNotificationUiForceExpanded.FLAG_NAME)
+ @EnableFlags({PromotedNotificationUi.FLAG_NAME, PromotedNotificationUiForceExpanded.FLAG_NAME})
public void isExpanded_promotedNotificationNotSaveSpaceOnLockScreen_expanded()
throws Exception {
// GIVEN
final ExpandableNotificationRow row = mNotificationTestHelper.createRow();
- row.setPromotedOngoing(true);
+ NotificationEntry entry = mock(NotificationEntry.class);
+ when(entry.isPromotedOngoing()).thenReturn(true);
+ row.setEntry(entry);
row.setOnKeyguard(true);
row.setSaveSpaceOnLockscreen(false);
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 a978ecdb3534..8de931a7af40 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -204,8 +204,6 @@ import com.android.wm.shell.taskview.TaskViewRepository;
import com.android.wm.shell.taskview.TaskViewTransitions;
import com.android.wm.shell.transition.Transitions;
-import kotlin.Lazy;
-
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -216,9 +214,6 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -227,6 +222,10 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executor;
+import kotlin.Lazy;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -602,14 +601,19 @@ public class BubblesTest extends SysuiTestCase {
// Get a reference to KeyguardStateController.Callback
verify(mKeyguardStateController, atLeastOnce())
.addCallback(mKeyguardStateControllerCallbackCaptor.capture());
+
+ // Make sure mocks are set up for current user
+ switchUser(ActivityManager.getCurrentUser());
}
@After
public void tearDown() throws Exception {
- ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles());
- for (int i = 0; i < bubbles.size(); i++) {
- mBubbleController.removeBubble(bubbles.get(i).getKey(),
- Bubbles.DISMISS_NO_LONGER_BUBBLE);
+ if (mBubbleData != null) {
+ ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles());
+ for (int i = 0; i < bubbles.size(); i++) {
+ mBubbleController.removeBubble(bubbles.get(i).getKey(),
+ Bubbles.DISMISS_NO_LONGER_BUBBLE);
+ }
}
mTestableLooper.processAllMessages();
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt
index 807bc82cbf4e..c9e25c31faa0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyevent/data/repository/FakeKeyEventRepository.kt
@@ -34,7 +34,7 @@ class FakeKeyEventRepository @Inject constructor() : KeyEventRepository {
_isPowerButtonDown.value = isDown
}
- fun setPowerButtonBeingLongPressed(isLongPressed: Boolean) {
+ fun setPowerButtonLongPressed(isLongPressed: Boolean) {
_isPowerButtonLongPressed.value = isLongPressed
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
index 255a780a84be..113059222aa2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.interactor
import android.content.applicationContext
+import android.os.powerManager
import android.view.accessibility.accessibilityManagerWrapper
import com.android.internal.logging.uiEventLogger
import com.android.systemui.broadcast.broadcastDispatcher
@@ -26,6 +27,8 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.shade.pulsingGestureListener
+import com.android.systemui.util.settings.data.repository.userAwareSecureSettingsRepository
+import com.android.systemui.util.time.fakeSystemClock
val Kosmos.keyguardTouchHandlingInteractor by
Kosmos.Fixture {
@@ -40,5 +43,8 @@ val Kosmos.keyguardTouchHandlingInteractor by
accessibilityManager = accessibilityManagerWrapper,
pulsingGestureListener = pulsingGestureListener,
faceAuthInteractor = deviceEntryFaceAuthInteractor,
+ secureSettingsRepository = userAwareSecureSettingsRepository,
+ powerManager = powerManager,
+ systemClock = fakeSystemClock,
)
}
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 4f8d5a14e390..9457de18b3b9 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
@@ -18,18 +18,14 @@ package com.android.systemui.qs
import com.android.systemui.plugins.qs.TileDetailsViewModel
-class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel() {
+class FakeTileDetailsViewModel(var tileSpec: String?) : TileDetailsViewModel {
private var _clickOnSettingsButton = 0
override fun clickOnSettingsButton() {
_clickOnSettingsButton++
}
- override fun getTitle(): String {
- return tileSpec ?: " Fake title"
- }
+ override val title = tileSpec ?: " Fake title"
- override fun getSubTitle(): String {
- return tileSpec ?: "Fake sub title"
- }
+ override val subTitle = tileSpec ?: "Fake sub title"
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt
index 75ca311689ce..4aa4a2b1a73a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/ToolbarViewModelKosmos.kt
@@ -18,6 +18,7 @@ package com.android.systemui.qs.panels.ui.viewmodel.toolbar
import android.content.applicationContext
import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.development.ui.viewmodel.buildNumberViewModelFactory
import com.android.systemui.globalactions.globalActionsDialogLite
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.footerActionsInteractor
@@ -29,6 +30,7 @@ val Kosmos.toolbarViewModelFactory by
override fun create(): ToolbarViewModel {
return ToolbarViewModel(
editModeButtonViewModelFactory,
+ buildNumberViewModelFactory,
footerActionsInteractor,
{ globalActionsDialogLite },
falsingInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt
index e99f61e7cd13..067e420b89c3 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/EntryAdapterFactoryKosmos.kt
@@ -25,12 +25,13 @@ import com.android.systemui.statusbar.notification.people.peopleNotificationIden
import com.android.systemui.statusbar.notification.row.icon.notificationIconStyleProvider
val Kosmos.entryAdapterFactory by
-Kosmos.Fixture {
- EntryAdapterFactoryImpl(
- mockNotificationActivityStarter,
- metricsLogger,
- peopleNotificationIdentifier,
- notificationIconStyleProvider,
- visualStabilityCoordinator,
- )
-} \ No newline at end of file
+ Kosmos.Fixture {
+ EntryAdapterFactoryImpl(
+ mockNotificationActivityStarter,
+ metricsLogger,
+ peopleNotificationIdentifier,
+ notificationIconStyleProvider,
+ visualStabilityCoordinator,
+ mockNotificationActionClickManager,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
index 7f012ba25ab9..6a674ca29ca4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt
@@ -375,6 +375,7 @@ class ExpandableNotificationRowBuilder(
Mockito.mock(PeopleNotificationIdentifier::class.java),
Mockito.mock(NotificationIconStyleProvider::class.java),
Mockito.mock(VisualStabilityCoordinator::class.java),
+ Mockito.mock(NotificationActionClickManager::class.java),
)
.create(entry)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt
new file mode 100644
index 000000000000..8e62ae8825f3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/NotificationActionClickManagerKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+var Kosmos.mockNotificationActionClickManager: NotificationActionClickManager by
+ Kosmos.Fixture { mock<NotificationActionClickManager>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
index 08cfd9f385d3..3e96fd7c729f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/shared/model/OngoingCallTestHelper.kt
@@ -17,6 +17,8 @@
package com.android.systemui.statusbar.phone.ongoingcall.shared.model
import android.app.PendingIntent
+import com.android.systemui.activity.data.repository.activityManagerRepository
+import com.android.systemui.activity.data.repository.fake
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
@@ -79,8 +81,10 @@ object OngoingCallTestHelper {
contentIntent: PendingIntent? = null,
uid: Int = DEFAULT_UID,
appName: String = "Fake name",
+ isAppVisible: Boolean = false,
) {
if (StatusBarChipsModernization.isEnabled) {
+ activityManagerRepository.fake.startingIsAppVisibleValue = isAppVisible
activeNotificationListRepository.addNotif(
activeNotificationModel(
key = key,
@@ -102,6 +106,7 @@ object OngoingCallTestHelper {
notificationKey = key,
appName = appName,
promotedContent = promotedContent,
+ isAppVisible = isAppVisible,
)
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt
new file mode 100644
index 000000000000..2d2a81586515
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/FakeSqueezeEffectRepository.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.data.repository
+
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeSqueezeEffectRepository : SqueezeEffectRepository {
+ override val isSqueezeEffectEnabled = MutableStateFlow(false)
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt
new file mode 100644
index 000000000000..aa8bb6b1e104
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/topwindoweffects/data/repository/SqueezeEffectRepositoryKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.topwindoweffects.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeSqueezeEffectRepository by Kosmos.Fixture { FakeSqueezeEffectRepository() } \ No newline at end of file
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index ccbc46fdb03b..5424ac3bd897 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -16,7 +16,7 @@ filegroup {
srcs: [
"texts/ravenwood-common-policies.txt",
],
- visibility: ["//visibility:private"],
+ visibility: [":__subpackages__"],
}
filegroup {
@@ -44,6 +44,22 @@ filegroup {
}
filegroup {
+ name: "ravenwood-standard-annotations",
+ srcs: [
+ "texts/ravenwood-standard-annotations.txt",
+ ],
+ visibility: [":__subpackages__"],
+}
+
+filegroup {
+ name: "ravenizer-standard-options",
+ srcs: [
+ "texts/ravenizer-standard-options.txt",
+ ],
+ visibility: [":__subpackages__"],
+}
+
+filegroup {
name: "ravenwood-annotation-allowed-classes",
srcs: [
"texts/ravenwood-annotation-allowed-classes.txt",
diff --git a/ravenwood/Framework.bp b/ravenwood/Framework.bp
index e36677189e02..f5b075b17fd4 100644
--- a/ravenwood/Framework.bp
+++ b/ravenwood/Framework.bp
@@ -33,6 +33,7 @@ genrule_defaults {
":ravenwood-common-policies",
":ravenwood-framework-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
":ravenwood-annotation-allowed-classes",
],
out: [
@@ -44,6 +45,7 @@ genrule_defaults {
framework_minus_apex_cmd = "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location hoststubgen_framework-minus-apex.log) " +
"--out-jar $(location ravenwood.jar) " +
"--in-jar $(location :framework-minus-apex-for-host) " +
@@ -178,6 +180,7 @@ java_genrule {
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location hoststubgen_services.core.log) " +
"--stats-file $(location hoststubgen_services.core_stats.csv) " +
@@ -196,6 +199,7 @@ java_genrule {
":ravenwood-common-policies",
":ravenwood-services-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
":ravenwood-annotation-allowed-classes",
],
out: [
@@ -247,6 +251,7 @@ java_genrule {
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location hoststubgen_core-icu4j-for-host.log) " +
"--stats-file $(location hoststubgen_core-icu4j-for-host_stats.csv) " +
@@ -265,6 +270,7 @@ java_genrule {
":ravenwood-common-policies",
":icu-ravenwood-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
],
out: [
"ravenwood.jar",
@@ -301,6 +307,7 @@ java_genrule {
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location framework-configinfrastructure.log) " +
"--stats-file $(location framework-configinfrastructure_stats.csv) " +
@@ -319,6 +326,7 @@ java_genrule {
":ravenwood-common-policies",
":framework-configinfrastructure-ravenwood-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
],
out: [
"ravenwood.jar",
@@ -355,6 +363,7 @@ java_genrule {
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location framework-statsd.log) " +
"--stats-file $(location framework-statsd_stats.csv) " +
@@ -373,6 +382,7 @@ java_genrule {
":ravenwood-common-policies",
":framework-statsd-ravenwood-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
],
out: [
"ravenwood.jar",
@@ -409,6 +419,7 @@ java_genrule {
tools: ["hoststubgen"],
cmd: "$(location hoststubgen) " +
"@$(location :ravenwood-standard-options) " +
+ "@$(location :ravenwood-standard-annotations) " +
"--debug-log $(location framework-graphics.log) " +
"--stats-file $(location framework-graphics_stats.csv) " +
@@ -427,6 +438,7 @@ java_genrule {
":ravenwood-common-policies",
":framework-graphics-ravenwood-policies",
":ravenwood-standard-options",
+ ":ravenwood-standard-annotations",
],
out: [
"ravenwood.jar",
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index df63cb9dfc50..1148539187ac 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -150,6 +150,10 @@
"host": true
},
{
+ "name": "RavenwoodCoreTest-light",
+ "host": true
+ },
+ {
"name": "RavenwoodMinimumTest",
"host": true
},
@@ -168,6 +172,10 @@
{
"name": "RavenwoodServicesTest",
"host": true
+ },
+ {
+ "name": "UinputTestsRavenwood",
+ "host": true
}
// AUTO-GENERATED-END
],
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
index 3e2c4051b792..f25ae6a34242 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodUtils.java
@@ -128,28 +128,6 @@ public class RavenwoodUtils {
runOnHandlerSync(getMainHandler(), r);
}
- public static class MockitoHelper {
- private MockitoHelper() {
- }
-
- /**
- * Allow verifyZeroInteractions to work on ravenwood. It was replaced with a different
- * method on. (Maybe we should do it in Ravenizer.)
- */
- public static void verifyZeroInteractions(Object... mocks) {
- if (RavenwoodRule.isOnRavenwood()) {
- // Mockito 4 or later
- reflectMethod("org.mockito.Mockito", "verifyNoInteractions", Object[].class)
- .callStatic(new Object[]{mocks});
- } else {
- // Mockito 2
- reflectMethod("org.mockito.Mockito", "verifyZeroInteractions", Object[].class)
- .callStatic(new Object[]{mocks});
- }
- }
- }
-
-
/**
* Wrap the given {@link Supplier} to become memoized.
*
diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java
new file mode 100644
index 000000000000..cdfd4a877f43
--- /dev/null
+++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodJdkPatchTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import org.junit.Test;
+
+import java.io.FileDescriptor;
+import java.util.LinkedHashMap;
+import java.util.regex.Pattern;
+
+public class RavenwoodJdkPatchTest {
+
+ @Test
+ public void testUnicodeRegex() {
+ var pattern = Pattern.compile("\\w+");
+ assertTrue(pattern.matcher("über").matches());
+ }
+
+ @Test
+ public void testLinkedHashMapEldest() {
+ var map = new LinkedHashMap<String, String>();
+ map.put("a", "b");
+ map.put("x", "y");
+ assertEquals(map.entrySet().iterator().next(), map.eldest());
+ }
+
+ @Test
+ public void testFileDescriptorGetSetInt() throws ErrnoException {
+ FileDescriptor fd = Os.open("/dev/zero", OsConstants.O_RDONLY, 0);
+ try {
+ int fdRaw = fd.getInt$();
+ assertNotEquals(-1, fdRaw);
+ fd.setInt$(-1);
+ assertEquals(-1, fd.getInt$());
+ fd.setInt$(fdRaw);
+ Os.close(fd);
+ assertEquals(-1, fd.getInt$());
+ } finally {
+ Os.close(fd);
+ }
+ }
+}
diff --git a/ravenwood/texts/ravenizer-standard-options.txt b/ravenwood/texts/ravenizer-standard-options.txt
new file mode 100644
index 000000000000..cef736f87e72
--- /dev/null
+++ b/ravenwood/texts/ravenizer-standard-options.txt
@@ -0,0 +1,13 @@
+# File containing standard options to Ravenizer for Ravenwood
+
+# Keep all classes / methods / fields in tests and its target
+--default-keep
+
+--delete-finals
+
+# Include standard annotations
+@jar:texts/ravenwood-standard-annotations.txt
+
+# Apply common policies
+--policy-override-file
+ jar:texts/ravenwood-common-policies.txt
diff --git a/ravenwood/texts/ravenwood-standard-annotations.txt b/ravenwood/texts/ravenwood-standard-annotations.txt
new file mode 100644
index 000000000000..75ec5cadb6fc
--- /dev/null
+++ b/ravenwood/texts/ravenwood-standard-annotations.txt
@@ -0,0 +1,37 @@
+# Standard annotations.
+# Note, each line is a single argument, so we need newlines after each `--xxx-annotation`.
+--keep-annotation
+ android.ravenwood.annotation.RavenwoodKeep
+
+--keep-annotation
+ android.ravenwood.annotation.RavenwoodKeepPartialClass
+
+--keep-class-annotation
+ android.ravenwood.annotation.RavenwoodKeepWholeClass
+
+--throw-annotation
+ android.ravenwood.annotation.RavenwoodThrow
+
+--remove-annotation
+ android.ravenwood.annotation.RavenwoodRemove
+
+--ignore-annotation
+ android.ravenwood.annotation.RavenwoodIgnore
+
+--partially-allowed-annotation
+ android.ravenwood.annotation.RavenwoodPartiallyAllowlisted
+
+--substitute-annotation
+ android.ravenwood.annotation.RavenwoodReplace
+
+--redirect-annotation
+ android.ravenwood.annotation.RavenwoodRedirect
+
+--redirection-class-annotation
+ android.ravenwood.annotation.RavenwoodRedirectionClass
+
+--class-load-hook-annotation
+ android.ravenwood.annotation.RavenwoodClassLoadHook
+
+--keep-static-initializer-annotation
+ android.ravenwood.annotation.RavenwoodKeepStaticInitializer
diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt
index 233657557747..0a650254a71f 100644
--- a/ravenwood/texts/ravenwood-standard-options.txt
+++ b/ravenwood/texts/ravenwood-standard-options.txt
@@ -15,41 +15,3 @@
#--default-class-load-hook
# com.android.hoststubgen.hosthelper.HostTestUtils.logClassLoaded
-
-# Standard annotations.
-# Note, each line is a single argument, so we need newlines after each `--xxx-annotation`.
---keep-annotation
- android.ravenwood.annotation.RavenwoodKeep
-
---keep-annotation
- android.ravenwood.annotation.RavenwoodKeepPartialClass
-
---keep-class-annotation
- android.ravenwood.annotation.RavenwoodKeepWholeClass
-
---throw-annotation
- android.ravenwood.annotation.RavenwoodThrow
-
---remove-annotation
- android.ravenwood.annotation.RavenwoodRemove
-
---ignore-annotation
- android.ravenwood.annotation.RavenwoodIgnore
-
---partially-allowed-annotation
- android.ravenwood.annotation.RavenwoodPartiallyAllowlisted
-
---substitute-annotation
- android.ravenwood.annotation.RavenwoodReplace
-
---redirect-annotation
- android.ravenwood.annotation.RavenwoodRedirect
-
---redirection-class-annotation
- android.ravenwood.annotation.RavenwoodRedirectionClass
-
---class-load-hook-annotation
- android.ravenwood.annotation.RavenwoodClassLoadHook
-
---keep-static-initializer-annotation
- android.ravenwood.annotation.RavenwoodKeepStaticInitializer
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt
index f59e143c1e4e..ae0a00855650 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/Exceptions.kt
@@ -15,8 +15,6 @@
*/
package com.android.hoststubgen
-import java.io.File
-
/**
* We will not print the stack trace for exceptions implementing it.
*/
@@ -64,9 +62,6 @@ class DuplicateAnnotationException(annotationName: String?) :
class InputFileNotFoundException(filename: String) :
ArgumentsException("File '$filename' not found")
-fun String.ensureFileExists(): String {
- if (!File(this).exists()) {
- throw InputFileNotFoundException(this)
- }
- return this
-}
+/** Thrown when a JAR resource does not exist. */
+class JarResourceNotFoundException(path: String) :
+ ArgumentsException("JAR resource '$path' not found")
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt
index 4fe21eac6972..98f96a89d889 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessor.kt
@@ -16,6 +16,7 @@
package com.android.hoststubgen
import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.findAnyAnnotation
import com.android.hoststubgen.filters.AnnotationBasedFilter
import com.android.hoststubgen.filters.ClassWidePolicyPropagatingFilter
import com.android.hoststubgen.filters.ConstantFilter
@@ -26,21 +27,25 @@ import com.android.hoststubgen.filters.KeepNativeFilter
import com.android.hoststubgen.filters.OutputFilter
import com.android.hoststubgen.filters.SanitizationFilter
import com.android.hoststubgen.filters.TextFileFilterPolicyBuilder
+import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
import com.android.hoststubgen.utils.ClassPredicate
import com.android.hoststubgen.visitors.BaseAdapter
+import com.android.hoststubgen.visitors.ImplGeneratingAdapter
import com.android.hoststubgen.visitors.PackageRedirectRemapper
+import java.io.PrintWriter
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.commons.ClassRemapper
import org.objectweb.asm.util.CheckClassAdapter
+import org.objectweb.asm.util.TraceClassVisitor
/**
* This class implements bytecode transformation of HostStubGen.
*/
class HostStubGenClassProcessor(
private val options: HostStubGenClassProcessorOptions,
- private val allClasses: ClassNodes,
+ val allClasses: ClassNodes,
private val errors: HostStubGenErrors = HostStubGenErrors(),
private val stats: HostStubGenStats? = null,
) {
@@ -48,6 +53,7 @@ class HostStubGenClassProcessor(
val remapper = FilterRemapper(filter)
private val packageRedirector = PackageRedirectRemapper(options.packageRedirects)
+ private val processedAnnotation = setOf(HostStubGenProcessedAsKeep.CLASS_DESCRIPTOR)
/**
* Build the filter, which decides what classes/methods/fields should be put in stub or impl
@@ -130,15 +136,10 @@ class HostStubGenClassProcessor(
return filter
}
- fun processClassBytecode(bytecode: ByteArray): ByteArray {
- val cr = ClassReader(bytecode)
-
- // COMPUTE_FRAMES wouldn't be happy if code uses
- val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES
- val cw = ClassWriter(flags)
+ private fun buildVisitor(base: ClassVisitor, className: String): ClassVisitor {
+ // Connect to the base visitor
+ var outVisitor: ClassVisitor = base
- // Connect to the class writer
- var outVisitor: ClassVisitor = cw
if (options.enableClassChecker.get) {
outVisitor = CheckClassAdapter(outVisitor)
}
@@ -149,15 +150,59 @@ class HostStubGenClassProcessor(
val visitorOptions = BaseAdapter.Options(
errors = errors,
stats = stats,
- enablePreTrace = options.enablePreTrace.get,
- enablePostTrace = options.enablePostTrace.get,
deleteClassFinals = options.deleteFinals.get,
deleteMethodFinals = options.deleteFinals.get,
)
- outVisitor = BaseAdapter.getVisitor(
- cr.className, allClasses, outVisitor, filter,
- packageRedirector, visitorOptions
- )
+
+ val verbosePrinter = PrintWriter(log.getWriter(LogLevel.Verbose))
+
+ // Inject TraceClassVisitor for debugging.
+ if (options.enablePostTrace.get) {
+ outVisitor = TraceClassVisitor(outVisitor, verbosePrinter)
+ }
+
+ // Handle --package-redirect
+ if (!packageRedirector.isEmpty) {
+ // Don't apply the remapper on redirect-from classes.
+ // Otherwise, if the target jar actually contains the "from" classes (which
+ // may or may not be the case) they'd be renamed.
+ // But we update all references in other places, so, a method call to a "from" class
+ // would be replaced with the "to" class. All type references (e.g. variable types)
+ // will be updated too.
+ if (!packageRedirector.isTarget(className)) {
+ outVisitor = ClassRemapper(outVisitor, packageRedirector)
+ } else {
+ log.v(
+ "Class $className is a redirect-from class, not applying" +
+ " --package-redirect"
+ )
+ }
+ }
+
+ outVisitor = ImplGeneratingAdapter(allClasses, outVisitor, filter, visitorOptions)
+
+ // Inject TraceClassVisitor for debugging.
+ if (options.enablePreTrace.get) {
+ outVisitor = TraceClassVisitor(outVisitor, verbosePrinter)
+ }
+
+ return outVisitor
+ }
+
+ fun processClassBytecode(bytecode: ByteArray): ByteArray {
+ val cr = ClassReader(bytecode)
+
+ // If the class was already processed previously, skip
+ val clz = allClasses.getClass(cr.className)
+ if (clz.findAnyAnnotation(processedAnnotation) != null) {
+ return bytecode
+ }
+
+ // COMPUTE_FRAMES wouldn't be happy if code uses
+ val flags = ClassWriter.COMPUTE_MAXS // or ClassWriter.COMPUTE_FRAMES
+ val cw = ClassWriter(flags)
+
+ val outVisitor = buildVisitor(cw, cr.className)
cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
return cw.toByteArray()
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt
index c7c45e65a8b6..e7166f11f597 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenClassProcessorOptions.kt
@@ -18,6 +18,7 @@ package com.android.hoststubgen
import com.android.hoststubgen.filters.FilterPolicy
import com.android.hoststubgen.utils.ArgIterator
import com.android.hoststubgen.utils.BaseOptions
+import com.android.hoststubgen.utils.FileOrResource
import com.android.hoststubgen.utils.SetOnce
private fun parsePackageRedirect(fromColonTo: String): Pair<String, String> {
@@ -53,7 +54,7 @@ open class HostStubGenClassProcessorOptions(
var defaultClassLoadHook: SetOnce<String?> = SetOnce(null),
var defaultMethodCallHook: SetOnce<String?> = SetOnce(null),
- var policyOverrideFiles: MutableList<String> = mutableListOf(),
+ var policyOverrideFiles: MutableList<FileOrResource> = mutableListOf(),
var defaultPolicy: SetOnce<FilterPolicy> = SetOnce(FilterPolicy.Remove),
@@ -73,15 +74,14 @@ open class HostStubGenClassProcessorOptions(
return name
}
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
// Define some shorthands...
- fun nextArg(): String = ai.nextArgRequired(option)
+ fun nextArg(): String = args.nextArgRequired(option)
fun MutableSet<String>.addUniqueAnnotationArg(): String =
nextArg().also { this += ensureUniqueAnnotation(it) }
when (option) {
- "--policy-override-file" ->
- policyOverrideFiles.add(nextArg().ensureFileExists())
+ "--policy-override-file" -> policyOverrideFiles.add(FileOrResource(nextArg()))
"--default-remove" -> defaultPolicy.set(FilterPolicy.Remove)
"--default-throw" -> defaultPolicy.set(FilterPolicy.Throw)
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
index b41ce0f65017..112ef01e20cb 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
@@ -217,7 +217,7 @@ private val numericalInnerClassName = """.*\$\d+$""".toRegex()
fun isAnonymousInnerClass(cn: ClassNode): Boolean {
// TODO: Is there a better way?
- return cn.name.matches(numericalInnerClassName)
+ return cn.outerClass != null && cn.name.matches(numericalInnerClassName)
}
/**
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt
index b8b0d8a31268..7f36aca33eee 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/DelegatingFilter.kt
@@ -19,9 +19,9 @@ package com.android.hoststubgen.filters
* Base class for an [OutputFilter] that uses another filter as a fallback.
*/
abstract class DelegatingFilter(
- // fallback shouldn't be used by subclasses directly, so make it private.
- // They should instead be calling into `super` or `outermostFilter`.
- private val fallback: OutputFilter
+ // fallback shouldn't be used by subclasses directly, so make it private.
+ // They should instead be calling into `super` or `outermostFilter`.
+ private val fallback: OutputFilter
) : OutputFilter() {
init {
fallback.outermostFilter = this
@@ -50,24 +50,24 @@ abstract class DelegatingFilter(
}
override fun getPolicyForField(
- className: String,
- fieldName: String
+ className: String,
+ fieldName: String
): FilterPolicyWithReason {
return fallback.getPolicyForField(className, fieldName)
}
override fun getPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String
+ className: String,
+ methodName: String,
+ descriptor: String
): FilterPolicyWithReason {
return fallback.getPolicyForMethod(className, methodName, descriptor)
}
override fun getRenameTo(
- className: String,
- methodName: String,
- descriptor: String
+ className: String,
+ methodName: String,
+ descriptor: String
): String? {
return fallback.getRenameTo(className, methodName, descriptor)
}
@@ -97,13 +97,12 @@ abstract class DelegatingFilter(
}
override fun getMethodCallReplaceTo(
- callerClassName: String,
- callerMethodName: String,
className: String,
methodName: String,
descriptor: String,
): MethodReplaceTarget? {
return fallback.getMethodCallReplaceTo(
- callerClassName, callerMethodName, className, methodName, descriptor)
+ className, methodName, descriptor
+ )
}
}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
index 474da6dfa1b9..d44d016f7c5b 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
@@ -16,7 +16,6 @@
package com.android.hoststubgen.filters
import com.android.hoststubgen.HostStubGenErrors
-import com.android.hoststubgen.HostStubGenInternalException
import com.android.hoststubgen.asm.CLASS_INITIALIZER_DESC
import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME
import com.android.hoststubgen.asm.ClassNodes
@@ -37,19 +36,15 @@ import org.objectweb.asm.tree.ClassNode
* TODO: Do we need a way to make anonymous class methods and lambdas "throw"?
*/
class ImplicitOutputFilter(
- private val errors: HostStubGenErrors,
- private val classes: ClassNodes,
- fallback: OutputFilter
+ private val errors: HostStubGenErrors,
+ private val classes: ClassNodes,
+ fallback: OutputFilter
) : DelegatingFilter(fallback) {
- private fun getClassImplicitPolicy(className: String, cn: ClassNode): FilterPolicyWithReason? {
+ private fun getClassImplicitPolicy(cn: ClassNode): FilterPolicyWithReason? {
if (isAnonymousInnerClass(cn)) {
log.forDebug {
// log.d(" anon-inner class: ${className} outer: ${cn.outerClass} ")
}
- if (cn.outerClass == null) {
- throw HostStubGenInternalException(
- "outerClass is null for anonymous inner class")
- }
// If the outer class needs to be in impl, it should be in impl too.
val outerPolicy = outermostFilter.getPolicyForClass(cn.outerClass)
if (outerPolicy.policy.needsInOutput) {
@@ -65,15 +60,15 @@ class ImplicitOutputFilter(
val cn = classes.getClass(className)
// Use the implicit policy, if any.
- getClassImplicitPolicy(className, cn)?.let { return it }
+ getClassImplicitPolicy(cn)?.let { return it }
return fallback
}
override fun getPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String
+ className: String,
+ methodName: String,
+ descriptor: String
): FilterPolicyWithReason {
val fallback = super.getPolicyForMethod(className, methodName, descriptor)
val classPolicy = outermostFilter.getPolicyForClass(className)
@@ -84,12 +79,14 @@ class ImplicitOutputFilter(
// "keep" instead.
// Unless it's an enum -- in that case, the below code would handle it.
if (!cn.isEnum() &&
- fallback.policy == FilterPolicy.Throw &&
- methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC) {
+ fallback.policy == FilterPolicy.Throw &&
+ methodName == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC
+ ) {
// TODO Maybe show a warning?? But that'd be too noisy with --default-throw.
return FilterPolicy.Ignore.withReason(
"'throw' on static initializer is handled as 'ignore'" +
- " [original throw reason: ${fallback.reason}]")
+ " [original throw reason: ${fallback.reason}]"
+ )
}
log.d("Class ${cn.name} Class policy: $classPolicy")
@@ -120,7 +117,8 @@ class ImplicitOutputFilter(
// For synthetic methods (such as lambdas), let's just inherit the class's
// policy.
return memberPolicy.withReason(classPolicy.reason).wrapReason(
- "is-synthetic-method")
+ "is-synthetic-method"
+ )
}
}
}
@@ -129,8 +127,8 @@ class ImplicitOutputFilter(
}
override fun getPolicyForField(
- className: String,
- fieldName: String
+ className: String,
+ fieldName: String
): FilterPolicyWithReason {
val fallback = super.getPolicyForField(className, fieldName)
@@ -161,4 +159,4 @@ class ImplicitOutputFilter(
return fallback
}
-} \ No newline at end of file
+}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
index fc885d6f463b..59da3da99ea5 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/InMemoryOutputFilter.kt
@@ -28,10 +28,12 @@ class InMemoryOutputFilter(
private val classes: ClassNodes,
fallback: OutputFilter,
) : DelegatingFilter(fallback) {
- private val mPolicies: MutableMap<String, FilterPolicyWithReason> = mutableMapOf()
- private val mRenames: MutableMap<String, String> = mutableMapOf()
- private val mRedirectionClasses: MutableMap<String, String> = mutableMapOf()
- private val mClassLoadHooks: MutableMap<String, String> = mutableMapOf()
+ private val mPolicies = mutableMapOf<String, FilterPolicyWithReason>()
+ private val mRenames = mutableMapOf<String, String>()
+ private val mRedirectionClasses = mutableMapOf<String, String>()
+ private val mClassLoadHooks = mutableMapOf<String, String>()
+ private val mMethodCallReplaceSpecs = mutableListOf<MethodCallReplaceSpec>()
+ private val mTypeRenameSpecs = mutableListOf<TypeRenameSpec>()
private fun getClassKey(className: String): String {
return className.toHumanReadableClassName()
@@ -45,10 +47,6 @@ class InMemoryOutputFilter(
return getClassKey(className) + "." + methodName + ";" + signature
}
- override fun getPolicyForClass(className: String): FilterPolicyWithReason {
- return mPolicies[getClassKey(className)] ?: super.getPolicyForClass(className)
- }
-
private fun checkClass(className: String) {
if (classes.findClass(className) == null) {
log.w("Unknown class $className")
@@ -74,6 +72,10 @@ class InMemoryOutputFilter(
}
}
+ override fun getPolicyForClass(className: String): FilterPolicyWithReason {
+ return mPolicies[getClassKey(className)] ?: super.getPolicyForClass(className)
+ }
+
fun setPolicyForClass(className: String, policy: FilterPolicyWithReason) {
checkClass(className)
mPolicies[getClassKey(className)] = policy
@@ -81,7 +83,7 @@ class InMemoryOutputFilter(
override fun getPolicyForField(className: String, fieldName: String): FilterPolicyWithReason {
return mPolicies[getFieldKey(className, fieldName)]
- ?: super.getPolicyForField(className, fieldName)
+ ?: super.getPolicyForField(className, fieldName)
}
fun setPolicyForField(className: String, fieldName: String, policy: FilterPolicyWithReason) {
@@ -90,21 +92,21 @@ class InMemoryOutputFilter(
}
override fun getPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String,
- ): FilterPolicyWithReason {
+ className: String,
+ methodName: String,
+ descriptor: String,
+ ): FilterPolicyWithReason {
return mPolicies[getMethodKey(className, methodName, descriptor)]
?: mPolicies[getMethodKey(className, methodName, "*")]
?: super.getPolicyForMethod(className, methodName, descriptor)
}
fun setPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String,
- policy: FilterPolicyWithReason,
- ) {
+ className: String,
+ methodName: String,
+ descriptor: String,
+ policy: FilterPolicyWithReason,
+ ) {
checkMethod(className, methodName, descriptor)
mPolicies[getMethodKey(className, methodName, descriptor)] = policy
}
@@ -123,7 +125,7 @@ class InMemoryOutputFilter(
override fun getRedirectionClass(className: String): String? {
return mRedirectionClasses[getClassKey(className)]
- ?: super.getRedirectionClass(className)
+ ?: super.getRedirectionClass(className)
}
fun setRedirectionClass(from: String, to: String) {
@@ -135,11 +137,52 @@ class InMemoryOutputFilter(
}
override fun getClassLoadHooks(className: String): List<String> {
- return addNonNullElement(super.getClassLoadHooks(className),
- mClassLoadHooks[getClassKey(className)])
+ return addNonNullElement(
+ super.getClassLoadHooks(className),
+ mClassLoadHooks[getClassKey(className)]
+ )
}
fun setClassLoadHook(className: String, methodName: String) {
mClassLoadHooks[getClassKey(className)] = methodName.toHumanReadableMethodName()
}
+
+ override fun hasAnyMethodCallReplace(): Boolean {
+ return mMethodCallReplaceSpecs.isNotEmpty() || super.hasAnyMethodCallReplace()
+ }
+
+ override fun getMethodCallReplaceTo(
+ className: String,
+ methodName: String,
+ descriptor: String,
+ ): MethodReplaceTarget? {
+ // Maybe use 'Tri' if we end up having too many replacements.
+ mMethodCallReplaceSpecs.forEach {
+ if (className == it.fromClass &&
+ methodName == it.fromMethod
+ ) {
+ if (it.fromDescriptor == "*" || descriptor == it.fromDescriptor) {
+ return MethodReplaceTarget(it.toClass, it.toMethod)
+ }
+ }
+ }
+ return super.getMethodCallReplaceTo(className, methodName, descriptor)
+ }
+
+ fun setMethodCallReplaceSpec(spec: MethodCallReplaceSpec) {
+ mMethodCallReplaceSpecs.add(spec)
+ }
+
+ override fun remapType(className: String): String? {
+ mTypeRenameSpecs.forEach {
+ if (it.typeInternalNamePattern.matcher(className).matches()) {
+ return it.typeInternalNamePrefix + className
+ }
+ }
+ return super.remapType(className)
+ }
+
+ fun setRemapTypeSpec(spec: TypeRenameSpec) {
+ mTypeRenameSpecs.add(spec)
+ }
}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt
index f99ce906240a..c47bb302920f 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/OutputFilter.kt
@@ -41,10 +41,10 @@ abstract class OutputFilter {
abstract fun getPolicyForField(className: String, fieldName: String): FilterPolicyWithReason
abstract fun getPolicyForMethod(
- className: String,
- methodName: String,
- descriptor: String,
- ): FilterPolicyWithReason
+ className: String,
+ methodName: String,
+ descriptor: String,
+ ): FilterPolicyWithReason
/**
* If a given method is a substitute-from method, return the substitute-to method name.
@@ -108,8 +108,6 @@ abstract class OutputFilter {
* If a method call should be forwarded to another method, return the target's class / method.
*/
open fun getMethodCallReplaceTo(
- callerClassName: String,
- callerMethodName: String,
className: String,
methodName: String,
descriptor: String,
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index dd353e9caeff..97fc35302528 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -22,13 +22,13 @@ import com.android.hoststubgen.asm.toHumanReadableClassName
import com.android.hoststubgen.asm.toJvmClassName
import com.android.hoststubgen.log
import com.android.hoststubgen.normalizeTextLine
+import com.android.hoststubgen.utils.FileOrResource
import com.android.hoststubgen.whitespaceRegex
-import org.objectweb.asm.tree.ClassNode
import java.io.BufferedReader
-import java.io.FileReader
import java.io.PrintWriter
import java.io.Reader
import java.util.regex.Pattern
+import org.objectweb.asm.tree.ClassNode
/**
* Print a class node as a "keep" policy.
@@ -58,6 +58,23 @@ enum class SpecialClass {
RFile,
}
+data class MethodCallReplaceSpec(
+ val fromClass: String,
+ val fromMethod: String,
+ val fromDescriptor: String,
+ val toClass: String,
+ val toMethod: String,
+)
+
+/**
+ * When a package name matches [typeInternalNamePattern], we prepend [typeInternalNamePrefix]
+ * to it.
+ */
+data class TypeRenameSpec(
+ val typeInternalNamePattern: Pattern,
+ val typeInternalNamePrefix: String,
+)
+
/**
* This receives [TextFileFilterPolicyBuilder] parsing result.
*/
@@ -99,7 +116,7 @@ interface PolicyFileProcessor {
className: String,
methodName: String,
methodDesc: String,
- replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+ replaceSpec: MethodCallReplaceSpec,
)
}
@@ -116,9 +133,6 @@ class TextFileFilterPolicyBuilder(
private var featureFlagsPolicy: FilterPolicyWithReason? = null
private var syspropsPolicy: FilterPolicyWithReason? = null
private var rFilePolicy: FilterPolicyWithReason? = null
- private val typeRenameSpec = mutableListOf<TextFilePolicyRemapperFilter.TypeRenameSpec>()
- private val methodReplaceSpec =
- mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>()
/**
* Fields for a filter chain used for "partial allowlisting", which are used by
@@ -126,47 +140,34 @@ class TextFileFilterPolicyBuilder(
*/
private val annotationAllowedInMemoryFilter: InMemoryOutputFilter
val annotationAllowedMembersFilter: OutputFilter
+ get() = annotationAllowedInMemoryFilter
private val annotationAllowedPolicy = FilterPolicy.AnnotationAllowed.withReason(FILTER_REASON)
init {
// Create a filter that checks "partial allowlisting".
- var aaf: OutputFilter = ConstantFilter(FilterPolicy.Remove, "default disallowed")
-
- aaf = InMemoryOutputFilter(classes, aaf)
- annotationAllowedInMemoryFilter = aaf
-
- annotationAllowedMembersFilter = annotationAllowedInMemoryFilter
+ val filter = ConstantFilter(FilterPolicy.Remove, "default disallowed")
+ annotationAllowedInMemoryFilter = InMemoryOutputFilter(classes, filter)
}
/**
* Parse a given policy file. This method can be called multiple times to read from
* multiple files. To get the resulting filter, use [createOutputFilter]
*/
- fun parse(file: String) {
+ fun parse(file: FileOrResource) {
// We may parse multiple files, but we reuse the same parser, because the parser
// will make sure there'll be no dupplicating "special class" policies.
- parser.parse(FileReader(file), file, Processor())
+ parser.parse(file.open(), file.path, Processor())
}
/**
* Generate the resulting [OutputFilter].
*/
fun createOutputFilter(): OutputFilter {
- var ret: OutputFilter = imf
- if (typeRenameSpec.isNotEmpty()) {
- ret = TextFilePolicyRemapperFilter(typeRenameSpec, ret)
- }
- if (methodReplaceSpec.isNotEmpty()) {
- ret = TextFilePolicyMethodReplaceFilter(methodReplaceSpec, classes, ret)
- }
-
// Wrap the in-memory-filter with AHF.
- ret = AndroidHeuristicsFilter(
- classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, ret
+ return AndroidHeuristicsFilter(
+ classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, imf
)
-
- return ret
}
private inner class Processor : PolicyFileProcessor {
@@ -180,9 +181,7 @@ class TextFileFilterPolicyBuilder(
}
override fun onRename(pattern: Pattern, prefix: String) {
- typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec(
- pattern, prefix
- )
+ imf.setRemapTypeSpec(TypeRenameSpec(pattern, prefix))
}
override fun onClassStart(className: String) {
@@ -284,12 +283,12 @@ class TextFileFilterPolicyBuilder(
className: String,
methodName: String,
methodDesc: String,
- replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+ replaceSpec: MethodCallReplaceSpec,
) {
// Keep the source method, because the target method may call it.
imf.setPolicyForMethod(className, methodName, methodDesc,
FilterPolicy.Keep.withReason(FILTER_REASON))
- methodReplaceSpec.add(replaceSpec)
+ imf.setMethodCallReplaceSpec(replaceSpec)
}
}
}
@@ -630,13 +629,13 @@ class TextFileFilterPolicyParser {
if (classAndMethod != null) {
// If the substitution target contains a ".", then
// it's a method call redirect.
- val spec = TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec(
- currentClassName!!.toJvmClassName(),
- methodName,
- signature,
- classAndMethod.first.toJvmClassName(),
- classAndMethod.second,
- )
+ val spec = MethodCallReplaceSpec(
+ className.toJvmClassName(),
+ methodName,
+ signature,
+ classAndMethod.first.toJvmClassName(),
+ classAndMethod.second,
+ )
processor.onMethodOutClassReplace(
className,
methodName,
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt
deleted file mode 100644
index a3f934cacc2c..000000000000
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyMethodReplaceFilter.kt
+++ /dev/null
@@ -1,60 +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.hoststubgen.filters
-
-import com.android.hoststubgen.asm.ClassNodes
-
-/**
- * Filter used by TextFileFilterPolicyParser for "method call relacement".
- */
-class TextFilePolicyMethodReplaceFilter(
- val spec: List<MethodCallReplaceSpec>,
- val classes: ClassNodes,
- val fallback: OutputFilter,
-) : DelegatingFilter(fallback) {
-
- data class MethodCallReplaceSpec(
- val fromClass: String,
- val fromMethod: String,
- val fromDescriptor: String,
- val toClass: String,
- val toMethod: String,
- )
-
- override fun hasAnyMethodCallReplace(): Boolean {
- return true
- }
-
- override fun getMethodCallReplaceTo(
- callerClassName: String,
- callerMethodName: String,
- className: String,
- methodName: String,
- descriptor: String,
- ): MethodReplaceTarget? {
- // Maybe use 'Tri' if we end up having too many replacements.
- spec.forEach {
- if (className == it.fromClass &&
- methodName == it.fromMethod
- ) {
- if (it.fromDescriptor == "*" || descriptor == it.fromDescriptor) {
- return MethodReplaceTarget(it.toClass, it.toMethod)
- }
- }
- }
- return null
- }
-}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt
deleted file mode 100644
index bc90d1248322..000000000000
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/TextFilePolicyRemapperFilter.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.hoststubgen.filters
-
-import java.util.regex.Pattern
-
-/**
- * A filter that provides a simple "jarjar" functionality via [mapType]
- */
-class TextFilePolicyRemapperFilter(
- val typeRenameSpecs: List<TypeRenameSpec>,
- fallback: OutputFilter,
-) : DelegatingFilter(fallback) {
- /**
- * When a package name matches [typeInternalNamePattern], we prepend [typeInternalNamePrefix]
- * to it.
- */
- data class TypeRenameSpec(
- val typeInternalNamePattern: Pattern,
- val typeInternalNamePrefix: String,
- )
-
- override fun remapType(className: String): String? {
- typeRenameSpecs.forEach {
- if (it.typeInternalNamePattern.matcher(className).matches()) {
- return it.typeInternalNamePrefix + className
- }
- }
- return null
- }
-}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt
index 0b17879b862c..d0869929edfb 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/utils/OptionUtils.kt
@@ -16,11 +16,16 @@
package com.android.hoststubgen.utils
import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
+import com.android.hoststubgen.InputFileNotFoundException
+import com.android.hoststubgen.JarResourceNotFoundException
import com.android.hoststubgen.log
import com.android.hoststubgen.normalizeTextLine
-import java.io.BufferedReader
+import java.io.File
import java.io.FileReader
+import java.io.InputStreamReader
+import java.io.Reader
+
+const val JAR_RESOURCE_PREFIX = "jar:"
/**
* Base class for parsing arguments from commandline.
@@ -73,7 +78,7 @@ abstract class BaseOptions {
*
* Subclasses override/extend this method to support more options.
*/
- abstract fun parseOption(option: String, ai: ArgIterator): Boolean
+ abstract fun parseOption(option: String, args: ArgIterator): Boolean
abstract fun dumpFields(): String
}
@@ -112,7 +117,9 @@ class ArgIterator(
companion object {
fun withAtFiles(args: List<String>): ArgIterator {
- return ArgIterator(expandAtFiles(args))
+ val expanded = mutableListOf<String>()
+ expandAtFiles(args.asSequence(), expanded)
+ return ArgIterator(expanded)
}
/**
@@ -125,34 +132,30 @@ class ArgIterator(
*
* The file can contain '#' as comments.
*/
- private fun expandAtFiles(args: List<String>): List<String> {
- val ret = mutableListOf<String>()
-
+ private fun expandAtFiles(args: Sequence<String>, out: MutableList<String>) {
args.forEach { arg ->
if (arg.startsWith("@@")) {
- ret += arg.substring(1)
+ out.add(arg.substring(1))
return@forEach
} else if (!arg.startsWith('@')) {
- ret += arg
+ out.add(arg)
return@forEach
}
+
// Read from the file, and add each line to the result.
- val filename = arg.substring(1).ensureFileExists()
+ val file = FileOrResource(arg.substring(1))
- log.v("Expanding options file $filename")
+ log.v("Expanding options file ${file.path}")
- BufferedReader(FileReader(filename)).use { reader ->
- while (true) {
- var line = reader.readLine() ?: break // EOF
+ val fileArgs = file
+ .open()
+ .buffered()
+ .lineSequence()
+ .map(::normalizeTextLine)
+ .filter(CharSequence::isNotEmpty)
- line = normalizeTextLine(line)
- if (line.isNotEmpty()) {
- ret += line
- }
- }
- }
+ expandAtFiles(fileArgs, out)
}
- return ret
}
}
}
@@ -204,3 +207,37 @@ class IntSetOnce(value: Int) : SetOnce<Int>(value) {
}
}
}
+
+/**
+ * A path either points to a file in filesystem, or an entry in the JAR.
+ */
+class FileOrResource(val path: String) {
+ init {
+ path.ensureFileExists()
+ }
+
+ /**
+ * Either read from filesystem, or read from JAR resources.
+ */
+ fun open(): Reader {
+ return if (path.startsWith(JAR_RESOURCE_PREFIX)) {
+ val path = path.removePrefix(JAR_RESOURCE_PREFIX)
+ InputStreamReader(this::class.java.classLoader.getResourceAsStream(path)!!)
+ } else {
+ FileReader(path)
+ }
+ }
+}
+
+fun String.ensureFileExists(): String {
+ if (this.startsWith(JAR_RESOURCE_PREFIX)) {
+ val cl = FileOrResource::class.java.classLoader
+ val path = this.removePrefix(JAR_RESOURCE_PREFIX)
+ if (cl.getResource(path) == null) {
+ throw JarResourceNotFoundException(path)
+ }
+ } else if (!File(this).exists()) {
+ throw InputFileNotFoundException(this)
+ }
+ return this
+}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt
index a08d1d605949..769b769d7a20 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/BaseAdapter.kt
@@ -17,7 +17,6 @@ package com.android.hoststubgen.visitors
import com.android.hoststubgen.HostStubGenErrors
import com.android.hoststubgen.HostStubGenStats
-import com.android.hoststubgen.LogLevel
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.UnifiedVisitor
import com.android.hoststubgen.asm.getPackageNameFromFullClassName
@@ -26,13 +25,10 @@ import com.android.hoststubgen.filters.FilterPolicyWithReason
import com.android.hoststubgen.filters.OutputFilter
import com.android.hoststubgen.hosthelper.HostStubGenProcessedAsKeep
import com.android.hoststubgen.log
-import java.io.PrintWriter
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.FieldVisitor
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
-import org.objectweb.asm.commons.ClassRemapper
-import org.objectweb.asm.util.TraceClassVisitor
const val OPCODE_VERSION = Opcodes.ASM9
@@ -49,8 +45,6 @@ abstract class BaseAdapter(
data class Options(
val errors: HostStubGenErrors,
val stats: HostStubGenStats?,
- val enablePreTrace: Boolean,
- val enablePostTrace: Boolean,
val deleteClassFinals: Boolean,
val deleteMethodFinals: Boolean,
// We don't remove finals from fields, because final fields have a stronger memory
@@ -253,50 +247,4 @@ abstract class BaseAdapter(
substituted: Boolean,
superVisitor: MethodVisitor?,
): MethodVisitor?
-
- companion object {
- fun getVisitor(
- classInternalName: String,
- classes: ClassNodes,
- nextVisitor: ClassVisitor,
- filter: OutputFilter,
- packageRedirector: PackageRedirectRemapper,
- options: Options,
- ): ClassVisitor {
- var next = nextVisitor
-
- val verbosePrinter = PrintWriter(log.getWriter(LogLevel.Verbose))
-
- // Inject TraceClassVisitor for debugging.
- if (options.enablePostTrace) {
- next = TraceClassVisitor(next, verbosePrinter)
- }
-
- // Handle --package-redirect
- if (!packageRedirector.isEmpty) {
- // Don't apply the remapper on redirect-from classes.
- // Otherwise, if the target jar actually contains the "from" classes (which
- // may or may not be the case) they'd be renamed.
- // But we update all references in other places, so, a method call to a "from" class
- // would be replaced with the "to" class. All type references (e.g. variable types)
- // will be updated too.
- if (!packageRedirector.isTarget(classInternalName)) {
- next = ClassRemapper(next, packageRedirector)
- } else {
- log.v(
- "Class $classInternalName is a redirect-from class, not applying" +
- " --package-redirect"
- )
- }
- }
-
- next = ImplGeneratingAdapter(classes, next, filter, options)
-
- // Inject TraceClassVisitor for debugging.
- if (options.enablePreTrace) {
- next = TraceClassVisitor(next, verbosePrinter)
- }
- return next
- }
- }
}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
index b8a357668c2b..617385ad438e 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
@@ -396,7 +396,7 @@ class ImplGeneratingAdapter(
}
val to = filter.getMethodCallReplaceTo(
- currentClassName, callerMethodName, owner, name, descriptor
+ owner, name, descriptor
)
if (to == null
diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index 8bb454fa12e7..d9cc54aebf51 100644
--- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -18,6 +18,7 @@ package com.android.hoststubgen
import com.android.hoststubgen.utils.ArgIterator
import com.android.hoststubgen.utils.IntSetOnce
import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
/**
* Options that can be set from command line arguments.
@@ -61,9 +62,9 @@ class HostStubGenOptions(
}
}
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
// Define some shorthands...
- fun nextArg(): String = ai.nextArgRequired(option)
+ fun nextArg(): String = args.nextArgRequired(option)
when (option) {
// TODO: Write help
@@ -94,7 +95,7 @@ class HostStubGenOptions(
}
}
- else -> return super.parseOption(option, ai)
+ else -> return super.parseOption(option, args)
}
return true
diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt
index f7fd0804c151..58bd9e987fd1 100644
--- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt
+++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaOptions.kt
@@ -16,10 +16,10 @@
package com.android.platform.test.ravenwood.ravenhelper.policytoannot
import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
import com.android.hoststubgen.utils.ArgIterator
import com.android.hoststubgen.utils.BaseOptions
import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
/**
* Options for the "ravenhelper pta" subcommand.
@@ -41,8 +41,8 @@ class PtaOptions(
var dumpOperations: SetOnce<Boolean> = SetOnce(false),
) : BaseOptions() {
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
- fun nextArg(): String = ai.nextArgRequired(option)
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
+ fun nextArg(): String = args.nextArgRequired(option)
when (option) {
// TODO: Write help
diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt
index fd6f732a06ce..5ce9a23e6e05 100644
--- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt
+++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/policytoannot/PtaProcessor.kt
@@ -19,10 +19,10 @@ import com.android.hoststubgen.LogLevel
import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME
import com.android.hoststubgen.asm.toJvmClassName
import com.android.hoststubgen.filters.FilterPolicyWithReason
+import com.android.hoststubgen.filters.MethodCallReplaceSpec
import com.android.hoststubgen.filters.PolicyFileProcessor
import com.android.hoststubgen.filters.SpecialClass
import com.android.hoststubgen.filters.TextFileFilterPolicyParser
-import com.android.hoststubgen.filters.TextFilePolicyMethodReplaceFilter
import com.android.hoststubgen.log
import com.android.hoststubgen.utils.ClassPredicate
import com.android.platform.test.ravenwood.ravenhelper.SubcommandHandler
@@ -448,7 +448,7 @@ private class TextPolicyToAnnotationConverter(
className: String,
methodName: String,
methodDesc: String,
- replaceSpec: TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec,
+ replaceSpec: MethodCallReplaceSpec,
) {
// This can't be converted to an annotation.
classHasMember = true
diff --git a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt
index 8b95843f08a6..6e0b7b89cf13 100644
--- a/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt
+++ b/ravenwood/tools/ravenhelper/src/com/android/platform/test/ravenwood/ravenhelper/sourcemap/MapOptions.kt
@@ -16,10 +16,10 @@
package com.android.platform.test.ravenwood.ravenhelper.sourcemap
import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
import com.android.hoststubgen.utils.ArgIterator
import com.android.hoststubgen.utils.BaseOptions
import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
/**
* Options for the "ravenhelper map" subcommand.
@@ -38,8 +38,8 @@ class MapOptions(
var text: SetOnce<String?> = SetOnce(null),
) : BaseOptions() {
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
- fun nextArg(): String = ai.nextArgRequired(option)
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
+ fun nextArg(): String = args.nextArgRequired(option)
when (option) {
// TODO: Write help
diff --git a/ravenwood/tools/ravenizer/Android.bp b/ravenwood/tools/ravenizer/Android.bp
index 957e20647d44..93cda4e3c4c9 100644
--- a/ravenwood/tools/ravenizer/Android.bp
+++ b/ravenwood/tools/ravenizer/Android.bp
@@ -15,5 +15,10 @@ java_binary_host {
"hoststubgen-lib",
"ravenwood-junit-for-ravenizer",
],
+ java_resources: [
+ ":ravenizer-standard-options",
+ ":ravenwood-standard-annotations",
+ ":ravenwood-common-policies",
+ ],
visibility: ["//visibility:public"],
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
index e67c730df069..04e3bda2ba27 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
@@ -16,23 +16,22 @@
package com.android.platform.test.ravenwood.ravenizer
import com.android.hoststubgen.GeneralUserErrorException
+import com.android.hoststubgen.HostStubGenClassProcessor
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.zipEntryNameToClassName
import com.android.hoststubgen.executableName
import com.android.hoststubgen.log
import com.android.platform.test.ravenwood.ravenizer.adapter.RunnerRewritingAdapter
-import org.objectweb.asm.ClassReader
-import org.objectweb.asm.ClassVisitor
-import org.objectweb.asm.ClassWriter
-import org.objectweb.asm.util.CheckClassAdapter
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.FileOutputStream
-import java.io.InputStream
-import java.io.OutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.util.CheckClassAdapter
/**
* Various stats on Ravenizer.
@@ -41,7 +40,7 @@ data class RavenizerStats(
/** Total end-to-end time. */
var totalTime: Double = .0,
- /** Time took to build [ClasNodes] */
+ /** Time took to build [ClassNodes] */
var loadStructureTime: Double = .0,
/** Time took to validate the classes */
@@ -50,14 +49,17 @@ data class RavenizerStats(
/** Total real time spent for converting the jar file */
var totalProcessTime: Double = .0,
- /** Total real time spent for converting class files (except for I/O time). */
- var totalConversionTime: Double = .0,
+ /** Total real time spent for ravenizing class files (excluding I/O time). */
+ var totalRavenizeTime: Double = .0,
+
+ /** Total real time spent for processing class files HSG style (excluding I/O time). */
+ var totalHostStubGenTime: Double = .0,
/** Total real time spent for copying class files without modification. */
var totalCopyTime: Double = .0,
/** # of entries in the input jar file */
- var totalEntiries: Int = 0,
+ var totalEntries: Int = 0,
/** # of *.class files in the input jar file */
var totalClasses: Int = 0,
@@ -67,14 +69,15 @@ data class RavenizerStats(
) {
override fun toString(): String {
return """
- RavenizerStats{
+ RavenizerStats {
totalTime=$totalTime,
loadStructureTime=$loadStructureTime,
validationTime=$validationTime,
totalProcessTime=$totalProcessTime,
- totalConversionTime=$totalConversionTime,
+ totalRavenizeTime=$totalRavenizeTime,
+ totalHostStubGenTime=$totalHostStubGenTime,
totalCopyTime=$totalCopyTime,
- totalEntiries=$totalEntiries,
+ totalEntries=$totalEntries,
totalClasses=$totalClasses,
processedClasses=$processedClasses,
}
@@ -90,12 +93,18 @@ class Ravenizer {
val stats = RavenizerStats()
stats.totalTime = log.nTime {
+ val allClasses = ClassNodes.loadClassStructures(options.inJar.get) {
+ stats.loadStructureTime = it
+ }
+ val processor = HostStubGenClassProcessor(options, allClasses)
+
process(
options.inJar.get,
options.outJar.get,
options.enableValidation.get,
options.fatalValidation.get,
options.stripMockito.get,
+ processor,
stats,
)
}
@@ -108,15 +117,13 @@ class Ravenizer {
enableValidation: Boolean,
fatalValidation: Boolean,
stripMockito: Boolean,
+ processor: HostStubGenClassProcessor,
stats: RavenizerStats,
) {
- var allClasses = ClassNodes.loadClassStructures(inJar) {
- time -> stats.loadStructureTime = time
- }
if (enableValidation) {
stats.validationTime = log.iTime("Validating classes") {
- if (!validateClasses(allClasses)) {
- var message = "Invalid test class(es) detected." +
+ if (!validateClasses(processor.allClasses)) {
+ val message = "Invalid test class(es) detected." +
" See error log for details."
if (fatalValidation) {
throw RavenizerInvalidTestException(message)
@@ -126,7 +133,7 @@ class Ravenizer {
}
}
}
- if (includeUnsupportedMockito(allClasses)) {
+ if (includeUnsupportedMockito(processor.allClasses)) {
log.w("Unsupported Mockito detected in $inJar!")
}
@@ -134,7 +141,7 @@ class Ravenizer {
ZipFile(inJar).use { inZip ->
val inEntries = inZip.entries()
- stats.totalEntiries = inZip.size()
+ stats.totalEntries = inZip.size()
ZipOutputStream(BufferedOutputStream(FileOutputStream(outJar))).use { outZip ->
while (inEntries.hasMoreElements()) {
@@ -159,9 +166,9 @@ class Ravenizer {
stats.totalClasses += 1
}
- if (className != null && shouldProcessClass(allClasses, className)) {
- stats.processedClasses += 1
- processSingleClass(inZip, entry, outZip, allClasses, stats)
+ if (className != null &&
+ shouldProcessClass(processor.allClasses, className)) {
+ processSingleClass(inZip, entry, outZip, processor, stats)
} else {
// Too slow, let's use merge_zips to bring back the original classes.
copyZipEntry(inZip, entry, outZip, stats)
@@ -201,14 +208,22 @@ class Ravenizer {
inZip: ZipFile,
entry: ZipEntry,
outZip: ZipOutputStream,
- allClasses: ClassNodes,
+ processor: HostStubGenClassProcessor,
stats: RavenizerStats,
) {
+ stats.processedClasses += 1
val newEntry = ZipEntry(entry.name)
outZip.putNextEntry(newEntry)
BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
- processSingleClass(entry, bis, outZip, allClasses, stats)
+ var classBytes = bis.readBytes()
+ stats.totalRavenizeTime += log.vTime("Ravenize ${entry.name}") {
+ classBytes = ravenizeSingleClass(entry, classBytes, processor.allClasses)
+ }
+ stats.totalHostStubGenTime += log.vTime("HostStubGen ${entry.name}") {
+ classBytes = processor.processClassBytecode(classBytes)
+ }
+ outZip.write(classBytes)
}
outZip.closeEntry()
}
@@ -217,41 +232,34 @@ class Ravenizer {
* Whether a class needs to be processed. This must be kept in sync with [processSingleClass].
*/
private fun shouldProcessClass(classes: ClassNodes, classInternalName: String): Boolean {
- return !classInternalName.shouldByBypassed()
+ return !classInternalName.shouldBypass()
&& RunnerRewritingAdapter.shouldProcess(classes, classInternalName)
}
- private fun processSingleClass(
+ private fun ravenizeSingleClass(
entry: ZipEntry,
- input: InputStream,
- output: OutputStream,
+ input: ByteArray,
allClasses: ClassNodes,
- stats: RavenizerStats,
- ) {
- val cr = ClassReader(input)
-
- lateinit var data: ByteArray
- stats.totalConversionTime += log.vTime("Modify ${entry.name}") {
+ ): ByteArray {
+ val classInternalName = zipEntryNameToClassName(entry.name)
+ ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}")
- val classInternalName = zipEntryNameToClassName(entry.name)
- ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}")
- val flags = ClassWriter.COMPUTE_MAXS
- val cw = ClassWriter(flags)
- var outVisitor: ClassVisitor = cw
+ val flags = ClassWriter.COMPUTE_MAXS
+ val cw = ClassWriter(flags)
+ var outVisitor: ClassVisitor = cw
- val enableChecker = false
- if (enableChecker) {
- outVisitor = CheckClassAdapter(outVisitor)
- }
+ val enableChecker = false
+ if (enableChecker) {
+ outVisitor = CheckClassAdapter(outVisitor)
+ }
- // This must be kept in sync with shouldProcessClass.
- outVisitor = RunnerRewritingAdapter.maybeApply(
- classInternalName, allClasses, outVisitor)
+ // This must be kept in sync with shouldProcessClass.
+ outVisitor = RunnerRewritingAdapter.maybeApply(
+ classInternalName, allClasses, outVisitor)
- cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
+ val cr = ClassReader(input)
+ cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
- data = cw.toByteArray()
- }
- output.write(data)
+ return cw.toByteArray()
}
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
index 7f4829ec6127..8a09e6d533b8 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
@@ -21,6 +21,7 @@ import com.android.hoststubgen.LogLevel
import com.android.hoststubgen.executableName
import com.android.hoststubgen.log
import com.android.hoststubgen.runMainWithBoilerplate
+import com.android.hoststubgen.utils.JAR_RESOURCE_PREFIX
import java.nio.file.Paths
import kotlin.io.path.exists
@@ -36,6 +37,10 @@ import kotlin.io.path.exists
*/
private val RAVENIZER_DOTFILE = System.getenv("HOME") + "/.ravenizer-unsafe"
+/**
+ * This is the name of the standard option text file embedded inside ravenizer.jar.
+ */
+private const val RAVENIZER_STANDARD_OPTIONS = "texts/ravenizer-standard-options.txt"
/**
* Entry point.
@@ -45,12 +50,12 @@ fun main(args: Array<String>) {
log.setConsoleLogLevel(LogLevel.Info)
runMainWithBoilerplate {
- var newArgs = args.asList()
+ val newArgs = args.toMutableList()
+ newArgs.add(0, "@$JAR_RESOURCE_PREFIX$RAVENIZER_STANDARD_OPTIONS")
+
if (Paths.get(RAVENIZER_DOTFILE).exists()) {
log.i("Reading options from $RAVENIZER_DOTFILE")
- newArgs = args.toMutableList().apply {
- add(0, "@$RAVENIZER_DOTFILE")
- }
+ newArgs.add(0, "@$RAVENIZER_DOTFILE")
}
val options = RavenizerOptions().apply { parseArgs(newArgs) }
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
index 2c0365404ab6..5d278bb046ae 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
@@ -16,10 +16,10 @@
package com.android.platform.test.ravenwood.ravenizer
import com.android.hoststubgen.ArgumentsException
-import com.android.hoststubgen.ensureFileExists
+import com.android.hoststubgen.HostStubGenClassProcessorOptions
import com.android.hoststubgen.utils.ArgIterator
-import com.android.hoststubgen.utils.BaseOptions
import com.android.hoststubgen.utils.SetOnce
+import com.android.hoststubgen.utils.ensureFileExists
class RavenizerOptions(
/** Input jar file*/
@@ -36,10 +36,10 @@ class RavenizerOptions(
/** Whether to remove mockito and dexmaker classes. */
var stripMockito: SetOnce<Boolean> = SetOnce(false),
-) : BaseOptions() {
+) : HostStubGenClassProcessorOptions() {
- override fun parseOption(option: String, ai: ArgIterator): Boolean {
- fun nextArg(): String = ai.nextArgRequired(option)
+ override fun parseOption(option: String, args: ArgIterator): Boolean {
+ fun nextArg(): String = args.nextArgRequired(option)
when (option) {
// TODO: Write help
@@ -57,7 +57,7 @@ class RavenizerOptions(
"--strip-mockito" -> stripMockito.set(true)
"--no-strip-mockito" -> stripMockito.set(false)
- else -> return false
+ else -> return super.parseOption(option, args)
}
return true
@@ -79,6 +79,6 @@ class RavenizerOptions(
enableValidation=$enableValidation,
fatalValidation=$fatalValidation,
stripMockito=$stripMockito,
- """.trimIndent()
+ """.trimIndent() + '\n' + super.dumpFields()
}
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
index 6092fcc9402d..b394a761c7ae 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
@@ -15,8 +15,8 @@
*/
package com.android.platform.test.ravenwood.ravenizer
-import android.platform.test.annotations.internal.InnerRunner
import android.platform.test.annotations.NoRavenizer
+import android.platform.test.annotations.internal.InnerRunner
import android.platform.test.ravenwood.RavenwoodAwareTestRunner
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.findAnyAnnotation
@@ -85,7 +85,7 @@ fun String.isRavenwoodClass(): Boolean {
/**
* Classes that should never be modified.
*/
-fun String.shouldByBypassed(): Boolean {
+fun String.shouldBypass(): Boolean {
if (this.isRavenwoodClass()) {
return true
}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
index 61e254b225c3..d252b4dc8ab6 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
@@ -20,8 +20,8 @@ import com.android.hoststubgen.asm.isAbstract
import com.android.hoststubgen.asm.startsWithAny
import com.android.hoststubgen.asm.toHumanReadableClassName
import com.android.hoststubgen.log
-import org.objectweb.asm.tree.ClassNode
import java.util.regex.Pattern
+import org.objectweb.asm.tree.ClassNode
fun validateClasses(classes: ClassNodes): Boolean {
var allOk = true
@@ -37,7 +37,7 @@ fun validateClasses(classes: ClassNodes): Boolean {
*
*/
fun checkClass(cn: ClassNode, classes: ClassNodes): Boolean {
- if (cn.name.shouldByBypassed()) {
+ if (cn.name.shouldBypass()) {
// Class doesn't need to be checked.
return true
}
@@ -145,4 +145,4 @@ com.android.server.power.stats.BatteryStatsTimerTest
private fun isAllowListedLegacyTest(targetClass: ClassNode): Boolean {
return allowListedLegacyTests.contains(targetClass.name)
-} \ No newline at end of file
+}
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index b52b3dabd47d..35db3c6f0a6d 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -260,6 +260,16 @@ flag {
}
flag {
+ name: "pointer_up_motion_event_in_touch_exploration"
+ namespace: "accessibility"
+ description: "Allows POINTER_UP motionEvents to trigger during touch exploration."
+ bug: "374930391"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "proxy_use_apps_on_virtual_device_listener"
namespace: "accessibility"
description: "Fixes race condition described in b/286587811"
@@ -336,3 +346,13 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "hearing_input_change_when_comm_device"
+ namespace: "accessibility"
+ description: "Listen to the CommunicationDeviceChanged to show hearing device input notification."
+ bug: "394070235"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index da36c6d8187d..6c26c1f74002 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -521,15 +521,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
@Nullable IBinder focusedToken) {
return AccessibilityManagerService.this.handleKeyGestureEvent(event);
}
-
- @Override
- public boolean isKeyGestureSupported(int gestureType) {
- return switch (gestureType) {
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAGNIFICATION,
- KeyGestureEvent.KEY_GESTURE_TYPE_ACTIVATE_SELECT_TO_SPEAK -> true;
- default -> false;
- };
- }
};
@VisibleForTesting
diff --git a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
index 10dffb59317e..805d7f820c8d 100644
--- a/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
+++ b/services/accessibility/java/com/android/server/accessibility/HearingDevicePhoneCallNotificationController.java
@@ -65,9 +65,9 @@ public class HearingDevicePhoneCallNotificationController {
private final Executor mCallbackExecutor;
public HearingDevicePhoneCallNotificationController(@NonNull Context context) {
- mTelephonyListener = new CallStateListener(context);
mTelephonyManager = context.getSystemService(TelephonyManager.class);
mCallbackExecutor = Executors.newSingleThreadExecutor();
+ mTelephonyListener = new CallStateListener(context, mCallbackExecutor);
}
@VisibleForTesting
@@ -109,14 +109,29 @@ public class HearingDevicePhoneCallNotificationController {
AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_BUILTIN_MIC, "");
private final Context mContext;
+ private final Executor mCommDeviceChangedExecutor;
+ private final AudioManager.OnCommunicationDeviceChangedListener mCommDeviceChangedListener;
private NotificationManager mNotificationManager;
private AudioManager mAudioManager;
private BroadcastReceiver mHearingDeviceActionReceiver;
private BluetoothDevice mHearingDevice;
+ private boolean mIsCommDeviceChangedRegistered = false;
private boolean mIsNotificationShown = false;
- CallStateListener(@NonNull Context context) {
+ CallStateListener(@NonNull Context context, @NonNull Executor executor) {
mContext = context;
+ mCommDeviceChangedExecutor = executor;
+ mCommDeviceChangedListener = device -> {
+ if (device == null) {
+ return;
+ }
+ mHearingDevice = getSupportedInputHearingDeviceInfo(List.of(device));
+ if (mHearingDevice != null) {
+ showNotificationIfNeeded();
+ } else {
+ dismissNotificationIfNeeded();
+ }
+ };
}
@Override
@@ -134,6 +149,11 @@ public class HearingDevicePhoneCallNotificationController {
}
if (state == TelephonyManager.CALL_STATE_IDLE) {
+ if (mIsCommDeviceChangedRegistered) {
+ mIsCommDeviceChangedRegistered = false;
+ mAudioManager.removeOnCommunicationDeviceChangedListener(
+ mCommDeviceChangedListener);
+ }
dismissNotificationIfNeeded();
if (mHearingDevice != null) {
@@ -143,10 +163,23 @@ public class HearingDevicePhoneCallNotificationController {
mHearingDevice = null;
}
if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
- mHearingDevice = getSupportedInputHearingDeviceInfo(
- mAudioManager.getAvailableCommunicationDevices());
- if (mHearingDevice != null) {
- showNotificationIfNeeded();
+ if (com.android.server.accessibility.Flags.hearingInputChangeWhenCommDevice()) {
+ AudioDeviceInfo commDevice = mAudioManager.getCommunicationDevice();
+ mHearingDevice = getSupportedInputHearingDeviceInfo(List.of(commDevice));
+ if (mHearingDevice != null) {
+ showNotificationIfNeeded();
+ } else {
+ mAudioManager.addOnCommunicationDeviceChangedListener(
+ mCommDeviceChangedExecutor,
+ mCommDeviceChangedListener);
+ mIsCommDeviceChangedRegistered = true;
+ }
+ } else {
+ mHearingDevice = getSupportedInputHearingDeviceInfo(
+ mAudioManager.getAvailableCommunicationDevices());
+ if (mHearingDevice != null) {
+ showNotificationIfNeeded();
+ }
}
}
}
@@ -264,6 +297,10 @@ public class HearingDevicePhoneCallNotificationController {
PendingIntent.FLAG_IMMUTABLE);
}
case ACTION_BLUETOOTH_DEVICE_DETAILS -> {
+ if (mHearingDevice == null) {
+ return null;
+ }
+
Bundle bundle = new Bundle();
bundle.putString(KEY_BLUETOOTH_ADDRESS, mHearingDevice.getAddress());
intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, bundle);
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 739ea0df87ab..a71224a68125 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java
@@ -124,6 +124,22 @@ public class AutoclickController extends BaseEventStreamTransformation {
}
};
+ @VisibleForTesting
+ final AutoclickScrollPanel.ScrollPanelControllerInterface mScrollPanelController =
+ new AutoclickScrollPanel.ScrollPanelControllerInterface() {
+ @Override
+ public void handleScroll(@AutoclickScrollPanel.ScrollDirection int direction) {
+ // TODO(b/388845721): Perform actual scroll.
+ }
+
+ @Override
+ public void exitScrollMode() {
+ if (mAutoclickScrollPanel != null) {
+ mAutoclickScrollPanel.hide();
+ }
+ }
+ };
+
public AutoclickController(Context context, int userId, AccessibilityTraceManager trace) {
mTrace = trace;
mContext = context;
@@ -168,7 +184,8 @@ public class AutoclickController extends BaseEventStreamTransformation {
mWindowManager = mContext.getSystemService(WindowManager.class);
mAutoclickTypePanel =
new AutoclickTypePanel(mContext, mWindowManager, mUserId, clickPanelController);
- mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager);
+ mAutoclickScrollPanel = new AutoclickScrollPanel(mContext, mWindowManager,
+ mScrollPanelController);
mAutoclickTypePanel.show();
mWindowManager.addView(mAutoclickIndicatorView, mAutoclickIndicatorView.getLayoutParams());
@@ -248,7 +265,11 @@ public class AutoclickController extends BaseEventStreamTransformation {
private boolean isPaused() {
return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isPaused()
- && !mAutoclickTypePanel.isHovered();
+ && !isHovered();
+ }
+
+ private boolean isHovered() {
+ return Flags.enableAutoclickIndicator() && mAutoclickTypePanel.isHovered();
}
private void cancelPendingClick() {
@@ -495,6 +516,8 @@ public class AutoclickController extends BaseEventStreamTransformation {
private int mEventPolicyFlags;
/** Current meta state. This value will be used as meta state for click event sequence. */
private int mMetaState;
+ /** Last observed panel hovered state when click was scheduled. */
+ private boolean mHoveredState;
/**
* The current anchor's coordinates. Should be ignored if #mLastMotionEvent is null.
@@ -648,6 +671,7 @@ public class AutoclickController extends BaseEventStreamTransformation {
}
mLastMotionEvent = MotionEvent.obtain(event);
mEventPolicyFlags = policyFlags;
+ mHoveredState = isHovered();
if (useAsAnchor) {
final int pointerIndex = mLastMotionEvent.getActionIndex();
@@ -729,14 +753,18 @@ public class AutoclickController extends BaseEventStreamTransformation {
final long now = SystemClock.uptimeMillis();
- // TODO(b/395094903): always triggers left-click when the cursor hovers over the
- // autoclick type panel, to always allow users to change a different click type.
- // Otherwise, if one chooses the right-click, this user won't be able to rely on
- // autoclick to select other click types.
- final int actionButton =
- mActiveClickType == AUTOCLICK_TYPE_RIGHT_CLICK
- ? BUTTON_SECONDARY
- : BUTTON_PRIMARY;
+ int actionButton;
+ if (mHoveredState) {
+ // Always triggers left-click when the cursor hovers over the autoclick type
+ // panel, to always allow users to change a different click type. Otherwise, if
+ // one chooses the right-click, this user won't be able to rely on autoclick to
+ // select other click types.
+ actionButton = BUTTON_PRIMARY;
+ } else {
+ actionButton = mActiveClickType == AUTOCLICK_TYPE_RIGHT_CLICK
+ ? BUTTON_SECONDARY
+ : BUTTON_PRIMARY;
+ }
MotionEvent downEvent =
MotionEvent.obtain(
diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
index 86f79a83ea28..e79be502a6fc 100644
--- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
+++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickScrollPanel.java
@@ -18,6 +18,7 @@ package com.android.server.accessibility.autoclick;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import android.annotation.IntDef;
import android.content.Context;
import android.graphics.PixelFormat;
import android.view.Gravity;
@@ -25,23 +26,97 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
+import android.widget.ImageButton;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.R;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
public class AutoclickScrollPanel {
+ public static final int DIRECTION_UP = 0;
+ public static final int DIRECTION_DOWN = 1;
+ public static final int DIRECTION_LEFT = 2;
+ public static final int DIRECTION_RIGHT = 3;
+
+ @IntDef({
+ DIRECTION_UP,
+ DIRECTION_DOWN,
+ DIRECTION_LEFT,
+ DIRECTION_RIGHT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ScrollDirection {}
+
private final Context mContext;
private final View mContentView;
private final WindowManager mWindowManager;
+ private ScrollPanelControllerInterface mScrollPanelController;
+
+ // Scroll panel buttons.
+ private final ImageButton mUpButton;
+ private final ImageButton mDownButton;
+ private final ImageButton mLeftButton;
+ private final ImageButton mRightButton;
+ private final ImageButton mExitButton;
+
private boolean mInScrollMode = false;
- public AutoclickScrollPanel(Context context, WindowManager windowManager) {
+ /**
+ * Interface for handling scroll operations.
+ */
+ public interface ScrollPanelControllerInterface {
+ /**
+ * Called when a scroll direction is hovered.
+ *
+ * @param direction The direction to scroll: one of {@link ScrollDirection} values.
+ */
+ void handleScroll(@ScrollDirection int direction);
+
+ /**
+ * Called when the exit button is hovered.
+ */
+ void exitScrollMode();
+ }
+
+ public AutoclickScrollPanel(Context context, WindowManager windowManager,
+ ScrollPanelControllerInterface controller) {
mContext = context;
mWindowManager = windowManager;
+ mScrollPanelController = controller;
mContentView = LayoutInflater.from(context).inflate(
R.layout.accessibility_autoclick_scroll_panel, null);
+
+ // Initialize buttons.
+ mUpButton = mContentView.findViewById(R.id.scroll_up);
+ mLeftButton = mContentView.findViewById(R.id.scroll_left);
+ mRightButton = mContentView.findViewById(R.id.scroll_right);
+ mDownButton = mContentView.findViewById(R.id.scroll_down);
+ mExitButton = mContentView.findViewById(R.id.scroll_exit);
+
+ initializeButtonState();
+ }
+
+ /**
+ * Sets up hover listeners for scroll panel buttons.
+ */
+ private void initializeButtonState() {
+ // Set up hover listeners for direction buttons.
+ setupHoverListenerForDirectionButton(mUpButton, DIRECTION_UP);
+ setupHoverListenerForDirectionButton(mLeftButton, DIRECTION_LEFT);
+ setupHoverListenerForDirectionButton(mRightButton, DIRECTION_RIGHT);
+ setupHoverListenerForDirectionButton(mDownButton, DIRECTION_DOWN);
+
+ // Set up hover listener for exit button.
+ mExitButton.setOnHoverListener((v, event) -> {
+ if (mScrollPanelController != null) {
+ mScrollPanelController.exitScrollMode();
+ }
+ return true;
+ });
}
/**
@@ -67,6 +142,19 @@ public class AutoclickScrollPanel {
}
/**
+ * Sets up a hover listener for a direction button.
+ */
+ private void setupHoverListenerForDirectionButton(ImageButton button,
+ @ScrollDirection int direction) {
+ button.setOnHoverListener((v, event) -> {
+ if (mScrollPanelController != null) {
+ mScrollPanelController.handleScroll(direction);
+ }
+ return true;
+ });
+ }
+
+ /**
* Retrieves the layout params for AutoclickScrollPanel, used when it's added to the Window
* Manager.
*/
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index fb329430acb2..b02fe2752a62 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -653,6 +653,14 @@ public class TouchExplorer extends BaseEventStreamTransformation
case ACTION_UP:
handleActionUp(event, rawEvent, policyFlags);
break;
+ case ACTION_POINTER_UP:
+ if (com.android.server.accessibility.Flags
+ .pointerUpMotionEventInTouchExploration()) {
+ if (mState.isServiceDetectingGestures()) {
+ mAms.sendMotionEventToListeningServices(rawEvent);
+ }
+ }
+ break;
default:
break;
}
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
index cd46b38272c2..568abd196735 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchState.java
@@ -26,6 +26,8 @@ import android.view.Display;
import android.view.MotionEvent;
import android.view.accessibility.AccessibilityEvent;
+import androidx.annotation.VisibleForTesting;
+
import com.android.server.accessibility.AccessibilityManagerService;
/**
@@ -73,7 +75,8 @@ public class TouchState {
private int mState = STATE_CLEAR;
// Helper class to track received pointers.
// Todo: collapse or hide this class so multiple classes don't modify it.
- private final ReceivedPointerTracker mReceivedPointerTracker;
+ @VisibleForTesting
+ public final ReceivedPointerTracker mReceivedPointerTracker;
// The most recently received motion event.
private MotionEvent mLastReceivedEvent;
// The accompanying raw event without any transformations.
@@ -219,8 +222,19 @@ public class TouchState {
startTouchInteracting();
break;
case AccessibilityEvent.TYPE_TOUCH_INTERACTION_END:
- setState(STATE_CLEAR);
- // We will clear when we actually handle the next ACTION_DOWN.
+ // When interaction ends, check if there are still down pointers.
+ // If there are any down pointers, go directly to TouchExploring instead.
+ if (com.android.server.accessibility.Flags
+ .pointerUpMotionEventInTouchExploration()) {
+ if (mReceivedPointerTracker.mReceivedPointersDown > 0) {
+ startTouchExploring();
+ } else {
+ setState(STATE_CLEAR);
+ // We will clear when we actually handle the next ACTION_DOWN.
+ }
+ } else {
+ setState(STATE_CLEAR);
+ }
break;
case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_START:
startTouchExploring();
@@ -419,7 +433,8 @@ public class TouchState {
private final PointerDownInfo[] mReceivedPointers = new PointerDownInfo[MAX_POINTER_COUNT];
// Which pointers are down.
- private int mReceivedPointersDown;
+ @VisibleForTesting
+ public int mReceivedPointersDown;
// The edge flags of the last received down event.
private int mLastReceivedDownEdgeFlags;
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index 6ccf5e47ca6c..59566677b1fc 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -39,6 +39,14 @@ import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__INLINE;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__MENU;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__UNKNOWN_AUTOFILL_DISPLAY_PRESENTATION_TYPE;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_DELAY_AFTER_ANIMATION_END;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_FILL_DIALOG_DISABLED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_LAST_TRIGGERED_ID_CHANGED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_SCREEN_HAS_CREDMAN_FIELD;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_AFTER_DELAY;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_SINCE_IME_ANIMATED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_UNKNOWN;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_WAIT_FOR_IME_ANIMATION;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_ACTIVITY_FINISHED;
import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_FILL_REQUEST_FAILED;
@@ -157,8 +165,24 @@ public final class PresentationStatsEventLogger {
DETECTION_PREFER_PCC
})
@Retention(RetentionPolicy.SOURCE)
- public @interface DetectionPreference {
- }
+ public @interface DetectionPreference {}
+
+ /**
+ * The fill dialog not shown reason. These are wrappers around
+ * {@link com.android.os.AtomsProto.AutofillPresentationEventReported.FillDialogNotShownReason}.
+ */
+ @IntDef(prefix = {"FILL_DIALOG_NOT_SHOWN_REASON"}, value = {
+ FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN,
+ FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED,
+ FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD,
+ FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED,
+ FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION,
+ FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED,
+ FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END,
+ FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FillDialogNotShownReason {}
public static final int NOT_SHOWN_REASON_ANY_SHOWN =
AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN;
@@ -219,6 +243,25 @@ public final class PresentationStatsEventLogger {
public static final int DETECTION_PREFER_PCC =
AUTOFILL_FILL_RESPONSE_REPORTED__DETECTION_PREFERENCE__DETECTION_PREFER_PCC;
+ // Values for AutofillFillResponseReported.fill_dialog_not_shown_reason
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_UNKNOWN;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_FILL_DIALOG_DISABLED;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_SCREEN_HAS_CREDMAN_FIELD;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_LAST_TRIGGERED_ID_CHANGED;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_WAIT_FOR_IME_ANIMATION;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_SINCE_IME_ANIMATED;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_DELAY_AFTER_ANIMATION_END;
+ public static final int FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY =
+ AUTOFILL_PRESENTATION_EVENT_REPORTED__FILL_DIALOG_NOT_SHOWN_REASON__REASON_TIMEOUT_AFTER_DELAY;
+
+
private static final int DEFAULT_VALUE_INT = -1;
private final int mSessionId;
@@ -871,6 +914,43 @@ public final class PresentationStatsEventLogger {
}
/**
+ * Set fill_dialog_not_shown_reason
+ * @param reason
+ */
+ public void maybeSetFillDialogNotShownReason(@FillDialogNotShownReason int reason) {
+ mEventInternal.ifPresent(event -> {
+ if ((event.mFillDialogNotShownReason
+ == FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END
+ || event.mFillDialogNotShownReason
+ == FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION) && reason
+ == FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED) {
+ event.mFillDialogNotShownReason = FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_AFTER_DELAY;
+ } else {
+ event.mFillDialogNotShownReason = reason;
+ }
+ });
+ }
+
+ /**
+ * Set fill_dialog_ready_to_show_ms
+ * @param val
+ */
+ public void maybeSetFillDialogReadyToShowMs(long val) {
+ mEventInternal.ifPresent(event -> {
+ event.mFillDialogReadyToShowMs = (int) (val - mSessionStartTimestamp);
+ });
+ }
+
+ /**
+ * Set ime_animation_finish_ms
+ * @param val
+ */
+ public void maybeSetImeAnimationFinishMs(long val) {
+ mEventInternal.ifPresent(event -> {
+ event.mImeAnimationFinishMs = (int) (val - mSessionStartTimestamp);
+ });
+ }
+ /**
* Set the log contains relayout metrics.
* This is being added as a temporary measure to add logging.
* In future, when we map Session's old view states to the new autofill id's as part of fixing
@@ -959,7 +1039,13 @@ public final class PresentationStatsEventLogger {
+ " event.notExpiringResponseDuringAuthCount="
+ event.mFixExpireResponseDuringAuthCount
+ " event.notifyViewEnteredIgnoredDuringAuthCount="
- + event.mNotifyViewEnteredIgnoredDuringAuthCount);
+ + event.mNotifyViewEnteredIgnoredDuringAuthCount
+ + " event.fillDialogNotShownReason="
+ + event.mFillDialogNotShownReason
+ + " event.fillDialogReadyToShowMs="
+ + event.mFillDialogReadyToShowMs
+ + " event.imeAnimationFinishMs="
+ + event.mImeAnimationFinishMs);
}
// TODO(b/234185326): Distinguish empty responses from other no presentation reasons.
@@ -1020,7 +1106,10 @@ public final class PresentationStatsEventLogger {
event.mViewFilledSuccessfullyOnRefillCount,
event.mViewFailedOnRefillCount,
event.mFixExpireResponseDuringAuthCount,
- event.mNotifyViewEnteredIgnoredDuringAuthCount);
+ event.mNotifyViewEnteredIgnoredDuringAuthCount,
+ event.mFillDialogNotShownReason,
+ event.mFillDialogReadyToShowMs,
+ event.mImeAnimationFinishMs);
mEventInternal = Optional.empty();
}
@@ -1087,6 +1176,9 @@ public final class PresentationStatsEventLogger {
// Following are not logged and used only for internal logic
boolean shouldResetShownCount = false;
boolean mHasRelayoutLog = false;
+ @FillDialogNotShownReason int mFillDialogNotShownReason = FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN;
+ int mFillDialogReadyToShowMs = DEFAULT_VALUE_INT;
+ int mImeAnimationFinishMs = DEFAULT_VALUE_INT;
PresentationStatsEventInternal() {}
}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 6515b237519a..ff3bf2acb080 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -81,6 +81,13 @@ import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTIC
import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_RESULT_SUCCESS;
import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_TYPE_DATASET_AUTHENTICATION;
import static com.android.server.autofill.PresentationStatsEventLogger.AUTHENTICATION_TYPE_FULL_AUTHENTICATION;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN;
+import static com.android.server.autofill.PresentationStatsEventLogger.FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION;
import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_ANY_SHOWN;
import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_NO_FOCUS;
import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_REQUEST_FAILED;
@@ -5622,6 +5629,10 @@ final class Session
synchronized (mLock) {
final ViewState currentView = mViewStates.get(mCurrentViewId);
currentView.setState(ViewState.STATE_FILL_DIALOG_SHOWN);
+ // Set fill_dialog_not_shown_reason to unknown (a.k.a shown). It is needed due
+ // to possible SHOW_FILL_DIALOG_WAIT.
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_UNKNOWN);
}
// Just show fill dialog once per fill request, so disabled after shown.
// Note: Cannot disable before requestShowFillDialog() because the method
@@ -5725,6 +5736,15 @@ final class Session
private boolean isFillDialogUiEnabled() {
synchronized (mLock) {
+ if (mSessionFlags.mFillDialogDisabled) {
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_FILL_DIALOG_DISABLED);
+ }
+ if (mSessionFlags.mScreenHasCredmanField) {
+ // Prefer to log "HAS_CREDMAN_FIELD" over "FILL_DIALOG_DISABLED".
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_SCREEN_HAS_CREDMAN_FIELD);
+ }
return !mSessionFlags.mFillDialogDisabled && !mSessionFlags.mScreenHasCredmanField;
}
}
@@ -5789,6 +5809,8 @@ final class Session
|| !ArrayUtils.contains(mLastFillDialogTriggerIds, filledId)) {
// Last fill dialog triggered ids are changed.
if (sDebug) Log.w(TAG, "Last fill dialog triggered ids are changed.");
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_LAST_TRIGGERED_ID_CHANGED);
return SHOW_FILL_DIALOG_NO;
}
@@ -5815,6 +5837,8 @@ final class Session
// we need to wait for animation to happen. We can't return from here yet.
// This is the situation #2 described above.
Log.d(TAG, "Waiting for ime animation to complete before showing fill dialog");
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_WAIT_FOR_IME_ANIMATION);
mFillDialogRunnable = createFillDialogEvalRunnable(
response, filledId, filterText, flags);
return SHOW_FILL_DIALOG_WAIT;
@@ -5824,9 +5848,15 @@ final class Session
// max of start input time or the ime finish time
long effectiveDuration = currentTimestampMs
- Math.max(mLastInputStartTime, mImeAnimationFinishTimeMs);
+ mPresentationStatsEventLogger.maybeSetFillDialogReadyToShowMs(
+ currentTimestampMs);
+ mPresentationStatsEventLogger.maybeSetImeAnimationFinishMs(
+ Math.max(mLastInputStartTime, mImeAnimationFinishTimeMs));
if (effectiveDuration >= mFillDialogTimeoutMs) {
Log.d(TAG, "Fill dialog not shown since IME has been up for more time than "
+ mFillDialogTimeoutMs + "ms");
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_TIMEOUT_SINCE_IME_ANIMATED);
return SHOW_FILL_DIALOG_NO;
} else if (effectiveDuration < mFillDialogMinWaitAfterImeAnimationMs) {
// we need to wait for some time after animation ends
@@ -5834,6 +5864,8 @@ final class Session
response, filledId, filterText, flags);
mHandler.postDelayed(runnable,
mFillDialogMinWaitAfterImeAnimationMs - effectiveDuration);
+ mPresentationStatsEventLogger.maybeSetFillDialogNotShownReason(
+ FILL_DIALOG_NOT_SHOWN_REASON_DELAY_AFTER_ANIMATION_END);
return SHOW_FILL_DIALOG_WAIT;
}
}
diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
index ebb1194c7c4a..b173f76e5f6f 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java
@@ -25,6 +25,7 @@ import static com.android.server.backup.UserBackupManagerService.SHARED_BACKUP_A
import android.annotation.UserIdInt;
import android.app.ApplicationThreadConstants;
import android.app.IBackupAgent;
+import android.app.backup.BackupManagerMonitor;
import android.app.backup.BackupTransport;
import android.app.backup.FullBackupDataOutput;
import android.content.pm.ApplicationInfo;
@@ -268,6 +269,12 @@ public class FullBackupEngine {
mBackupManagerMonitorEventSender.monitorAgentLoggingResults(mPkg, mAgent);
} catch (IOException e) {
Slog.e(TAG, "Error backing up " + mPkg.packageName + ": " + e.getMessage());
+ // This is likely due to the app process dying.
+ mBackupManagerMonitorEventSender.monitorEvent(
+ BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN,
+ mPkg,
+ BackupManagerMonitor.LOG_EVENT_CATEGORY_AGENT,
+ /* extras= */ null);
result = BackupTransport.AGENT_ERROR;
} finally {
try {
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index f677c9dbf4d0..48d21c3a222f 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -294,6 +294,14 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba
// SinglePackageBackupPreflight.
if (cancellationReason == CancellationReason.TIMEOUT) {
Slog.wtf(TAG, "This task cannot time out");
+ return;
+ }
+
+ // We don't cancel the entire operation if a single agent is disconnected unexpectedly.
+ // SinglePackageBackupRunner and SinglePackageBackupPreflight will receive the same
+ // callback and fail gracefully. The operation should then continue to the next package.
+ if (cancellationReason == CancellationReason.AGENT_DISCONNECTED) {
+ return;
}
if (mCancelled) {
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index 1263146fe405..20f103cdfab4 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -589,6 +589,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
monitoringExtras);
Slog.e(TAG, "Failure getting next package name");
EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE);
+ mStatus = BackupTransport.TRANSPORT_ERROR;
nextState = UnifiedRestoreState.FINAL;
return;
} else if (mRestoreDescription == RestoreDescription.NO_MORE_PACKAGES) {
diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java
index fad59d23a6dc..855c72acd7ca 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorDumpsysUtils.java
@@ -389,6 +389,8 @@ public class BackupManagerMonitorDumpsysUtils {
"Agent failure during restore";
case BackupManagerMonitor.LOG_EVENT_ID_FAILED_TO_READ_DATA_FROM_TRANSPORT ->
"Failed to read data from Transport";
+ case BackupManagerMonitor.LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN ->
+ "LOG_EVENT_ID_FULL_BACKUP_AGENT_PIPE_BROKEN";
default -> "Unknown log event ID: " + code;
};
return id;
diff --git a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
index cd9285cdfe91..cbee8391458d 100644
--- a/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/association/AssociationRequestsProcessor.java
@@ -58,13 +58,16 @@ import android.os.Handler;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.UserHandle;
+import android.util.ArraySet;
import android.util.Slog;
import com.android.internal.R;
import com.android.server.companion.CompanionDeviceManagerService;
import com.android.server.companion.utils.PackageUtils;
+import java.util.Arrays;
import java.util.List;
+import java.util.Set;
/**
* Class responsible for handling incoming {@link AssociationRequest}s.
@@ -130,6 +133,12 @@ public class AssociationRequestsProcessor {
private static final int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5;
private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min;
+ // Set of profiles for which the association dialog cannot be skipped.
+ private static final Set<String> DEVICE_PROFILES_WITH_REQUIRED_CONFIRMATION = new ArraySet<>(
+ Arrays.asList(
+ AssociationRequest.DEVICE_PROFILE_APP_STREAMING,
+ AssociationRequest.DEVICE_PROFILE_NEARBY_DEVICE_STREAMING));
+
private final @NonNull Context mContext;
private final @NonNull PackageManagerInternal mPackageManagerInternal;
private final @NonNull AssociationStore mAssociationStore;
@@ -174,6 +183,7 @@ public class AssociationRequestsProcessor {
// 2a. Check if association can be created without launching UI (i.e. CDM needs NEITHER
// to perform discovery NOR to collect user consent).
if (request.isSelfManaged() && !request.isForceConfirmation()
+ && !DEVICE_PROFILES_WITH_REQUIRED_CONFIRMATION.contains(request.getDeviceProfile())
&& !willAddRoleHolder(request, packageName, userId)) {
// 2a.1. Create association right away.
createAssociationAndNotifyApplication(request, packageName, userId,
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 14d9d3f0c0a1..decac40d20f8 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -122,10 +122,10 @@ genrule {
}
genrule {
- name: "statslog-mediarouter-java-gen",
- tools: ["stats-log-api-gen"],
- cmd: "$(location stats-log-api-gen) --java $(out) --module mediarouter --javaPackage com.android.server.media --javaClass MediaRouterStatsLog",
- out: ["com/android/server/media/MediaRouterStatsLog.java"],
+ name: "statslog-mediarouter-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module mediarouter --javaPackage com.android.server.media --javaClass MediaRouterStatsLog",
+ out: ["com/android/server/media/MediaRouterStatsLog.java"],
}
java_library_static {
@@ -138,6 +138,7 @@ java_library_static {
"ondeviceintelligence_conditionally",
],
srcs: [
+ ":android.hardware.audio.effect-V1-java-source",
":android.hardware.tv.hdmi.connection-V1-java-source",
":android.hardware.tv.hdmi.earc-V1-java-source",
":android.hardware.tv.mediaquality-V1-java-source",
diff --git a/services/core/java/com/android/server/adb/AdbDebuggingManager.java b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
index 2930c691c1ac..658ea4c27e4c 100644
--- a/services/core/java/com/android/server/adb/AdbDebuggingManager.java
+++ b/services/core/java/com/android/server/adb/AdbDebuggingManager.java
@@ -154,8 +154,6 @@ public class AdbDebuggingManager {
@Nullable private final File mUserKeyFile;
@Nullable private final File mTempKeysFile;
- private static final String WIFI_PERSISTENT_CONFIG_PROPERTY =
- "persist.adb.tls_server.enable";
private static final String WIFI_PERSISTENT_GUID =
"persist.adb.wifi.guid";
private static final int PAIRING_CODE_LENGTH = 6;
@@ -1089,7 +1087,7 @@ public class AdbDebuggingManager {
intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
mContext.registerReceiver(mBroadcastReceiver, intentFilter);
- SystemProperties.set(WIFI_PERSISTENT_CONFIG_PROPERTY, "1");
+ SystemProperties.set(AdbService.WIFI_PERSISTENT_CONFIG_PROPERTY, "1");
mConnectionPortPoller =
new AdbDebuggingManager.AdbConnectionPortPoller(mPortListener);
mConnectionPortPoller.start();
@@ -1139,7 +1137,7 @@ public class AdbDebuggingManager {
intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
mContext.registerReceiver(mBroadcastReceiver, intentFilter);
- SystemProperties.set(WIFI_PERSISTENT_CONFIG_PROPERTY, "1");
+ SystemProperties.set(AdbService.WIFI_PERSISTENT_CONFIG_PROPERTY, "1");
mConnectionPortPoller =
new AdbDebuggingManager.AdbConnectionPortPoller(mPortListener);
mConnectionPortPoller.start();
diff --git a/services/core/java/com/android/server/adb/AdbService.java b/services/core/java/com/android/server/adb/AdbService.java
index 2baccbf9232e..aae48daa5dde 100644
--- a/services/core/java/com/android/server/adb/AdbService.java
+++ b/services/core/java/com/android/server/adb/AdbService.java
@@ -229,7 +229,7 @@ public class AdbService extends IAdbManager.Stub {
* May also contain vendor-specific default functions for testing purposes.
*/
private static final String USB_PERSISTENT_CONFIG_PROPERTY = "persist.sys.usb.config";
- private static final String WIFI_PERSISTENT_CONFIG_PROPERTY = "persist.adb.tls_server.enable";
+ static final String WIFI_PERSISTENT_CONFIG_PROPERTY = "persist.adb.tls_server.enable";
private final Context mContext;
private final ContentResolver mContentResolver;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b0b34d0ab9c4..76ba0054583b 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -16190,14 +16190,16 @@ public class ActivityManagerService extends IActivityManager.Stub
return mUserController.switchUser(targetUserId);
}
+ @Nullable
@Override
- public String getSwitchingFromUserMessage() {
- return mUserController.getSwitchingFromSystemUserMessage();
+ public String getSwitchingFromUserMessage(@UserIdInt int userId) {
+ return mUserController.getSwitchingFromUserMessage(userId);
}
+ @Nullable
@Override
- public String getSwitchingToUserMessage() {
- return mUserController.getSwitchingToSystemUserMessage();
+ public String getSwitchingToUserMessage(@UserIdInt int userId) {
+ return mUserController.getSwitchingToUserMessage(userId);
}
@Override
@@ -16938,13 +16940,13 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
- public void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage) {
- mUserController.setSwitchingFromSystemUserMessage(switchingFromSystemUserMessage);
+ public void setSwitchingFromUserMessage(@UserIdInt int userId, @Nullable String message) {
+ mUserController.setSwitchingFromUserMessage(userId, message);
}
@Override
- public void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage) {
- mUserController.setSwitchingToSystemUserMessage(switchingToSystemUserMessage);
+ public void setSwitchingToUserMessage(@UserIdInt int userId, @Nullable String message) {
+ mUserController.setSwitchingToUserMessage(userId, message);
}
@Override
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index db0562f5750a..508c01802156 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -810,7 +810,7 @@ class BroadcastProcessQueue {
* Return the broadcast being actively dispatched in this process.
*/
public @NonNull BroadcastRecord getActive() {
- return Objects.requireNonNull(mActive);
+ return Objects.requireNonNull(mActive, toString());
}
/**
@@ -818,7 +818,7 @@ class BroadcastProcessQueue {
* being actively dispatched in this process.
*/
public int getActiveIndex() {
- Objects.requireNonNull(mActive);
+ Objects.requireNonNull(mActive, toString());
return mActiveIndex;
}
diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
index c76a0d0ac59a..d276b9a94791 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java
@@ -606,8 +606,9 @@ class BroadcastQueueImpl extends BroadcastQueue {
} else {
mRunningColdStart.reEnqueueActiveBroadcast();
}
- demoteFromRunningLocked(mRunningColdStart);
+ final BroadcastProcessQueue queue = mRunningColdStart;
clearRunningColdStart();
+ demoteFromRunningLocked(queue);
enqueueUpdateRunningList();
}
@@ -1527,6 +1528,15 @@ class BroadcastQueueImpl extends BroadcastQueue {
final int cookie = traceBegin("demoteFromRunning");
// We've drained running broadcasts; maybe move back to runnable
+ if (mRunningColdStart == queue) {
+ // TODO: b/399020479 - Remove wtf log once we identify the case where mRunningColdStart
+ // is not getting cleared.
+ // If this queue is mRunningColdStart, then it should have been cleared before
+ // it is demoted. Log a wtf if this isn't the case.
+ Slog.wtf(TAG, "mRunningColdStart has not been cleared; mRunningColdStart.app: "
+ + mRunningColdStart.app + " , queue.app: " + queue.app,
+ new IllegalStateException());
+ }
queue.makeActiveIdle();
queue.traceProcessEnd();
@@ -2332,12 +2342,6 @@ class BroadcastQueueImpl extends BroadcastQueue {
@VisibleForTesting
@GuardedBy("mService")
- @Nullable BroadcastProcessQueue removeProcessQueue(@NonNull ProcessRecord app) {
- return removeProcessQueue(app.processName, app.info.uid);
- }
-
- @VisibleForTesting
- @GuardedBy("mService")
@Nullable BroadcastProcessQueue removeProcessQueue(@NonNull String processName,
int uid) {
BroadcastProcessQueue prev = null;
diff --git a/services/core/java/com/android/server/am/ConnectionRecord.java b/services/core/java/com/android/server/am/ConnectionRecord.java
index 31704c442290..4e1d77c26129 100644
--- a/services/core/java/com/android/server/am/ConnectionRecord.java
+++ b/services/core/java/com/android/server/am/ConnectionRecord.java
@@ -142,6 +142,10 @@ final class ConnectionRecord implements OomAdjusterModernImpl.Connection{
| Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS);
}
+ @Override
+ public boolean transmitsCpuTime() {
+ return !hasFlag(Context.BIND_ALLOW_FREEZE);
+ }
public long getFlags() {
return flags;
@@ -273,6 +277,9 @@ final class ConnectionRecord implements OomAdjusterModernImpl.Connection{
if (hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) {
sb.append("CAPS ");
}
+ if (hasFlag(Context.BIND_ALLOW_FREEZE)) {
+ sb.append("!CPU ");
+ }
if (serviceDead) {
sb.append("DEAD ");
}
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 336a35e7a7e3..fa35da30bf4b 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -2802,7 +2802,7 @@ public class OomAdjuster {
// we check the final procstate, and remove it if the procsate is below BFGS.
capability |= getBfslCapabilityFromClient(client);
- capability |= getCpuCapabilityFromClient(client);
+ capability |= getCpuCapabilityFromClient(cr, client);
if (cr.notHasFlag(Context.BIND_WAIVE_PRIORITY)) {
if (cr.hasFlag(Context.BIND_INCLUDE_CAPABILITIES)) {
@@ -3259,7 +3259,7 @@ public class OomAdjuster {
// we check the final procstate, and remove it if the procsate is below BFGS.
capability |= getBfslCapabilityFromClient(client);
- capability |= getCpuCapabilityFromClient(client);
+ capability |= getCpuCapabilityFromClient(conn, client);
if (clientProcState >= PROCESS_STATE_CACHED_ACTIVITY) {
// If the other app is cached for any reason, for purposes here
@@ -3502,10 +3502,13 @@ public class OomAdjuster {
/**
* @return the CPU capability from a client (of a service binding or provider).
*/
- private static int getCpuCapabilityFromClient(ProcessRecord client) {
- // Just grant CPU capability every time
- // TODO(b/370817323): Populate with reasons to not propagate cpu capability across bindings.
- return client.mState.getCurCapability() & PROCESS_CAPABILITY_CPU_TIME;
+ private static int getCpuCapabilityFromClient(OomAdjusterModernImpl.Connection conn,
+ ProcessRecord client) {
+ if (conn == null || conn.transmitsCpuTime()) {
+ return client.mState.getCurCapability() & PROCESS_CAPABILITY_CPU_TIME;
+ } else {
+ return 0;
+ }
}
/**
diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
index 1b7e8f0bd244..7e7b5685cf13 100644
--- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
+++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
@@ -635,6 +635,15 @@ public class OomAdjusterModernImpl extends OomAdjuster {
* Returns true if this connection can propagate capabilities.
*/
boolean canAffectCapabilities();
+
+ /**
+ * Returns whether this connection transmits PROCESS_CAPABILITY_CPU_TIME to the host, if the
+ * client possesses it.
+ */
+ default boolean transmitsCpuTime() {
+ // Always lend this capability by default.
+ return true;
+ }
}
/**
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 18f3500b2d56..40a9bbec3598 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -340,16 +340,16 @@ class UserController implements Handler.Callback {
private volatile ArraySet<String> mCurWaitingUserSwitchCallbacks;
/**
- * Messages for switching from {@link android.os.UserHandle#SYSTEM}.
+ * Message shown when switching from a user.
*/
@GuardedBy("mLock")
- private String mSwitchingFromSystemUserMessage;
+ private final SparseArray<String> mSwitchingFromUserMessage = new SparseArray<>();
/**
- * Messages for switching to {@link android.os.UserHandle#SYSTEM}.
+ * Message shown when switching to a user.
*/
@GuardedBy("mLock")
- private String mSwitchingToSystemUserMessage;
+ private final SparseArray<String> mSwitchingToUserMessage = new SparseArray<>();
/**
* Callbacks that are still active after {@link #getUserSwitchTimeoutMs}
@@ -2271,8 +2271,8 @@ class UserController implements Handler.Callback {
private void showUserSwitchDialog(Pair<UserInfo, UserInfo> fromToUserPair) {
// The dialog will show and then initiate the user switch by calling startUserInForeground
mInjector.showUserSwitchingDialog(fromToUserPair.first, fromToUserPair.second,
- getSwitchingFromSystemUserMessageUnchecked(),
- getSwitchingToSystemUserMessageUnchecked(),
+ getSwitchingFromUserMessageUnchecked(fromToUserPair.first.id),
+ getSwitchingToUserMessageUnchecked(fromToUserPair.second.id),
/* onShown= */ () -> sendStartUserSwitchFgMessage(fromToUserPair.second.id));
}
@@ -3388,41 +3388,45 @@ class UserController implements Handler.Callback {
return mLockPatternUtils.isLockScreenDisabled(userId);
}
- void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage) {
+ void setSwitchingFromUserMessage(@UserIdInt int user, @Nullable String message) {
synchronized (mLock) {
- mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage;
+ mSwitchingFromUserMessage.put(user, message);
}
}
- void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage) {
+ void setSwitchingToUserMessage(@UserIdInt int user, @Nullable String message) {
synchronized (mLock) {
- mSwitchingToSystemUserMessage = switchingToSystemUserMessage;
+ mSwitchingToUserMessage.put(user, message);
}
}
// Called by AMS, must check permission
- String getSwitchingFromSystemUserMessage() {
- checkHasManageUsersPermission("getSwitchingFromSystemUserMessage()");
+ @Nullable
+ String getSwitchingFromUserMessage(@UserIdInt int userId) {
+ checkHasManageUsersPermission("getSwitchingFromUserMessage()");
- return getSwitchingFromSystemUserMessageUnchecked();
+ return getSwitchingFromUserMessageUnchecked(userId);
}
// Called by AMS, must check permission
- String getSwitchingToSystemUserMessage() {
- checkHasManageUsersPermission("getSwitchingToSystemUserMessage()");
+ @Nullable
+ String getSwitchingToUserMessage(@UserIdInt int userId) {
+ checkHasManageUsersPermission("getSwitchingToUserMessage()");
- return getSwitchingToSystemUserMessageUnchecked();
+ return getSwitchingToUserMessageUnchecked(userId);
}
- private String getSwitchingFromSystemUserMessageUnchecked() {
+ @Nullable
+ private String getSwitchingFromUserMessageUnchecked(@UserIdInt int userId) {
synchronized (mLock) {
- return mSwitchingFromSystemUserMessage;
+ return mSwitchingFromUserMessage.get(userId);
}
}
- private String getSwitchingToSystemUserMessageUnchecked() {
+ @Nullable
+ private String getSwitchingToUserMessageUnchecked(@UserIdInt int userId) {
synchronized (mLock) {
- return mSwitchingToSystemUserMessage;
+ return mSwitchingToUserMessage.get(userId);
}
}
@@ -3518,12 +3522,8 @@ class UserController implements Handler.Callback {
+ mIsBroadcastSentForSystemUserStarted);
pw.println(" mIsBroadcastSentForSystemUserStarting:"
+ mIsBroadcastSentForSystemUserStarting);
- if (mSwitchingFromSystemUserMessage != null) {
- pw.println(" mSwitchingFromSystemUserMessage: " + mSwitchingFromSystemUserMessage);
- }
- if (mSwitchingToSystemUserMessage != null) {
- pw.println(" mSwitchingToSystemUserMessage: " + mSwitchingToSystemUserMessage);
- }
+ pw.println(" mSwitchingFromUserMessage:" + mSwitchingFromUserMessage);
+ pw.println(" mSwitchingToUserMessage:" + mSwitchingToUserMessage);
pw.println(" mLastUserUnlockingUptime: " + mLastUserUnlockingUptime);
}
}
@@ -4046,7 +4046,7 @@ class UserController implements Handler.Callback {
}
void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser,
- String switchingFromSystemUserMessage, String switchingToSystemUserMessage,
+ @Nullable String switchingFromUserMessage, @Nullable String switchingToUserMessage,
@NonNull Runnable onShown) {
if (mService.mContext.getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
@@ -4059,7 +4059,7 @@ class UserController implements Handler.Callback {
synchronized (mUserSwitchingDialogLock) {
dismissUserSwitchingDialog(null);
mUserSwitchingDialog = new UserSwitchingDialog(mService.mContext, fromUser, toUser,
- mHandler, switchingFromSystemUserMessage, switchingToSystemUserMessage);
+ mHandler, switchingFromUserMessage, switchingToUserMessage);
mUserSwitchingDialog.show(onShown);
}
}
diff --git a/services/core/java/com/android/server/am/UserSwitchingDialog.java b/services/core/java/com/android/server/am/UserSwitchingDialog.java
index 223e0b79ec0b..f4e733a0c99f 100644
--- a/services/core/java/com/android/server/am/UserSwitchingDialog.java
+++ b/services/core/java/com/android/server/am/UserSwitchingDialog.java
@@ -52,6 +52,7 @@ import com.android.internal.R;
import com.android.internal.util.ObjectUtils;
import com.android.internal.util.UserIcons;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -75,21 +76,23 @@ class UserSwitchingDialog extends Dialog {
protected final UserInfo mOldUser;
protected final UserInfo mNewUser;
- private final String mSwitchingFromSystemUserMessage;
- private final String mSwitchingToSystemUserMessage;
+ @Nullable
+ private final String mSwitchingFromUserMessage;
+ @Nullable
+ private final String mSwitchingToUserMessage;
protected final Context mContext;
private final int mTraceCookie;
UserSwitchingDialog(Context context, UserInfo oldUser, UserInfo newUser, Handler handler,
- String switchingFromSystemUserMessage, String switchingToSystemUserMessage) {
+ @Nullable String switchingFromUserMessage, @Nullable String switchingToUserMessage) {
super(context, R.style.Theme_Material_NoActionBar_Fullscreen);
mContext = context;
mOldUser = oldUser;
mNewUser = newUser;
mHandler = handler;
- mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage;
- mSwitchingToSystemUserMessage = switchingToSystemUserMessage;
+ mSwitchingFromUserMessage = switchingFromUserMessage;
+ mSwitchingToUserMessage = switchingToUserMessage;
mDisableAnimations = SystemProperties.getBoolean(
"debug.usercontroller.disable_user_switching_dialog_animations", false);
mTraceCookie = UserHandle.MAX_SECONDARY_USER_ID * oldUser.id + newUser.id;
@@ -166,14 +169,14 @@ class UserSwitchingDialog extends Dialog {
: R.string.demo_starting_message);
}
- final String message =
- mOldUser.id == UserHandle.USER_SYSTEM ? mSwitchingFromSystemUserMessage
- : mNewUser.id == UserHandle.USER_SYSTEM ? mSwitchingToSystemUserMessage : null;
+ if (mSwitchingFromUserMessage != null || mSwitchingToUserMessage != null) {
+ if (mSwitchingFromUserMessage != null && mSwitchingToUserMessage != null) {
+ return mSwitchingFromUserMessage + " " + mSwitchingToUserMessage;
+ }
+ return Objects.requireNonNullElse(mSwitchingFromUserMessage, mSwitchingToUserMessage);
+ }
- return message != null ? message
- // If switchingFromSystemUserMessage or switchingToSystemUserMessage is null,
- // fallback to system message.
- : res.getString(R.string.user_switching_message, mNewUser.name);
+ return res.getString(R.string.user_switching_message, mNewUser.name);
}
@Override
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java
index 86f5d9bd637f..c53e4bdc2205 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsDbHelper.java
@@ -189,11 +189,11 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper {
@AppOpsManager.HistoricalOpsRequestFilter int requestFilters,
int uidFilter, @Nullable String packageNameFilter,
@Nullable String attributionTagFilter, IntArray opCodesFilter, int opFlagsFilter,
- long beginTime, long endTime, int limit, String orderByColumn) {
+ long beginTime, long endTime, int limit, String orderByColumn, boolean ascending) {
List<SQLCondition> conditions = prepareConditions(beginTime, endTime, requestFilters,
uidFilter, packageNameFilter,
attributionTagFilter, opCodesFilter, opFlagsFilter);
- String sql = buildSql(conditions, orderByColumn, limit);
+ String sql = buildSql(conditions, orderByColumn, ascending, limit);
long startTime = 0;
if (Flags.sqliteDiscreteOpEventLoggingEnabled()) {
startTime = SystemClock.elapsedRealtime();
@@ -249,7 +249,8 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper {
return results;
}
- private String buildSql(List<SQLCondition> conditions, String orderByColumn, int limit) {
+ private String buildSql(List<SQLCondition> conditions, String orderByColumn, boolean ascending,
+ int limit) {
StringBuilder sql = new StringBuilder(DiscreteOpsTable.SELECT_TABLE_DATA);
if (!conditions.isEmpty()) {
sql.append(" WHERE ");
@@ -264,6 +265,7 @@ class DiscreteOpsDbHelper extends SQLiteOpenHelper {
if (orderByColumn != null) {
sql.append(" ORDER BY ").append(orderByColumn);
+ sql.append(ascending ? " ASC " : " DESC ");
}
if (limit > 0) {
sql.append(" LIMIT ").append(limit);
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java
index 4610b6a82e47..70b7016fbb90 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsRegistry.java
@@ -31,23 +31,15 @@ import static android.app.AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION;
import static android.app.AppOpsManager.OP_MONITOR_LOCATION;
import static android.app.AppOpsManager.OP_PHONE_CALL_CAMERA;
import static android.app.AppOpsManager.OP_PHONE_CALL_MICROPHONE;
-import static android.app.AppOpsManager.OP_PROCESS_OUTGOING_CALLS;
import static android.app.AppOpsManager.OP_READ_DEVICE_IDENTIFIERS;
import static android.app.AppOpsManager.OP_READ_HEART_RATE;
-import static android.app.AppOpsManager.OP_READ_ICC_SMS;
import static android.app.AppOpsManager.OP_READ_OXYGEN_SATURATION;
import static android.app.AppOpsManager.OP_READ_SKIN_TEMPERATURE;
-import static android.app.AppOpsManager.OP_READ_SMS;
import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
import static android.app.AppOpsManager.OP_RECEIVE_SANDBOX_TRIGGER_AUDIO;
import static android.app.AppOpsManager.OP_RECORD_AUDIO;
import static android.app.AppOpsManager.OP_RESERVED_FOR_TESTING;
import static android.app.AppOpsManager.OP_RUN_IN_BACKGROUND;
-import static android.app.AppOpsManager.OP_SEND_SMS;
-import static android.app.AppOpsManager.OP_SMS_FINANCIAL_TRANSACTIONS;
-import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
-import static android.app.AppOpsManager.OP_WRITE_ICC_SMS;
-import static android.app.AppOpsManager.OP_WRITE_SMS;
import static java.lang.Long.min;
import static java.lang.Math.max;
@@ -134,15 +126,6 @@ abstract class DiscreteOpsRegistry {
+ "," + OP_READ_HEART_RATE + "," + OP_READ_OXYGEN_SATURATION + ","
+ OP_READ_SKIN_TEMPERATURE + "," + OP_RESERVED_FOR_TESTING;
- static final int[] sDiscreteOpsToLog =
- new int[]{OP_FINE_LOCATION, OP_COARSE_LOCATION, OP_EMERGENCY_LOCATION, OP_CAMERA,
- OP_RECORD_AUDIO, OP_PHONE_CALL_MICROPHONE, OP_PHONE_CALL_CAMERA,
- OP_RECEIVE_AMBIENT_TRIGGER_AUDIO, OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, OP_READ_SMS,
- OP_WRITE_SMS, OP_SEND_SMS, OP_READ_ICC_SMS, OP_WRITE_ICC_SMS,
- OP_SMS_FINANCIAL_TRANSACTIONS, OP_SYSTEM_ALERT_WINDOW, OP_MONITOR_LOCATION,
- OP_MONITOR_HIGH_POWER_LOCATION, OP_PROCESS_OUTGOING_CALLS,
- };
-
static final long DEFAULT_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(7).toMillis();
static final long MAXIMUM_DISCRETE_HISTORY_CUTOFF = Duration.ofDays(30).toMillis();
// The duration for which the data is kept, default is 7 days and max 30 days enforced.
diff --git a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
index c897891d02c3..0e1fbf3a6d1a 100644
--- a/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteOpsSqlRegistry.java
@@ -180,7 +180,7 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
ChronoUnit.MILLIS).toEpochMilli());
List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter,
packageNameFilter, attributionTagFilter, opCodes, opFlagsFilter, beginTimeMillis,
- endTimeMillis, -1, null);
+ endTimeMillis, -1, null, false);
LongSparseArray<AttributionChain> attributionChains = null;
if (assembleChains) {
@@ -213,14 +213,15 @@ public class DiscreteOpsSqlRegistry extends DiscreteOpsRegistry {
@AppOpsManager.HistoricalOpsRequestFilter int filter, int dumpOp,
@NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix,
int nDiscreteOps) {
- writeAndClearOldAccessHistory();
+ // flush the cache into database before dump.
+ mDiscreteOpsDbHelper.insertDiscreteOps(mDiscreteOpCache.getAllEventsAndClear());
IntArray opCodes = new IntArray();
if (dumpOp != AppOpsManager.OP_NONE) {
opCodes.add(dumpOp);
}
List<DiscreteOp> discreteOps = mDiscreteOpsDbHelper.getDiscreteOps(filter, uidFilter,
packageNameFilter, attributionTagFilter, opCodes, 0, -1,
- -1, nDiscreteOps, DiscreteOpsTable.Columns.ACCESS_TIME);
+ -1, nDiscreteOps, DiscreteOpsTable.Columns.ACCESS_TIME, false);
pw.print(prefix);
pw.print("Largest chain id: ");
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 4b5f06b13885..8ef79a916530 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1472,8 +1472,8 @@ public class AudioDeviceBroker {
mAudioService.postAccessoryPlugMediaUnmute(device);
}
- /*package*/ int getVolumeForDeviceIgnoreMute(int streamType, int device) {
- return mAudioService.getVolumeForDeviceIgnoreMute(streamType, device);
+ /*package*/ int getVssVolumeForDevice(int streamType, int device) {
+ return mAudioService.getVssVolumeForDevice(streamType, device);
}
/*package*/ int getMaxVssVolumeForStream(int streamType) {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 2e6d98485e85..829d9ea7495f 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -2482,7 +2482,7 @@ public class AudioDeviceInventory {
@GuardedBy("mDevicesLock")
private void makeHearingAidDeviceAvailable(
String address, String name, int streamType, String eventSource) {
- final int hearingAidVolIndex = mDeviceBroker.getVolumeForDeviceIgnoreMute(streamType,
+ final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType,
DEVICE_OUT_HEARING_AID);
mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType);
@@ -2672,7 +2672,7 @@ public class AudioDeviceInventory {
}
final int leAudioVolIndex = (volumeIndex == -1)
- ? mDeviceBroker.getVolumeForDeviceIgnoreMute(streamType, device)
+ ? mDeviceBroker.getVssVolumeForDevice(streamType, device)
: volumeIndex;
final int maxIndex = mDeviceBroker.getMaxVssVolumeForStream(streamType);
mDeviceBroker.postSetLeAudioVolumeIndex(leAudioVolIndex, maxIndex, streamType);
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index a43e4d98c077..766456134b20 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -529,7 +529,7 @@ public class AudioService extends IAudioService.Stub
*/
private InputDeviceVolumeHelper mInputDeviceVolumeHelper;
- /*package*/ int getVolumeForDeviceIgnoreMute(int stream, int device) {
+ /*package*/ int getVssVolumeForDevice(int stream, int device) {
final VolumeStreamState streamState = mStreamStates.get(stream);
return streamState != null ? streamState.getIndex(device) : -1;
}
@@ -4997,6 +4997,8 @@ public class AudioService extends IAudioService.Stub
pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:"
+ disablePrescaleAbsoluteVolume());
pw.println("\tcom.android.media.audio.setStreamVolumeOrder - EOL");
+ pw.println("\tandroid.media.audio.ringtoneUserUriCheck:"
+ + android.media.audio.Flags.ringtoneUserUriCheck());
pw.println("\tandroid.media.audio.roForegroundAudioControl:"
+ roForegroundAudioControl());
pw.println("\tandroid.media.audio.scoManagedByAudio:"
@@ -5098,7 +5100,7 @@ public class AudioService extends IAudioService.Stub
}
final int device = absVolumeDevices.toArray(new Integer[0])[0].intValue();
- final int index = getVolumeForDeviceIgnoreMute(streamType, device);
+ final int index = getStreamVolume(streamType, device);
if (DEBUG_VOL) {
Slog.i(TAG, "onUpdateContextualVolumes streamType: " + streamType
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index 67afff79dffd..643f3308d8f5 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -724,7 +724,7 @@ public class SoundDoseHelper {
int device = mAudioService.getDeviceForStream(AudioSystem.STREAM_MUSIC);
if (safeDevicesContains(device) && isStreamActive) {
scheduleMusicActiveCheck();
- int index = mAudioService.getVolumeForDeviceIgnoreMute(AudioSystem.STREAM_MUSIC,
+ int index = mAudioService.getVssVolumeForDevice(AudioSystem.STREAM_MUSIC,
device);
if (index > safeMediaVolumeIndex(device)) {
// Approximate cumulative active music time
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 97f9a7c4f2b0..8f5b831ca0b4 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -219,7 +219,9 @@ public class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
&& reason != HdmiControlService.INITIATED_BY_BOOT_UP;
List<HdmiCecMessage> bufferedActiveSource = mDelayedMessageBuffer
.getBufferedMessagesWithOpcode(Constants.MESSAGE_ACTIVE_SOURCE);
- if (bufferedActiveSource.isEmpty()) {
+ List<HdmiCecMessage> bufferedActiveSourceFromService = mService.getCecMessageWithOpcode(
+ Constants.MESSAGE_ACTIVE_SOURCE);
+ if (bufferedActiveSource.isEmpty() && bufferedActiveSourceFromService.isEmpty()) {
addAndStartAction(new RequestActiveSourceAction(this, new IHdmiControlCallback.Stub() {
@Override
public void onComplete(int result) {
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 6d973ac8d1b5..fdd0ef2f90e1 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -1593,6 +1593,17 @@ public class HdmiControlService extends SystemService {
this.mCecMessageBuffer = cecMessageBuffer;
}
+ List<HdmiCecMessage> getCecMessageWithOpcode(int opcode) {
+ List<HdmiCecMessage> cecMessagesWithOpcode = new ArrayList<>();
+ List<HdmiCecMessage> cecMessages = mCecMessageBuffer.getBuffer();
+ for (HdmiCecMessage message: cecMessages) {
+ if (message.getOpcode() == opcode) {
+ cecMessagesWithOpcode.add(message);
+ }
+ }
+ return cecMessagesWithOpcode;
+ }
+
/**
* Returns {@link Looper} of main thread. Use this {@link Looper} instance
* for tasks that are running on main service thread.
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index c2fecf283a34..d9db178e0dc2 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -568,6 +568,7 @@ public class InputManagerService extends IInputManager.Stub
}
mWindowManagerCallbacks = callbacks;
registerLidSwitchCallbackInternal(mWindowManagerCallbacks);
+ mKeyGestureController.setWindowManagerCallbacks(callbacks);
}
public void setWiredAccessoryCallbacks(WiredAccessoryCallbacks callbacks) {
@@ -2756,24 +2757,6 @@ public class InputManagerService extends IInputManager.Stub
@Nullable IBinder focussedToken) {
return InputManagerService.this.handleKeyGestureEvent(event);
}
-
- @Override
- public boolean isKeyGestureSupported(int gestureType) {
- switch (gestureType) {
- case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP:
- case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN:
- case KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS:
- return true;
- default:
- return false;
-
- }
- }
});
}
@@ -3371,6 +3354,11 @@ public class InputManagerService extends IInputManager.Stub
*/
@Nullable
SurfaceControl createSurfaceForGestureMonitor(String name, int displayId);
+
+ /**
+ * Provide information on whether the keyguard is currently locked or not.
+ */
+ boolean isKeyguardLocked(int displayId);
}
/**
diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java
index ef5babf19d83..395c77322c04 100644
--- a/services/core/java/com/android/server/input/KeyGestureController.java
+++ b/services/core/java/com/android/server/input/KeyGestureController.java
@@ -62,8 +62,10 @@ import android.view.Display;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
+import android.view.ViewConfiguration;
import com.android.internal.R;
+import com.android.internal.accessibility.AccessibilityShortcutController;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.IShortcutService;
@@ -104,6 +106,7 @@ final class KeyGestureController {
private static final int MSG_NOTIFY_KEY_GESTURE_EVENT = 1;
private static final int MSG_PERSIST_CUSTOM_GESTURES = 2;
private static final int MSG_LOAD_CUSTOM_GESTURES = 3;
+ private static final int MSG_ACCESSIBILITY_SHORTCUT = 4;
// must match: config_settingsKeyBehavior in config.xml
private static final int SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0;
@@ -122,12 +125,15 @@ final class KeyGestureController {
static final int POWER_VOLUME_UP_BEHAVIOR_GLOBAL_ACTIONS = 2;
private final Context mContext;
+ private InputManagerService.WindowManagerCallbacks mWindowManagerCallbacks;
private final Handler mHandler;
private final Handler mIoHandler;
private final int mSystemPid;
private final KeyCombinationManager mKeyCombinationManager;
private final SettingsObserver mSettingsObserver;
private final AppLaunchShortcutManager mAppLaunchShortcutManager;
+ @VisibleForTesting
+ final AccessibilityShortcutController mAccessibilityShortcutController;
private final InputGestureManager mInputGestureManager;
private final DisplayManager mDisplayManager;
@GuardedBy("mInputDataStore")
@@ -175,8 +181,14 @@ final class KeyGestureController {
private final boolean mVisibleBackgroundUsersEnabled = isVisibleBackgroundUsersEnabled();
- KeyGestureController(Context context, Looper looper, Looper ioLooper,
+ public KeyGestureController(Context context, Looper looper, Looper ioLooper,
InputDataStore inputDataStore) {
+ this(context, looper, ioLooper, inputDataStore, new Injector());
+ }
+
+ @VisibleForTesting
+ KeyGestureController(Context context, Looper looper, Looper ioLooper,
+ InputDataStore inputDataStore, Injector injector) {
mContext = context;
mHandler = new Handler(looper, this::handleMessage);
mIoHandler = new Handler(ioLooper, this::handleIoMessage);
@@ -197,6 +209,8 @@ final class KeyGestureController {
mSettingsObserver = new SettingsObserver(mHandler);
mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext);
mInputGestureManager = new InputGestureManager(mContext);
+ mAccessibilityShortcutController = injector.getAccessibilityShortcutController(mContext,
+ mHandler);
mDisplayManager = Objects.requireNonNull(mContext.getSystemService(DisplayManager.class));
mInputDataStore = inputDataStore;
mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
@@ -295,8 +309,8 @@ final class KeyGestureController {
KeyEvent.KEYCODE_VOLUME_UP) {
@Override
public boolean preCondition() {
- return isKeyGestureSupported(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD);
+ return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
+ mWindowManagerCallbacks.isKeyguardLocked(DEFAULT_DISPLAY));
}
@Override
@@ -376,15 +390,15 @@ final class KeyGestureController {
KeyEvent.KEYCODE_DPAD_DOWN) {
@Override
public boolean preCondition() {
- return isKeyGestureSupported(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD);
+ return mAccessibilityShortcutController
+ .isAccessibilityShortcutAvailable(false);
}
@Override
public void execute() {
handleMultiKeyGesture(
new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
KeyGestureEvent.ACTION_GESTURE_START, 0);
}
@@ -392,7 +406,7 @@ final class KeyGestureController {
public void cancel() {
handleMultiKeyGesture(
new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN},
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
KeyGestureEvent.ACTION_GESTURE_COMPLETE,
KeyGestureEvent.FLAG_CANCELLED);
}
@@ -438,6 +452,7 @@ final class KeyGestureController {
mSettingsObserver.observe();
mAppLaunchShortcutManager.systemRunning();
mInputGestureManager.systemRunning();
+ initKeyGestures();
int userId;
synchronized (mUserLock) {
@@ -447,6 +462,27 @@ final class KeyGestureController {
mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget();
}
+ @SuppressLint("MissingPermission")
+ private void initKeyGestures() {
+ InputManager im = Objects.requireNonNull(mContext.getSystemService(InputManager.class));
+ im.registerKeyGestureEventHandler((event, focusedToken) -> {
+ switch (event.getKeyGestureType()) {
+ case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
+ if (event.getAction() == KeyGestureEvent.ACTION_GESTURE_START) {
+ mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+ mHandler.sendMessageDelayed(
+ mHandler.obtainMessage(MSG_ACCESSIBILITY_SHORTCUT),
+ getAccessibilityShortcutTimeout());
+ } else {
+ mHandler.removeMessages(MSG_ACCESSIBILITY_SHORTCUT);
+ }
+ return true;
+ default:
+ return false;
+ }
+ });
+ }
+
public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
if (mVisibleBackgroundUsersEnabled && shouldIgnoreKeyEventForVisibleBackgroundUser(event)) {
return false;
@@ -971,17 +1007,6 @@ final class KeyGestureController {
return false;
}
- private boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
- synchronized (mKeyGestureHandlerRecords) {
- for (KeyGestureHandlerRecord handler : mKeyGestureHandlerRecords.values()) {
- if (handler.isKeyGestureSupported(gestureType)) {
- return true;
- }
- }
- }
- return false;
- }
-
public void notifyKeyGestureCompleted(int deviceId, int[] keycodes, int modifierState,
@KeyGestureEvent.KeyGestureType int gestureType) {
// TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally
@@ -1019,9 +1044,16 @@ final class KeyGestureController {
synchronized (mUserLock) {
mCurrentUserId = userId;
}
+ mAccessibilityShortcutController.setCurrentUser(userId);
mIoHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget();
}
+
+ public void setWindowManagerCallbacks(
+ @NonNull InputManagerService.WindowManagerCallbacks callbacks) {
+ mWindowManagerCallbacks = callbacks;
+ }
+
private boolean isDefaultDisplayOn() {
Display defaultDisplay = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
if (defaultDisplay == null) {
@@ -1068,6 +1100,9 @@ final class KeyGestureController {
AidlKeyGestureEvent event = (AidlKeyGestureEvent) msg.obj;
notifyKeyGestureEvent(event);
break;
+ case MSG_ACCESSIBILITY_SHORTCUT:
+ mAccessibilityShortcutController.performAccessibilityShortcut();
+ break;
}
return true;
}
@@ -1347,17 +1382,6 @@ final class KeyGestureController {
}
return false;
}
-
- public boolean isKeyGestureSupported(@KeyGestureEvent.KeyGestureType int gestureType) {
- try {
- return mKeyGestureHandler.isKeyGestureSupported(gestureType);
- } catch (RemoteException ex) {
- Slog.w(TAG, "Failed to identify if key gesture type is supported by the "
- + "process " + mPid + ", assuming it died.", ex);
- binderDied();
- }
- return false;
- }
}
private class SettingsObserver extends ContentObserver {
@@ -1413,6 +1437,25 @@ final class KeyGestureController {
return event;
}
+ private long getAccessibilityShortcutTimeout() {
+ synchronized (mUserLock) {
+ final ViewConfiguration config = ViewConfiguration.get(mContext);
+ final boolean hasDialogShown = Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, mCurrentUserId) != 0;
+ final boolean skipTimeoutRestriction =
+ Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION,
+ 0, mCurrentUserId) != 0;
+
+ // If users manually set the volume key shortcut for any accessibility service, the
+ // system would bypass the timeout restriction of the shortcut dialog.
+ return hasDialogShown || skipTimeoutRestriction
+ ? config.getAccessibilityShortcutKeyTimeoutAfterConfirmation()
+ : config.getAccessibilityShortcutKeyTimeout();
+ }
+ }
+
public void dump(IndentingPrintWriter ipw) {
ipw.println("KeyGestureController:");
ipw.increaseIndent();
@@ -1459,4 +1502,12 @@ final class KeyGestureController {
mAppLaunchShortcutManager.dump(ipw);
mInputGestureManager.dump(ipw);
}
+
+ @VisibleForTesting
+ static class Injector {
+ AccessibilityShortcutController getAccessibilityShortcutController(Context context,
+ Handler handler) {
+ return new AccessibilityShortcutController(context, handler, UserHandle.USER_SYSTEM);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index f137de1b3e1d..988924d9f498 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -25,6 +25,7 @@ import static android.media.MediaRouter2.SCANNING_STATE_SCANNING_FULL;
import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE;
import static android.media.MediaRouter2Utils.getOriginalId;
import static android.media.MediaRouter2Utils.getProviderId;
+
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_CREATE_SESSION;
import static com.android.server.media.MediaRouterStatsLog.MEDIA_ROUTER_EVENT_REPORTED__EVENT_TYPE__EVENT_TYPE_DESELECT_ROUTE;
@@ -63,6 +64,7 @@ import android.media.MediaRouter2Manager;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
+import android.media.SuggestedDeviceInfo;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
@@ -76,18 +78,21 @@ import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
+
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.function.pooled.PooledLambda;
import com.android.media.flags.Flags;
import com.android.server.LocalServices;
import com.android.server.pm.UserManagerInternal;
import com.android.server.statusbar.StatusBarManagerInternal;
+
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -551,6 +556,36 @@ class MediaRouter2ServiceImpl {
}
}
+ public void setDeviceSuggestionsWithRouter2(
+ @NonNull IMediaRouter2 router,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ Objects.requireNonNull(router, "router must not be null");
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ setDeviceSuggestionsWithRouter2Locked(router, suggestedDeviceInfo);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2(
+ @NonNull IMediaRouter2 router) {
+ Objects.requireNonNull(router, "router must not be null");
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ return getDeviceSuggestionsWithRouter2Locked(router);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
// End of methods that implement MediaRouter2 operations.
// Start of methods that implement MediaRouter2Manager operations.
@@ -805,6 +840,36 @@ class MediaRouter2ServiceImpl {
}
}
+ public void setDeviceSuggestionsWithManager(
+ @NonNull IMediaRouter2Manager manager,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ Objects.requireNonNull(manager, "manager must not be null");
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ setDeviceSuggestionsWithManagerLocked(manager, suggestedDeviceInfo);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager(
+ @NonNull IMediaRouter2Manager manager) {
+ Objects.requireNonNull(manager, "manager must not be null");
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ return getDeviceSuggestionsWithManagerLocked(manager);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
@RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS)
public boolean showMediaOutputSwitcherWithProxyRouter(
@NonNull IMediaRouter2Manager proxyRouter) {
@@ -1582,6 +1647,61 @@ class MediaRouter2ServiceImpl {
DUMMY_REQUEST_ID, routerRecord, uniqueSessionId));
}
+ @GuardedBy("mLock")
+ private void setDeviceSuggestionsWithRouter2Locked(
+ @NonNull IMediaRouter2 router,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ final IBinder binder = router.asBinder();
+ final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+ if (routerRecord == null) {
+ Slog.w(
+ TAG,
+ TextUtils.formatSimple(
+ "Ignoring set device suggestion for unknown router: %s", router));
+ return;
+ }
+
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "setDeviceSuggestions | router: %d suggestion: %d",
+ routerRecord.mPackageName, suggestedDeviceInfo));
+
+ routerRecord.mUserRecord.updateDeviceSuggestionsLocked(
+ routerRecord.mPackageName, routerRecord.mPackageName, suggestedDeviceInfo);
+ routerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(
+ UserHandler::notifyDeviceSuggestionsUpdatedOnHandler,
+ routerRecord.mUserRecord.mHandler,
+ routerRecord.mPackageName,
+ routerRecord.mPackageName,
+ suggestedDeviceInfo));
+ }
+
+ @GuardedBy("mLock")
+ @Nullable
+ private Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2Locked(
+ @NonNull IMediaRouter2 router) {
+ final IBinder binder = router.asBinder();
+ final RouterRecord routerRecord = mAllRouterRecords.get(binder);
+
+ if (routerRecord == null) {
+ Slog.w(
+ TAG,
+ TextUtils.formatSimple(
+ "Attempted to get device suggestion for unknown router: %s", router));
+ return null;
+ }
+
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "getDeviceSuggestions | router: %d", routerRecord.mPackageName));
+
+ return routerRecord.mUserRecord.getDeviceSuggestionsLocked(routerRecord.mPackageName);
+ }
+
// End of locked methods that are used by MediaRouter2.
// Start of locked methods that are used by MediaRouter2Manager.
@@ -1972,6 +2092,68 @@ class MediaRouter2ServiceImpl {
uniqueRequestId, routerRecord, uniqueSessionId));
}
+ @GuardedBy("mLock")
+ private void setDeviceSuggestionsWithManagerLocked(
+ @NonNull IMediaRouter2Manager manager,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ final IBinder binder = manager.asBinder();
+ ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+ if (managerRecord == null || managerRecord.mTargetPackageName == null) {
+ Slog.w(
+ TAG,
+ TextUtils.formatSimple(
+ "Ignoring set device suggestion for unknown manager: %s", manager));
+ return;
+ }
+
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "setDeviceSuggestions | manager: %d, suggestingPackageName: %d suggestion:"
+ + " %d",
+ managerRecord.mManagerId,
+ managerRecord.mOwnerPackageName,
+ suggestedDeviceInfo));
+
+ managerRecord.mUserRecord.updateDeviceSuggestionsLocked(
+ managerRecord.mTargetPackageName,
+ managerRecord.mOwnerPackageName,
+ suggestedDeviceInfo);
+ managerRecord.mUserRecord.mHandler.sendMessage(
+ obtainMessage(
+ UserHandler::notifyDeviceSuggestionsUpdatedOnHandler,
+ managerRecord.mUserRecord.mHandler,
+ managerRecord.mTargetPackageName,
+ managerRecord.mOwnerPackageName,
+ suggestedDeviceInfo));
+ }
+
+ @GuardedBy("mLock")
+ @Nullable
+ private Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManagerLocked(
+ @NonNull IMediaRouter2Manager manager) {
+ final IBinder binder = manager.asBinder();
+ ManagerRecord managerRecord = mAllManagerRecords.get(binder);
+
+ if (managerRecord == null || managerRecord.mTargetPackageName == null) {
+ Slog.w(
+ TAG,
+ TextUtils.formatSimple(
+ "Attempted to get device suggestion for unknown manager: %s", manager));
+ return null;
+ }
+
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "getDeviceSuggestionsWithManagerLocked | manager: %d",
+ managerRecord.mManagerId));
+
+ return managerRecord.mUserRecord.getDeviceSuggestionsLocked(
+ managerRecord.mTargetPackageName);
+ }
+
// End of locked methods that are used by MediaRouter2Manager.
// Start of locked methods that are used by both MediaRouter2 and MediaRouter2Manager.
@@ -2047,6 +2229,11 @@ class MediaRouter2ServiceImpl {
//TODO: make records private for thread-safety
final ArrayList<RouterRecord> mRouterRecords = new ArrayList<>();
final ArrayList<ManagerRecord> mManagerRecords = new ArrayList<>();
+
+ // @GuardedBy("mLock")
+ private final Map<String, Map<String, List<SuggestedDeviceInfo>>> mDeviceSuggestions =
+ new HashMap<>();
+
RouteDiscoveryPreference mCompositeDiscoveryPreference = RouteDiscoveryPreference.EMPTY;
Set<String> mActivelyScanningPackages = Set.of();
final UserHandler mHandler;
@@ -2076,6 +2263,25 @@ class MediaRouter2ServiceImpl {
return null;
}
+ // @GuardedBy("mLock")
+ public void updateDeviceSuggestionsLocked(
+ String packageName,
+ String suggestingPackageName,
+ List<SuggestedDeviceInfo> deviceSuggestions) {
+ mDeviceSuggestions.putIfAbsent(
+ packageName, new HashMap<String, List<SuggestedDeviceInfo>>());
+ Map<String, List<SuggestedDeviceInfo>> suggestions =
+ mDeviceSuggestions.get(packageName);
+ suggestions.put(suggestingPackageName, deviceSuggestions);
+ }
+
+ // @GuardedBy("mLock")
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsLocked(
+ String packageName) {
+ return mDeviceSuggestions.get(packageName);
+ }
+
public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
pw.println(prefix + "UserRecord");
@@ -2314,6 +2520,15 @@ class MediaRouter2ServiceImpl {
}
}
+ public void notifyDeviceSuggestionsUpdated(
+ String suggestingPackageName, List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ try {
+ mRouter.notifyDeviceSuggestionsUpdated(suggestingPackageName, suggestedDeviceInfo);
+ } catch (RemoteException ex) {
+ logRemoteException("notifyDeviceSuggestionsUpdated", ex);
+ }
+ }
+
/**
* Sends the corresponding router a {@link RoutingSessionInfo session} creation request,
* with the given {@link MediaRoute2Info} as the initial member.
@@ -3556,6 +3771,41 @@ class MediaRouter2ServiceImpl {
// need to update routers other than the one making the update.
}
+ private void notifyDeviceSuggestionsUpdatedOnHandler(
+ String routerPackageName,
+ String suggestingPackageName,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ MediaRouter2ServiceImpl service = mServiceRef.get();
+ if (service == null) {
+ return;
+ }
+ List<IMediaRouter2Manager> managers = new ArrayList<>();
+ synchronized (service.mLock) {
+ for (ManagerRecord managerRecord : mUserRecord.mManagerRecords) {
+ if (TextUtils.equals(managerRecord.mTargetPackageName, routerPackageName)) {
+ managers.add(managerRecord.mManager);
+ }
+ }
+ for (IMediaRouter2Manager manager : managers) {
+ try {
+ manager.notifyDeviceSuggestionsUpdated(
+ routerPackageName, suggestingPackageName, suggestedDeviceInfo);
+ } catch (RemoteException ex) {
+ Slog.w(
+ TAG,
+ "Failed to notify suggesteion changed. Manager probably died.",
+ ex);
+ }
+ }
+ for (RouterRecord routerRecord : mUserRecord.mRouterRecords) {
+ if (TextUtils.equals(routerRecord.mPackageName, routerPackageName)) {
+ routerRecord.notifyDeviceSuggestionsUpdated(
+ suggestingPackageName, suggestedDeviceInfo);
+ }
+ }
+ }
+ }
+
private void updateDiscoveryPreferenceOnHandler() {
MediaRouter2ServiceImpl service = mServiceRef.get();
if (service == null) {
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 35bb19943a24..11f449e790a8 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -49,6 +49,7 @@ import android.media.RemoteDisplayState.RemoteDisplayInfo;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
+import android.media.SuggestedDeviceInfo;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
@@ -80,6 +81,7 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
/**
@@ -526,6 +528,21 @@ public final class MediaRouterService extends IMediaRouterService.Stub
// Binder call
@Override
+ public void setDeviceSuggestionsWithRouter2(
+ IMediaRouter2 router, @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ mService2.setDeviceSuggestionsWithRouter2(router, suggestedDeviceInfo);
+ }
+
+ // Binder call
+ @Override
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithRouter2(
+ IMediaRouter2 router) {
+ return mService2.getDeviceSuggestionsWithRouter2(router);
+ }
+
+ // Binder call
+ @Override
public List<RoutingSessionInfo> getRemoteSessions(IMediaRouter2Manager manager) {
return mService2.getRemoteSessions(manager);
}
@@ -666,6 +683,22 @@ public final class MediaRouterService extends IMediaRouterService.Stub
return mService2.showMediaOutputSwitcherWithProxyRouter(proxyRouter);
}
+ // Binder call
+ @Override
+ public void setDeviceSuggestionsWithManager(
+ @NonNull IMediaRouter2Manager manager,
+ @Nullable List<SuggestedDeviceInfo> suggestedDeviceInfo) {
+ mService2.setDeviceSuggestionsWithManager(manager, suggestedDeviceInfo);
+ }
+
+ // Binder call
+ @Override
+ @Nullable
+ public Map<String, List<SuggestedDeviceInfo>> getDeviceSuggestionsWithManager(
+ IMediaRouter2Manager manager) {
+ return mService2.getDeviceSuggestionsWithManager(manager);
+ }
+
void restoreBluetoothA2dp() {
try {
boolean a2dpOn;
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 9e38435ff7f1..ad108f64ffe3 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -28,6 +28,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
+import android.hardware.audio.effect.DefaultExtension;
import android.hardware.tv.mediaquality.AmbientBacklightColorFormat;
import android.hardware.tv.mediaquality.IMediaQuality;
import android.hardware.tv.mediaquality.IPictureProfileAdjustmentListener;
@@ -57,6 +58,7 @@ import android.os.Binder;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
+import android.os.Parcel;
import android.os.PersistableBundle;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
@@ -365,13 +367,21 @@ public class MediaQualityService extends SystemService {
try {
if (mMediaQuality != null) {
+ PictureParameters pp = new PictureParameters();
PictureParameter[] pictureParameters = MediaQualityUtils
.convertPersistableBundleToPictureParameterList(params);
- PictureParameters pp = new PictureParameters();
+ PersistableBundle vendorPictureParameters = params
+ .getPersistableBundle(BaseParameters.VENDOR_PARAMETERS);
+ Parcel parcel = Parcel.obtain();
+ if (vendorPictureParameters != null) {
+ setVendorPictureParameters(pp, parcel, vendorPictureParameters);
+ }
+
pp.pictureParameters = pictureParameters;
mMediaQuality.sendDefaultPictureParameters(pp);
+ parcel.recycle();
return true;
}
} catch (RemoteException e) {
@@ -1419,11 +1429,19 @@ public class MediaQualityService extends SystemService {
MediaQualityUtils.convertPersistableBundleToPictureParameterList(
params);
+ PersistableBundle vendorPictureParameters = params
+ .getPersistableBundle(BaseParameters.VENDOR_PARAMETERS);
+ Parcel parcel = Parcel.obtain();
+ if (vendorPictureParameters != null) {
+ setVendorPictureParameters(pictureParameters, parcel, vendorPictureParameters);
+ }
+
android.hardware.tv.mediaquality.PictureProfile toReturn =
new android.hardware.tv.mediaquality.PictureProfile();
toReturn.pictureProfileId = id;
toReturn.parameters = pictureParameters;
+ parcel.recycle();
return toReturn;
}
@@ -1729,4 +1747,16 @@ public class MediaQualityService extends SystemService {
return android.hardware.tv.mediaquality.IMediaQualityCallback.Stub.VERSION;
}
}
+
+ private void setVendorPictureParameters(
+ PictureParameters pictureParameters,
+ Parcel parcel,
+ PersistableBundle vendorPictureParameters) {
+ vendorPictureParameters.writeToParcel(parcel, 0);
+ byte[] vendorBundleToByteArray = parcel.marshall();
+ DefaultExtension defaultExtension = new DefaultExtension();
+ defaultExtension.bytes = Arrays.copyOf(
+ vendorBundleToByteArray, vendorBundleToByteArray.length);
+ pictureParameters.vendorPictureParameters.setParcelable(defaultExtension);
+ }
}
diff --git a/services/core/java/com/android/server/os/instrumentation/OWNERS b/services/core/java/com/android/server/os/instrumentation/OWNERS
new file mode 100644
index 000000000000..2522426d93f8
--- /dev/null
+++ b/services/core/java/com/android/server/os/instrumentation/OWNERS
@@ -0,0 +1 @@
+include platform/packages/modules/UprobeStats:/OWNERS \ No newline at end of file
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 46dc75817a36..3230e891db55 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -4240,66 +4240,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (!useKeyGestureEventHandler()) {
return;
}
- mInputManager.registerKeyGestureEventHandler(new InputManager.KeyGestureEventHandler() {
- @Override
- public boolean handleKeyGestureEvent(@NonNull KeyGestureEvent event,
- @Nullable IBinder focusedToken) {
- boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event,
- focusedToken);
- if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch(
- (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
- mPowerKeyHandled = true;
- }
- return handled;
- }
-
- @Override
- public boolean isKeyGestureSupported(int gestureType) {
- switch (gestureType) {
- case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_VOICE_ASSISTANT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_HOME:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_BACK:
- case KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION:
- case KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE:
- case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER:
- case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP:
- case KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN:
- case KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER:
- case KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH:
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_DO_NOT_DISTURB:
- case KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD:
- case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
- case KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK:
- case KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_VOICE_ACCESS:
- return true;
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
- return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
- isKeyguardLocked());
- case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
- return mAccessibilityShortcutController.isAccessibilityShortcutAvailable(
- false);
- default:
- return false;
- }
+ mInputManager.registerKeyGestureEventHandler((event, focusedToken) -> {
+ boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event,
+ focusedToken);
+ if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch(
+ (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) {
+ mPowerKeyHandled = true;
}
+ return handled;
});
}
@@ -4457,13 +4405,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
cancelPendingScreenshotChordAction();
}
return true;
- case KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD:
- if (start) {
- interceptAccessibilityShortcutChord();
- } else {
- cancelPendingAccessibilityShortcutAction();
- }
- return true;
case KeyGestureEvent.KEY_GESTURE_TYPE_RINGER_TOGGLE_CHORD:
if (start) {
interceptRingerToggleChord();
@@ -4481,14 +4422,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
cancelGlobalActionsAction();
}
return true;
- // TODO (b/358569822): Consolidate TV and non-TV gestures into same KeyGestureEvent
- case KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD:
- if (start) {
- interceptAccessibilityGestureTv();
- } else {
- cancelAccessibilityGestureTv();
- }
- return true;
case KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT:
if (start) {
interceptBugreportGestureTv();
diff --git a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java
index c8e7a8dea5c3..250e99b47b1a 100644
--- a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java
+++ b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java
@@ -19,10 +19,7 @@ package com.android.server.updates;
import android.content.Context;
import android.content.Intent;
-import java.io.File;
-
public class CertPinInstallReceiver extends ConfigUpdateInstallReceiver {
- private static final String KEYCHAIN_DIR = "/data/misc/keychain/";
public CertPinInstallReceiver() {
super("/data/misc/keychain/", "pins", "metadata/", "version");
@@ -30,22 +27,7 @@ public class CertPinInstallReceiver extends ConfigUpdateInstallReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
- if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
- if (com.android.server.flags.Flags.certpininstallerRemoval()) {
- File pins = new File(KEYCHAIN_DIR + "pins");
- if (pins.exists()) {
- pins.delete();
- }
- File version = new File(KEYCHAIN_DIR + "metadata/version");
- if (version.exists()) {
- version.delete();
- }
- File metadata = new File(KEYCHAIN_DIR + "metadata");
- if (metadata.exists()) {
- metadata.delete();
- }
- }
- } else if (!com.android.server.flags.Flags.certpininstallerRemoval()) {
+ if (!com.android.server.flags.Flags.certpininstallerRemoval()) {
super.onReceive(context, intent);
}
}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index 8e8455ad5288..6e640d890fb8 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -17,7 +17,6 @@
package com.android.server.wallpaper;
import static android.app.WallpaperManager.ORIENTATION_LANDSCAPE;
-import static android.app.WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE;
import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
import static android.app.WallpaperManager.getOrientation;
import static android.app.WallpaperManager.getRotatedOrientation;
@@ -85,20 +84,11 @@ public class WallpaperCropper {
private final WallpaperDisplayHelper mWallpaperDisplayHelper;
- /**
- * Helpers exposed to the window manager part (WallpaperController)
- */
- public interface WallpaperCropUtils {
-
- /**
- * Equivalent to {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)}
- */
- Rect getCrop(Point displaySize, Point bitmapSize,
- SparseArray<Rect> suggestedCrops, boolean rtl);
- }
+ private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo;
WallpaperCropper(WallpaperDisplayHelper wallpaperDisplayHelper) {
mWallpaperDisplayHelper = wallpaperDisplayHelper;
+ mDefaultDisplayInfo = mWallpaperDisplayHelper.getDefaultDisplayInfo();
}
/**
@@ -116,16 +106,16 @@ public class WallpaperCropper {
* {@link #getAdjustedCrop}.
* </ul>
*
- * @param displaySize The dimensions of the surface where we want to render the wallpaper
- * @param bitmapSize The dimensions of the wallpaper bitmap
- * @param rtl Whether the device is right-to-left
- * @param suggestedCrops An optional list of user-defined crops for some orientations.
- * If there is a suggested crop for
+ * @param displaySize The dimensions of the surface where we want to render the wallpaper
+ * @param defaultDisplayInfo The default display info
+ * @param bitmapSize The dimensions of the wallpaper bitmap
+ * @param rtl Whether the device is right-to-left
+ * @param suggestedCrops An optional list of user-defined crops for some orientations.
*
* @return A Rect indicating how to crop the bitmap for the current display.
*/
- public Rect getCrop(Point displaySize, Point bitmapSize,
- SparseArray<Rect> suggestedCrops, boolean rtl) {
+ public static Rect getCrop(Point displaySize, WallpaperDefaultDisplayInfo defaultDisplayInfo,
+ Point bitmapSize, SparseArray<Rect> suggestedCrops, boolean rtl) {
int orientation = getOrientation(displaySize);
@@ -135,23 +125,24 @@ public class WallpaperCropper {
// The first exception is if the device is a foldable and we're on the folded screen.
// In that case, show the center of what's on the unfolded screen.
- int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation);
+ int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation(orientation);
if (unfoldedOrientation != ORIENTATION_UNKNOWN) {
// Let the system know that we're showing the full image on the unfolded screen
SparseArray<Rect> newSuggestedCrops = new SparseArray<>();
newSuggestedCrops.put(unfoldedOrientation, crop);
// This will fall into "Case 4" of this function and center the folded screen
- return getCrop(displaySize, bitmapSize, newSuggestedCrops, rtl);
+ return getCrop(displaySize, defaultDisplayInfo, bitmapSize, newSuggestedCrops,
+ rtl);
}
// The second exception is if we're on tablet and we're on portrait mode.
// In that case, center the wallpaper relatively to landscape and put some parallax.
- boolean isTablet = mWallpaperDisplayHelper.isLargeScreen()
- && !mWallpaperDisplayHelper.isFoldable();
+ boolean isTablet = defaultDisplayInfo.isLargeScreen && !defaultDisplayInfo.isFoldable;
if (isTablet && displaySize.x < displaySize.y) {
Point rotatedDisplaySize = new Point(displaySize.y, displaySize.x);
// compute the crop on landscape (without parallax)
- Rect landscapeCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl);
+ Rect landscapeCrop = getCrop(rotatedDisplaySize, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl);
landscapeCrop = noParallax(landscapeCrop, rotatedDisplaySize, bitmapSize, rtl);
// compute the crop on portrait at the center of the landscape crop
crop = getAdjustedCrop(landscapeCrop, bitmapSize, displaySize, false, rtl, ADD);
@@ -173,7 +164,8 @@ public class WallpaperCropper {
if (testCrop == null || testCrop.left < 0 || testCrop.top < 0
|| testCrop.right > bitmapSize.x || testCrop.bottom > bitmapSize.y) {
Slog.w(TAG, "invalid crop: " + testCrop + " for bitmap size: " + bitmapSize);
- return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl);
+ return getCrop(displaySize, defaultDisplayInfo, bitmapSize, new SparseArray<>(),
+ rtl);
}
}
@@ -185,10 +177,9 @@ public class WallpaperCropper {
// Case 3: if we have the 90° rotated orientation in the suggested crops, reuse it and
// trying to preserve the zoom level and the center of the image
- SparseArray<Point> defaultDisplaySizes = mWallpaperDisplayHelper.getDefaultDisplaySizes();
int rotatedOrientation = getRotatedOrientation(orientation);
suggestedCrop = suggestedCrops.get(rotatedOrientation);
- Point suggestedDisplaySize = defaultDisplaySizes.get(rotatedOrientation);
+ Point suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(rotatedOrientation);
if (suggestedCrop != null) {
// only keep the visible part (without parallax)
Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl);
@@ -197,9 +188,9 @@ public class WallpaperCropper {
// Case 4: if the device is a foldable, if we're looking for a folded orientation and have
// the suggested crop of the relative unfolded orientation, reuse it by removing content.
- int unfoldedOrientation = mWallpaperDisplayHelper.getUnfoldedOrientation(orientation);
+ int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation(orientation);
suggestedCrop = suggestedCrops.get(unfoldedOrientation);
- suggestedDisplaySize = defaultDisplaySizes.get(unfoldedOrientation);
+ suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(unfoldedOrientation);
if (suggestedCrop != null) {
// compute the visible part (without parallax) of the unfolded screen
Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl);
@@ -207,8 +198,11 @@ public class WallpaperCropper {
Rect res = getAdjustedCrop(adjustedCrop, bitmapSize, displaySize, false, rtl, REMOVE);
// if we removed some width, add it back to add a parallax effect
if (res.width() < adjustedCrop.width()) {
- if (rtl) res.left = Math.min(res.left, adjustedCrop.left);
- else res.right = Math.max(res.right, adjustedCrop.right);
+ if (rtl) {
+ res.left = Math.min(res.left, adjustedCrop.left);
+ } else {
+ res.right = Math.max(res.right, adjustedCrop.right);
+ }
// use getAdjustedCrop(parallax=true) to make sure we don't exceed MAX_PARALLAX
res = getAdjustedCrop(res, bitmapSize, displaySize, true, rtl, ADD);
}
@@ -218,9 +212,9 @@ public class WallpaperCropper {
// Case 5: if the device is a foldable, if we're looking for an unfolded orientation and
// have the suggested crop of the relative folded orientation, reuse it by adding content.
- int foldedOrientation = mWallpaperDisplayHelper.getFoldedOrientation(orientation);
+ int foldedOrientation = defaultDisplayInfo.getFoldedOrientation(orientation);
suggestedCrop = suggestedCrops.get(foldedOrientation);
- suggestedDisplaySize = defaultDisplaySizes.get(foldedOrientation);
+ suggestedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(foldedOrientation);
if (suggestedCrop != null) {
// only keep the visible part (without parallax)
Rect adjustedCrop = noParallax(suggestedCrop, suggestedDisplaySize, bitmapSize, rtl);
@@ -229,17 +223,19 @@ public class WallpaperCropper {
// Case 6: for a foldable device, try to combine case 3 + case 4 or 5:
// rotate, then fold or unfold
- Point rotatedDisplaySize = defaultDisplaySizes.get(rotatedOrientation);
+ Point rotatedDisplaySize = defaultDisplayInfo.defaultDisplaySizes.get(rotatedOrientation);
if (rotatedDisplaySize != null) {
- int rotatedFolded = mWallpaperDisplayHelper.getFoldedOrientation(rotatedOrientation);
- int rotateUnfolded = mWallpaperDisplayHelper.getUnfoldedOrientation(rotatedOrientation);
+ int rotatedFolded = defaultDisplayInfo.getFoldedOrientation(rotatedOrientation);
+ int rotateUnfolded = defaultDisplayInfo.getUnfoldedOrientation(rotatedOrientation);
for (int suggestedOrientation : new int[]{rotatedFolded, rotateUnfolded}) {
suggestedCrop = suggestedCrops.get(suggestedOrientation);
if (suggestedCrop != null) {
- Rect rotatedCrop = getCrop(rotatedDisplaySize, bitmapSize, suggestedCrops, rtl);
+ Rect rotatedCrop = getCrop(rotatedDisplaySize, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl);
SparseArray<Rect> rotatedCropMap = new SparseArray<>();
rotatedCropMap.put(rotatedOrientation, rotatedCrop);
- return getCrop(displaySize, bitmapSize, rotatedCropMap, rtl);
+ return getCrop(displaySize, defaultDisplayInfo, bitmapSize, rotatedCropMap,
+ rtl);
}
}
}
@@ -248,8 +244,8 @@ public class WallpaperCropper {
Slog.w(TAG, "Could not find a proper default crop for display: " + displaySize
+ ", bitmap size: " + bitmapSize + ", suggested crops: " + suggestedCrops
+ ", orientation: " + orientation + ", rtl: " + rtl
- + ", defaultDisplaySizes: " + defaultDisplaySizes);
- return getCrop(displaySize, bitmapSize, new SparseArray<>(), rtl);
+ + ", defaultDisplaySizes: " + defaultDisplayInfo.defaultDisplaySizes);
+ return getCrop(displaySize, defaultDisplayInfo, bitmapSize, new SparseArray<>(), rtl);
}
/**
@@ -445,7 +441,7 @@ public class WallpaperCropper {
Rect suggestedCrop = suggestedCrops.get(orientation);
if (suggestedCrop != null) {
adjustedSuggestedCrops.put(orientation,
- getCrop(displaySize, bitmapSize, suggestedCrops, rtl));
+ getCrop(displaySize, mDefaultDisplayInfo, bitmapSize, suggestedCrops, rtl));
}
}
@@ -455,7 +451,8 @@ public class WallpaperCropper {
int orientation = defaultDisplaySizes.keyAt(i);
if (result.contains(orientation)) continue;
Point displaySize = defaultDisplaySizes.valueAt(i);
- Rect newCrop = getCrop(displaySize, bitmapSize, adjustedSuggestedCrops, rtl);
+ Rect newCrop = getCrop(displaySize, mDefaultDisplayInfo, bitmapSize,
+ adjustedSuggestedCrops, rtl);
result.put(orientation, newCrop);
}
return result;
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
index ba0262a8bd19..69f0ef7c430e 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java
@@ -542,9 +542,11 @@ public class WallpaperDataParser {
// to support back compatibility in B&R, save the crops for one orientation in the
// legacy "cropLeft", "cropTop", "cropRight", "cropBottom" entries
int orientationToPutInLegacyCrop = wallpaper.mOrientationWhenSet;
- if (mWallpaperDisplayHelper.isFoldable()) {
- int unfoldedOrientation = mWallpaperDisplayHelper
- .getUnfoldedOrientation(orientationToPutInLegacyCrop);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo =
+ mWallpaperDisplayHelper.getDefaultDisplayInfo();
+ if (defaultDisplayInfo.isFoldable) {
+ int unfoldedOrientation = defaultDisplayInfo.getUnfoldedOrientation(
+ orientationToPutInLegacyCrop);
if (unfoldedOrientation != ORIENTATION_UNKNOWN) {
orientationToPutInLegacyCrop = unfoldedOrientation;
}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java b/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java
new file mode 100644
index 000000000000..dabe91968338
--- /dev/null
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDefaultDisplayInfo.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wallpaper;
+
+import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
+import static android.app.WallpaperManager.getRotatedOrientation;
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
+
+import android.app.WallpaperManager;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.util.SparseArray;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+
+/** A data class for the default display attributes used in wallpaper related operations. */
+public final class WallpaperDefaultDisplayInfo {
+ /**
+ * A data class representing the screen orientations for a foldable device in the folded and
+ * unfolded states.
+ */
+ @VisibleForTesting
+ static final class FoldableOrientations {
+ @WallpaperManager.ScreenOrientation
+ public final int foldedOrientation;
+ @WallpaperManager.ScreenOrientation
+ public final int unfoldedOrientation;
+
+ FoldableOrientations(int foldedOrientation, int unfoldedOrientation) {
+ this.foldedOrientation = foldedOrientation;
+ this.unfoldedOrientation = unfoldedOrientation;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (!(other instanceof FoldableOrientations that)) return false;
+ return foldedOrientation == that.foldedOrientation
+ && unfoldedOrientation == that.unfoldedOrientation;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(foldedOrientation, unfoldedOrientation);
+ }
+ }
+
+ public final SparseArray<Point> defaultDisplaySizes;
+ public final boolean isLargeScreen;
+ public final boolean isFoldable;
+ @VisibleForTesting
+ final List<FoldableOrientations> foldableOrientations;
+
+ public WallpaperDefaultDisplayInfo() {
+ this.defaultDisplaySizes = new SparseArray<>();
+ this.isLargeScreen = false;
+ this.isFoldable = false;
+ this.foldableOrientations = Collections.emptyList();
+ }
+
+ public WallpaperDefaultDisplayInfo(WindowManager windowManager, Resources resources) {
+ Set<WindowMetrics> metrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY);
+ boolean isFoldable = resources.getIntArray(R.array.config_foldedDeviceStates).length > 0;
+ if (isFoldable) {
+ this.foldableOrientations = getFoldableOrientations(metrics);
+ } else {
+ this.foldableOrientations = Collections.emptyList();
+ }
+ this.defaultDisplaySizes = getDisplaySizes(metrics);
+ this.isLargeScreen = isLargeScreen(metrics);
+ this.isFoldable = isFoldable;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+ if (!(other instanceof WallpaperDefaultDisplayInfo that)) return false;
+ return isLargeScreen == that.isLargeScreen && isFoldable == that.isFoldable
+ && defaultDisplaySizes.contentEquals(that.defaultDisplaySizes)
+ && Objects.equals(foldableOrientations, that.foldableOrientations);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * Objects.hash(isLargeScreen, isFoldable, foldableOrientations)
+ + defaultDisplaySizes.contentHashCode();
+ }
+
+ /**
+ * Returns the folded orientation corresponds to the {@code unfoldedOrientation} found in
+ * {@link #foldableOrientations}. If not found, returns
+ * {@link WallpaperManager.ORIENTATION_UNKNOWN}.
+ */
+ public int getFoldedOrientation(int unfoldedOrientation) {
+ for (FoldableOrientations orientations : foldableOrientations) {
+ if (orientations.unfoldedOrientation == unfoldedOrientation) {
+ return orientations.foldedOrientation;
+ }
+ }
+ return ORIENTATION_UNKNOWN;
+ }
+
+ /**
+ * Returns the unfolded orientation corresponds to the {@code foldedOrientation} found in
+ * {@link #foldableOrientations}. If not found, returns
+ * {@link WallpaperManager.ORIENTATION_UNKNOWN}.
+ */
+ public int getUnfoldedOrientation(int foldedOrientation) {
+ for (FoldableOrientations orientations : foldableOrientations) {
+ if (orientations.foldedOrientation == foldedOrientation) {
+ return orientations.unfoldedOrientation;
+ }
+ }
+ return ORIENTATION_UNKNOWN;
+ }
+
+ private static SparseArray<Point> getDisplaySizes(Set<WindowMetrics> displayMetrics) {
+ SparseArray<Point> displaySizes = new SparseArray<>();
+ for (WindowMetrics metric : displayMetrics) {
+ Rect bounds = metric.getBounds();
+ Point displaySize = new Point(bounds.width(), bounds.height());
+ Point reversedDisplaySize = new Point(displaySize.y, displaySize.x);
+ for (Point point : List.of(displaySize, reversedDisplaySize)) {
+ int orientation = WallpaperManager.getOrientation(point);
+ // don't add an entry if there is already a larger display of the same orientation
+ Point display = displaySizes.get(orientation);
+ if (display == null || display.x * display.y < point.x * point.y) {
+ displaySizes.put(orientation, point);
+ }
+ }
+ }
+ return displaySizes;
+ }
+
+ private static boolean isLargeScreen(Set<WindowMetrics> displayMetrics) {
+ float smallestWidth = Float.MAX_VALUE;
+ for (WindowMetrics metric : displayMetrics) {
+ Rect bounds = metric.getBounds();
+ smallestWidth = Math.min(smallestWidth, bounds.width() / metric.getDensity());
+ }
+ return smallestWidth >= LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
+ }
+
+ /**
+ * Determines all potential foldable orientations, populating {@code
+ * outFoldableOrientationPairs} with pairs of (folded orientation, unfolded orientation). If
+ * {@code defaultDisplayMetrics} isn't for foldable, {@code outFoldableOrientationPairs} will
+ * not be populated.
+ */
+ private static List<FoldableOrientations> getFoldableOrientations(
+ Set<WindowMetrics> defaultDisplayMetrics) {
+ if (defaultDisplayMetrics.size() != 2) {
+ return Collections.emptyList();
+ }
+ List<FoldableOrientations> foldableOrientations = new ArrayList<>();
+ float surface = 0;
+ int firstOrientation = -1;
+ for (WindowMetrics metric : defaultDisplayMetrics) {
+ Rect bounds = metric.getBounds();
+ Point displaySize = new Point(bounds.width(), bounds.height());
+
+ int orientation = WallpaperManager.getOrientation(displaySize);
+ float newSurface = displaySize.x * displaySize.y
+ / (metric.getDensity() * metric.getDensity());
+ if (surface <= 0) {
+ surface = newSurface;
+ firstOrientation = orientation;
+ } else {
+ FoldableOrientations orientations = (newSurface > surface)
+ ? new FoldableOrientations(firstOrientation, orientation)
+ : new FoldableOrientations(orientation, firstOrientation);
+ FoldableOrientations rotatedOrientations = new FoldableOrientations(
+ getRotatedOrientation(orientations.foldedOrientation),
+ getRotatedOrientation(orientations.unfoldedOrientation));
+ foldableOrientations.add(orientations);
+ foldableOrientations.add(rotatedOrientations);
+ }
+ }
+ return Collections.unmodifiableList(foldableOrientations);
+ }
+}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
index 3636f5aa8f27..bff5fc9c49f3 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
@@ -16,31 +16,25 @@
package com.android.server.wallpaper;
-import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
-import static android.app.WallpaperManager.getRotatedOrientation;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.window.flags.Flags.multiCrop;
import android.app.WallpaperManager;
+import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Binder;
import android.os.Debug;
-import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.WindowManager;
-import android.view.WindowMetrics;
import com.android.server.wm.WindowManagerInternal;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
import java.util.function.Consumer;
/**
@@ -59,65 +53,25 @@ class WallpaperDisplayHelper {
}
private static final String TAG = WallpaperDisplayHelper.class.getSimpleName();
- private static final float LARGE_SCREEN_MIN_DP = 600f;
private final SparseArray<DisplayData> mDisplayDatas = new SparseArray<>();
private final DisplayManager mDisplayManager;
private final WindowManagerInternal mWindowManagerInternal;
- private final SparseArray<Point> mDefaultDisplaySizes = new SparseArray<>();
- // related orientations pairs for foldable (folded orientation, unfolded orientation)
- private final List<Pair<Integer, Integer>> mFoldableOrientationPairs = new ArrayList<>();
-
- private final boolean mIsFoldable;
- private boolean mIsLargeScreen = false;
+ private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo;
WallpaperDisplayHelper(
DisplayManager displayManager,
WindowManager windowManager,
WindowManagerInternal windowManagerInternal,
- boolean isFoldable) {
+ Resources resources) {
mDisplayManager = displayManager;
mWindowManagerInternal = windowManagerInternal;
- mIsFoldable = isFoldable;
- if (!multiCrop()) return;
- Set<WindowMetrics> metrics = windowManager.getPossibleMaximumWindowMetrics(DEFAULT_DISPLAY);
- boolean populateOrientationPairs = isFoldable && metrics.size() == 2;
- float surface = 0;
- int firstOrientation = -1;
- for (WindowMetrics metric: metrics) {
- Rect bounds = metric.getBounds();
- Point displaySize = new Point(bounds.width(), bounds.height());
- Point reversedDisplaySize = new Point(displaySize.y, displaySize.x);
- for (Point point : List.of(displaySize, reversedDisplaySize)) {
- int orientation = WallpaperManager.getOrientation(point);
- // don't add an entry if there is already a larger display of the same orientation
- Point display = mDefaultDisplaySizes.get(orientation);
- if (display == null || display.x * display.y < point.x * point.y) {
- mDefaultDisplaySizes.put(orientation, point);
- }
- }
-
- mIsLargeScreen |= (displaySize.x / metric.getDensity() >= LARGE_SCREEN_MIN_DP);
-
- if (populateOrientationPairs) {
- int orientation = WallpaperManager.getOrientation(displaySize);
- float newSurface = displaySize.x * displaySize.y
- / (metric.getDensity() * metric.getDensity());
- if (surface <= 0) {
- surface = newSurface;
- firstOrientation = orientation;
- } else {
- Pair<Integer, Integer> pair = (newSurface > surface)
- ? new Pair<>(firstOrientation, orientation)
- : new Pair<>(orientation, firstOrientation);
- Pair<Integer, Integer> rotatedPair = new Pair<>(
- getRotatedOrientation(pair.first), getRotatedOrientation(pair.second));
- mFoldableOrientationPairs.add(pair);
- mFoldableOrientationPairs.add(rotatedPair);
- }
- }
+ if (!multiCrop()) {
+ mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo();
+ return;
}
+ mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(windowManager, resources);
}
DisplayData getDisplayDataOrCreate(int displayId) {
@@ -203,51 +157,21 @@ class WallpaperDisplayHelper {
}
SparseArray<Point> getDefaultDisplaySizes() {
- return mDefaultDisplaySizes;
+ return mDefaultDisplayInfo.defaultDisplaySizes;
}
/** Return the number of pixel of the largest dimension of the default display */
int getDefaultDisplayLargestDimension() {
+ SparseArray<Point> defaultDisplaySizes = mDefaultDisplayInfo.defaultDisplaySizes;
int result = -1;
- for (int i = 0; i < mDefaultDisplaySizes.size(); i++) {
- Point size = mDefaultDisplaySizes.valueAt(i);
+ for (int i = 0; i < defaultDisplaySizes.size(); i++) {
+ Point size = defaultDisplaySizes.valueAt(i);
result = Math.max(result, Math.max(size.x, size.y));
}
return result;
}
- boolean isFoldable() {
- return mIsFoldable;
- }
-
- /**
- * Return true if any of the screens of the default display is considered large (DP >= 600)
- */
- boolean isLargeScreen() {
- return mIsLargeScreen;
- }
-
- /**
- * If a given orientation corresponds to an unfolded orientation on foldable, return the
- * corresponding folded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the
- * device is not a foldable.
- */
- int getFoldedOrientation(int orientation) {
- for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) {
- if (pair.second.equals(orientation)) return pair.first;
- }
- return ORIENTATION_UNKNOWN;
- }
-
- /**
- * If a given orientation corresponds to a folded orientation on foldable, return the
- * corresponding unfolded orientation. Otherwise, return UNKNOWN. Always return UNKNOWN if the
- * device is not a foldable.
- */
- int getUnfoldedOrientation(int orientation) {
- for (Pair<Integer, Integer> pair : mFoldableOrientationPairs) {
- if (pair.first.equals(orientation)) return pair.second;
- }
- return ORIENTATION_UNKNOWN;
+ public WallpaperDefaultDisplayInfo getDefaultDisplayInfo() {
+ return mDefaultDisplayInfo;
}
}
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index bac732637d8d..e7da33d50b27 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -1666,12 +1666,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
displayManager.registerDisplayListener(mDisplayListener, null /* handler */);
WindowManager windowManager = mContext.getSystemService(WindowManager.class);
- boolean isFoldable = mContext.getResources()
- .getIntArray(R.array.config_foldedDeviceStates).length > 0;
mWallpaperDisplayHelper = new WallpaperDisplayHelper(
- displayManager, windowManager, mWindowManagerInternal, isFoldable);
+ displayManager, windowManager, mWindowManagerInternal, mContext.getResources());
mWallpaperCropper = new WallpaperCropper(mWallpaperDisplayHelper);
- mWindowManagerInternal.setWallpaperCropUtils(mWallpaperCropper::getCrop);
mActivityManager = mContext.getSystemService(ActivityManager.class);
if (mContext.getResources().getBoolean(
@@ -2510,9 +2507,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
List<Rect> result = new ArrayList<>();
boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
== View.LAYOUT_DIRECTION_RTL;
+ WallpaperDefaultDisplayInfo defaultDisplayInfo =
+ mWallpaperDisplayHelper.getDefaultDisplayInfo();
for (Point displaySize : displaySizes) {
- result.add(mWallpaperCropper.getCrop(
- displaySize, croppedBitmapSize, adjustedRelativeSuggestedCrops, rtl));
+ result.add(WallpaperCropper.getCrop(displaySize, defaultDisplayInfo,
+ croppedBitmapSize, adjustedRelativeSuggestedCrops, rtl));
}
if (originalBitmap) result = WallpaperCropper.getOriginalCropHints(wallpaper, result);
return result;
@@ -2548,8 +2547,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
List<Rect> result = new ArrayList<>();
boolean rtl = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
== View.LAYOUT_DIRECTION_RTL;
+ WallpaperDefaultDisplayInfo defaultDisplayInfo =
+ mWallpaperDisplayHelper.getDefaultDisplayInfo();
for (Point displaySize : displaySizes) {
- result.add(mWallpaperCropper.getCrop(displaySize, bitmapSize, defaultCrops, rtl));
+ result.add(WallpaperCropper.getCrop(displaySize, defaultDisplayInfo, bitmapSize,
+ defaultCrops, rtl));
}
return result;
}
diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
index a94183849bc5..e2b47b92f232 100644
--- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
+++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java
@@ -112,7 +112,6 @@ import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.apphibernation.AppHibernationManagerInternal;
import com.android.server.apphibernation.AppHibernationService;
-import com.android.window.flags.Flags;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
@@ -807,14 +806,8 @@ class ActivityMetricsLogger {
}
final Task otherTask = otherInfo.mLastLaunchedActivity.getTask();
// The adjacent task is the split root in which activities are started
- final boolean isDescendantOfAdjacent;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- isDescendantOfAdjacent = launchedSplitRootTask.forOtherAdjacentTasks(
- otherTask::isDescendantOf);
- } else {
- isDescendantOfAdjacent = otherTask.isDescendantOf(
- launchedSplitRootTask.getAdjacentTask());
- }
+ final boolean isDescendantOfAdjacent = launchedSplitRootTask.forOtherAdjacentTasks(
+ otherTask::isDescendantOf);
if (isDescendantOfAdjacent) {
if (DEBUG_METRICS) {
Slog.i(TAG, "Found adjacent tasks t1=" + launchedActivityTask.mTaskId
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 3cd4db7d8dfc..e91d88901751 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -1716,6 +1716,7 @@ final class ActivityRecord extends WindowToken {
}
mAppCompatController.getLetterboxPolicy().onMovedToDisplay(mDisplayContent.getDisplayId());
+ mAppCompatController.getDisplayCompatModePolicy().onMovedToDisplay();
}
void layoutLetterboxIfNeeded(WindowState winHint) {
@@ -3801,19 +3802,10 @@ final class ActivityRecord extends WindowToken {
final TaskFragment taskFragment = getTaskFragment();
if (next != null && taskFragment != null && taskFragment.isEmbedded()) {
final TaskFragment organized = taskFragment.getOrganizedTaskFragment();
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- delayRemoval = organized != null
- && organized.topRunningActivity() == null
- && organized.isDelayLastActivityRemoval()
- && organized.forOtherAdjacentTaskFragments(next::isDescendantOf);
- } else {
- final TaskFragment adjacent =
- organized != null ? organized.getAdjacentTaskFragment() : null;
- if (adjacent != null && next.isDescendantOf(adjacent)
- && organized.topRunningActivity() == null) {
- delayRemoval = organized.isDelayLastActivityRemoval();
- }
- }
+ delayRemoval = organized != null
+ && organized.topRunningActivity() == null
+ && organized.isDelayLastActivityRemoval()
+ && organized.forOtherAdjacentTaskFragments(next::isDescendantOf);
}
// isNextNotYetVisible is to check if the next activity is invisible, or it has been
@@ -4787,11 +4779,6 @@ final class ActivityRecord extends WindowToken {
}
// Make sure the embedded adjacent can also be shown.
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final ActivityRecord adjacentActivity = taskFragment.getAdjacentTaskFragment()
- .getTopNonFinishingActivity();
- return canShowWhenLocked(adjacentActivity);
- }
final boolean hasAdjacentNotAllowToShow = taskFragment.forOtherAdjacentTaskFragments(
adjacentTF -> !canShowWhenLocked(adjacentTF.getTopNonFinishingActivity()));
return !hasAdjacentNotAllowToShow;
@@ -8980,6 +8967,7 @@ final class ActivityRecord extends WindowToken {
// Reset the existing override configuration so it can be updated according to the latest
// configuration.
mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode();
+ mAppCompatController.getDisplayCompatModePolicy().onProcessRestarted();
if (!attachedToProcess()) {
return;
diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
index cb122f2080a2..0f1939bfbb49 100644
--- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java
+++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java
@@ -34,7 +34,6 @@ import android.window.TaskSnapshot;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
-import com.android.window.flags.Flags;
import java.io.File;
import java.io.PrintWriter;
@@ -532,26 +531,6 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord
final int currentIndex = currTF.asTask() != null
? currentTask.mChildren.indexOf(currentActivity)
: currentTask.mChildren.indexOf(currTF);
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final int prevAdjacentIndex = currentTask.mChildren.indexOf(
- prevTF.getAdjacentTaskFragment());
- if (prevAdjacentIndex > currentIndex) {
- // PrevAdjacentTF already above currentActivity
- return;
- }
- // Add both the one below, and its adjacent.
- if (!inTransition || isInParticipant(initPrev, mTmpTransitionParticipants)) {
- result.add(initPrev);
- }
- final ActivityRecord prevAdjacentActivity = prevTF.getAdjacentTaskFragment()
- .getTopMostActivity();
- if (prevAdjacentActivity != null && (!inTransition
- || isInParticipant(prevAdjacentActivity, mTmpTransitionParticipants))) {
- result.add(prevAdjacentActivity);
- }
- return;
- }
-
final boolean hasAdjacentAboveCurrent = prevTF.forOtherAdjacentTaskFragments(
prevAdjacentTF -> {
final int prevAdjacentIndex = currentTask.mChildren.indexOf(prevAdjacentTF);
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 3f24da9d89f2..51025d204b46 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -60,6 +60,7 @@ import com.android.server.wm.ActivityStarter.DefaultFactory;
import com.android.server.wm.ActivityStarter.Factory;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -97,6 +98,9 @@ public class ActivityStartController {
/** Whether an {@link ActivityStarter} is currently executing (starting an Activity). */
private boolean mInExecution = false;
+ /** The {@link TaskDisplayArea}s that are currently starting home activity. */
+ private ArrayList<TaskDisplayArea> mHomeLaunchingTaskDisplayAreas = new ArrayList<>();
+
/**
* TODO(b/64750076): Capture information necessary for dump and
* {@link #postStartActivityProcessingForLastStarter} rather than keeping the entire object
@@ -162,6 +166,11 @@ public class ActivityStartController {
void startHomeActivity(Intent intent, ActivityInfo aInfo, String reason,
TaskDisplayArea taskDisplayArea) {
+ if (mHomeLaunchingTaskDisplayAreas.contains(taskDisplayArea)) {
+ Slog.e(TAG, "Abort starting home on " + taskDisplayArea + " recursively.");
+ return;
+ }
+
final ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN);
if (!ActivityRecord.isResolverActivity(aInfo.name)) {
@@ -186,13 +195,18 @@ public class ActivityStartController {
mSupervisor.endDeferResume();
}
- mLastHomeActivityStartResult = obtainStarter(intent, "startHomeActivity: " + reason)
- .setOutActivity(tmpOutRecord)
- .setCallingUid(0)
- .setActivityInfo(aInfo)
- .setActivityOptions(options.toBundle(),
- Binder.getCallingPid(), Binder.getCallingUid())
- .execute();
+ try {
+ mHomeLaunchingTaskDisplayAreas.add(taskDisplayArea);
+ mLastHomeActivityStartResult = obtainStarter(intent, "startHomeActivity: " + reason)
+ .setOutActivity(tmpOutRecord)
+ .setCallingUid(0)
+ .setActivityInfo(aInfo)
+ .setActivityOptions(options.toBundle(),
+ Binder.getCallingPid(), Binder.getCallingUid())
+ .execute();
+ } finally {
+ mHomeLaunchingTaskDisplayAreas.remove(taskDisplayArea);
+ }
mLastHomeActivityStartRecord = tmpOutRecord[0];
if (rootHomeTask.mInResumeTopActivity) {
// If we are in resume section already, home activity will be initialized, but not
@@ -479,9 +493,9 @@ public class ActivityStartController {
}
} catch (SecurityException securityException) {
ActivityStarter.logAndThrowExceptionForIntentRedirect(mService.mContext,
- "Creator URI Grant Caused Exception.", intent, creatorUid,
- creatorPackage, filterCallingUid, callingPackage,
- securityException);
+ ActivityStarter.INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION,
+ intent, creatorUid, creatorPackage, filterCallingUid,
+ callingPackage, securityException);
}
}
if ((aInfo.applicationInfo.privateFlags
@@ -720,6 +734,12 @@ public class ActivityStartController {
}
}
+ if (!mHomeLaunchingTaskDisplayAreas.isEmpty()) {
+ dumped = true;
+ pw.print(prefix);
+ pw.println("mHomeLaunchingTaskDisplayAreas:" + mHomeLaunchingTaskDisplayAreas);
+ }
+
if (!dumped) {
pw.print(prefix);
pw.println("(nothing)");
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 233f91385ca4..a84a008f66eb 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -65,6 +65,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TAS
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_CONFIGURATION;
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_TASKS;
import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS;
+import static com.android.internal.util.FrameworkStatsLog.INTENT_REDIRECT_BLOCKED;
import static com.android.server.pm.PackageArchiver.isArchivingEnabled;
import static com.android.server.wm.ActivityRecord.State.RESUMED;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW;
@@ -140,6 +141,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.HeavyWeightSwitcherActivity;
import com.android.internal.app.IVoiceInteractor;
import com.android.internal.protolog.ProtoLog;
+import com.android.internal.util.FrameworkStatsLog;
import com.android.server.UiThread;
import com.android.server.am.ActivityManagerService.IntentCreatorToken;
import com.android.server.am.PendingIntentRecord;
@@ -623,7 +625,7 @@ class ActivityStarter {
if ((intent.getExtendedFlags() & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN)
!= 0) {
logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext,
- "Unparceled intent does not have a creator token set.", intent,
+ ActivityStarter.INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN, intent,
intentCreatorUid, intentCreatorPackage, resolvedCallingUid,
resolvedCallingPackage, null);
}
@@ -659,9 +661,9 @@ class ActivityStarter {
}
} catch (SecurityException securityException) {
logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext,
- "Creator URI Grant Caused Exception.", intent, intentCreatorUid,
- intentCreatorPackage, resolvedCallingUid,
- resolvedCallingPackage, securityException);
+ ActivityStarter.INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION,
+ intent, intentCreatorUid, intentCreatorPackage,
+ resolvedCallingUid, resolvedCallingPackage, securityException);
}
}
} else {
@@ -683,9 +685,9 @@ class ActivityStarter {
}
} catch (SecurityException securityException) {
logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext,
- "Creator URI Grant Caused Exception.", intent, intentCreatorUid,
- intentCreatorPackage, resolvedCallingUid,
- resolvedCallingPackage, securityException);
+ ActivityStarter.INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION,
+ intent, intentCreatorUid, intentCreatorPackage,
+ resolvedCallingUid, resolvedCallingPackage, securityException);
}
}
}
@@ -1109,8 +1111,11 @@ class ActivityStarter {
if (sourceRecord != null) {
if (requestCode >= 0 && !sourceRecord.finishing) {
resultRecord = sourceRecord;
+ request.logMessage.append(" (rr=");
+ } else {
+ request.logMessage.append(" (sr=");
}
- request.logMessage.append(" (sr=" + System.identityHashCode(sourceRecord) + ")");
+ request.logMessage.append(System.identityHashCode(sourceRecord) + ")");
}
}
@@ -1261,27 +1266,27 @@ class ActivityStarter {
request.ignoreTargetSecurity, inTask != null, null, resultRecord,
resultRootTask)) {
abort = logAndAbortForIntentRedirect(mService.mContext,
- "Creator checkStartAnyActivityPermission Caused abortion.",
+ ActivityStarter.INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION,
intent, intentCreatorUid, intentCreatorPackage, callingUid,
callingPackage);
}
} catch (SecurityException e) {
logAndThrowExceptionForIntentRedirect(mService.mContext,
- "Creator checkStartAnyActivityPermission Caused Exception.",
+ ActivityStarter.INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION,
intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage,
e);
}
if (!mService.mIntentFirewall.checkStartActivity(intent, intentCreatorUid,
0, resolvedType, aInfo.applicationInfo)) {
abort = logAndAbortForIntentRedirect(mService.mContext,
- "Creator IntentFirewall.checkStartActivity Caused abortion.",
+ ActivityStarter.INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY,
intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage);
}
if (!mService.getPermissionPolicyInternal().checkStartActivity(intent,
intentCreatorUid, intentCreatorPackage)) {
abort = logAndAbortForIntentRedirect(mService.mContext,
- "Creator PermissionPolicyService.checkStartActivity Caused abortion.",
+ ActivityStarter.INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY,
intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage);
}
}
@@ -3626,13 +3631,41 @@ class ActivityStarter {
pw.println(mInTaskFragment);
}
+ /**
+ * Error codes for intent redirect.
+ *
+ * @hide
+ */
+ @IntDef(prefix = {"INTENT_REDIRECT_"}, value = {
+ INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN,
+ INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION,
+ INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION,
+ INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION,
+ INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY,
+ INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface IntentRedirectErrorCode {
+ }
+
+ /**
+ * Error codes for intent redirect issues
+ */
+ static final int INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN = 1;
+ static final int INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION = 2;
+ static final int INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION = 3;
+ static final int INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION = 4;
+ static final int INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY = 5;
+ static final int INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY = 6;
+
static void logAndThrowExceptionForIntentRedirect(@NonNull Context context,
- @NonNull String message, @NonNull Intent intent, int intentCreatorUid,
+ @IntentRedirectErrorCode int errorCode, @NonNull Intent intent, int intentCreatorUid,
@Nullable String intentCreatorPackage, int callingUid, @Nullable String callingPackage,
@Nullable SecurityException originalException) {
- String msg = getIntentRedirectPreventedLogMessage(message, intent, intentCreatorUid,
+ String msg = getIntentRedirectPreventedLogMessage(errorCode, intent, intentCreatorUid,
intentCreatorPackage, callingUid, callingPackage);
Slog.wtf(TAG, msg);
+ FrameworkStatsLog.write(INTENT_REDIRECT_BLOCKED, intentCreatorUid, callingUid, errorCode);
if (preventIntentRedirectShowToast()) {
UiThread.getHandler().post(
() -> Toast.makeText(context,
@@ -3646,12 +3679,13 @@ class ActivityStarter {
}
private static boolean logAndAbortForIntentRedirect(@NonNull Context context,
- @NonNull String message, @NonNull Intent intent, int intentCreatorUid,
+ @IntentRedirectErrorCode int errorCode, @NonNull Intent intent, int intentCreatorUid,
@Nullable String intentCreatorPackage, int callingUid,
@Nullable String callingPackage) {
- String msg = getIntentRedirectPreventedLogMessage(message, intent, intentCreatorUid,
+ String msg = getIntentRedirectPreventedLogMessage(errorCode, intent, intentCreatorUid,
intentCreatorPackage, callingUid, callingPackage);
Slog.wtf(TAG, msg);
+ FrameworkStatsLog.write(INTENT_REDIRECT_BLOCKED, intentCreatorUid, callingUid, errorCode);
if (preventIntentRedirectShowToast()) {
UiThread.getHandler().post(
() -> Toast.makeText(context,
@@ -3662,11 +3696,38 @@ class ActivityStarter {
ENABLE_PREVENT_INTENT_REDIRECT_TAKE_ACTION, callingUid);
}
- private static String getIntentRedirectPreventedLogMessage(@NonNull String message,
+ private static String getIntentRedirectPreventedLogMessage(
+ @IntentRedirectErrorCode int errorCode,
@NonNull Intent intent, int intentCreatorUid, @Nullable String intentCreatorPackage,
int callingUid, @Nullable String callingPackage) {
+ String message = getIntentRedirectErrorMessageFromCode(errorCode);
return "[IntentRedirect Hardening] " + message + " intentCreatorUid: " + intentCreatorUid
+ "; intentCreatorPackage: " + intentCreatorPackage + "; callingUid: " + callingUid
+ "; callingPackage: " + callingPackage + "; intent: " + intent;
}
+
+ private static String getIntentRedirectErrorMessageFromCode(
+ @IntentRedirectErrorCode int errorCode) {
+ return switch (errorCode) {
+ case INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN ->
+ "INTENT_REDIRECT_EXCEPTION_MISSING_OR_INVALID_TOKEN"
+ + " (Unparceled intent does not have a creator token set, throw exception.)";
+ case INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION ->
+ "INTENT_REDIRECT_EXCEPTION_GRANT_URI_PERMISSION"
+ + " (Creator URI permission grant throw exception.)";
+ case INTENT_REDIRECT_EXCEPTION_START_ANY_ACTIVITY_PERMISSION ->
+ "INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION"
+ + " (Creator checkStartAnyActivityPermission, throw exception)";
+ case INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION ->
+ "INTENT_REDIRECT_ABORT_START_ANY_ACTIVITY_PERMISSION"
+ + " (Creator checkStartAnyActivityPermission, abort)";
+ case INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY ->
+ "INTENT_REDIRECT_ABORT_INTENT_FIREWALL_START_ACTIVITY"
+ + " (Creator IntentFirewall.checkStartActivity, abort)";
+ case INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY ->
+ "INTENT_REDIRECT_ABORT_PERMISSION_POLICY_START_ACTIVITY"
+ + " (Creator PermissionPolicyService.checkStartActivity, abort)";
+ default -> "Unknown error code: " + errorCode;
+ };
+ }
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index a7f2153993bb..b0563128870a 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -164,7 +164,6 @@ import com.android.server.companion.virtual.VirtualDeviceManagerInternal;
import com.android.server.pm.SaferIntentUtils;
import com.android.server.utils.Slogf;
import com.android.server.wm.ActivityMetricsLogger.LaunchingState;
-import com.android.window.flags.Flags;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -2991,17 +2990,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
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);
- }
+ final boolean isAnyTranslucent = !isOpaque(child)
+ || child.asTaskFragment().forOtherAdjacentTaskFragments(
+ tf -> !isOpaque(tf));
if (!isAnyTranslucent) {
// This task fragment and all its adjacent task fragments are opaque,
// consider it opaque even if it doesn't fill its parent.
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index 48f08e945a59..c479591a5e0d 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -46,6 +46,8 @@ class AppCompatController {
private final AppCompatSizeCompatModePolicy mSizeCompatModePolicy;
@NonNull
private final AppCompatSandboxingPolicy mSandboxingPolicy;
+ @NonNull
+ private final AppCompatDisplayCompatModePolicy mDisplayCompatModePolicy;
AppCompatController(@NonNull WindowManagerService wmService,
@NonNull ActivityRecord activityRecord) {
@@ -69,6 +71,7 @@ class AppCompatController {
mSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(activityRecord,
mAppCompatOverrides);
mSandboxingPolicy = new AppCompatSandboxingPolicy(activityRecord);
+ mDisplayCompatModePolicy = new AppCompatDisplayCompatModePolicy();
}
@NonNull
@@ -151,6 +154,11 @@ class AppCompatController {
return mSandboxingPolicy;
}
+ @NonNull
+ AppCompatDisplayCompatModePolicy getDisplayCompatModePolicy() {
+ return mDisplayCompatModePolicy;
+ }
+
void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
getTransparentPolicy().dump(pw, prefix);
getLetterboxPolicy().dump(pw, prefix);
diff --git a/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java
new file mode 100644
index 000000000000..acf51707c894
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AppCompatDisplayCompatModePolicy.java
@@ -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.server.wm;
+
+import com.android.window.flags.Flags;
+
+/**
+ * Encapsulate app-compat logic for multi-display environments.
+ */
+class AppCompatDisplayCompatModePolicy {
+
+ private boolean mIsRestartMenuEnabledForDisplayMove;
+
+ boolean isRestartMenuEnabledForDisplayMove() {
+ return Flags.enableRestartMenuForConnectedDisplays() && mIsRestartMenuEnabledForDisplayMove;
+ }
+
+ void onMovedToDisplay() {
+ mIsRestartMenuEnabledForDisplayMove = true;
+ }
+
+ void onProcessRestarted() {
+ mIsRestartMenuEnabledForDisplayMove = false;
+ }
+}
diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
index b03aa5263927..0f1e36d70db2 100644
--- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
+++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java
@@ -361,7 +361,10 @@ class AppCompatSizeCompatModePolicy {
if (enableSizeCompatModeImprovementsForConnectedDisplays()) {
overrideConfig.touchscreen = fullConfig.touchscreen;
overrideConfig.navigation = fullConfig.navigation;
- overrideConfig.fontScale = fullConfig.fontScale;
+ overrideConfig.keyboard = fullConfig.keyboard;
+ overrideConfig.keyboardHidden = fullConfig.keyboardHidden;
+ overrideConfig.hardKeyboardHidden = fullConfig.hardKeyboardHidden;
+ overrideConfig.navigationHidden = fullConfig.navigationHidden;
}
// The smallest screen width is the short side of screen bounds. Because the bounds
// and density won't be changed, smallestScreenWidthDp is also fixed.
diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java
index 146044008b3f..b91a12598e01 100644
--- a/services/core/java/com/android/server/wm/AppCompatUtils.java
+++ b/services/core/java/com/android/server/wm/AppCompatUtils.java
@@ -161,6 +161,9 @@ final class AppCompatUtils {
top.mAppCompatController.getLetterboxOverrides()
.isLetterboxEducationEnabled());
+ appCompatTaskInfo.setRestartMenuEnabledForDisplayMove(top.mAppCompatController
+ .getDisplayCompatModePolicy().isRestartMenuEnabledForDisplayMove());
+
final AppCompatAspectRatioOverrides aspectRatioOverrides =
top.mAppCompatController.getAspectRatioOverrides();
appCompatTaskInfo.setUserFullscreenOverrideEnabled(
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index e9b7649e8cbd..dfe323c43abb 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -479,21 +479,16 @@ class BackNavigationController {
}
} else {
// If adjacent TF has companion to current TF, those two TF will be closed together.
- final TaskFragment adjacentTF;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- if (currTF.getAdjacentTaskFragments().size() > 2) {
- throw new IllegalStateException(
- "Not yet support 3+ adjacent for non-Task TFs");
- }
- final TaskFragment[] tmpAdjacent = new TaskFragment[1];
- currTF.forOtherAdjacentTaskFragments(tf -> {
- tmpAdjacent[0] = tf;
- return true;
- });
- adjacentTF = tmpAdjacent[0];
- } else {
- adjacentTF = currTF.getAdjacentTaskFragment();
+ if (currTF.getAdjacentTaskFragments().size() > 2) {
+ throw new IllegalStateException(
+ "Not yet support 3+ adjacent for non-Task TFs");
}
+ final TaskFragment[] tmpAdjacent = new TaskFragment[1];
+ currTF.forOtherAdjacentTaskFragments(tf -> {
+ tmpAdjacent[0] = tf;
+ return true;
+ });
+ final TaskFragment adjacentTF = tmpAdjacent[0];
if (isSecondCompanionToFirst(currTF, adjacentTF)) {
// The two TFs are adjacent (visually displayed side-by-side), search if any
// activity below the lowest one.
@@ -553,15 +548,6 @@ class BackNavigationController {
if (!prevTF.hasAdjacentTaskFragment()) {
return;
}
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment prevTFAdjacent = prevTF.getAdjacentTaskFragment();
- final ActivityRecord prevActivityAdjacent =
- prevTFAdjacent.getTopNonFinishingActivity();
- if (prevActivityAdjacent != null) {
- outPrevActivities.add(prevActivityAdjacent);
- }
- return;
- }
prevTF.forOtherAdjacentTaskFragments(prevTFAdjacent -> {
final ActivityRecord prevActivityAdjacent =
prevTFAdjacent.getTopNonFinishingActivity();
@@ -577,14 +563,6 @@ class BackNavigationController {
if (mainTF == null || !mainTF.hasAdjacentTaskFragment()) {
return;
}
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment adjacentTF = mainTF.getAdjacentTaskFragment();
- final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity();
- if (topActivity != null) {
- outList.add(topActivity);
- }
- return;
- }
mainTF.forOtherAdjacentTaskFragments(adjacentTF -> {
final ActivityRecord topActivity = adjacentTF.getTopNonFinishingActivity();
if (topActivity != null) {
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 2287a687700c..f50a68cc5389 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -49,8 +49,8 @@ import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskS
import static com.android.window.flags.Flags.balImprovedMetrics;
import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator;
import static com.android.window.flags.Flags.balShowToastsBlocked;
-import static com.android.window.flags.Flags.balStrictModeRo;
import static com.android.window.flags.Flags.balStrictModeGracePeriod;
+import static com.android.window.flags.Flags.balStrictModeRo;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import static java.util.Objects.requireNonNull;
@@ -91,7 +91,6 @@ import com.android.internal.util.Preconditions;
import com.android.server.UiThread;
import com.android.server.am.PendingIntentRecord;
import com.android.server.wm.BackgroundLaunchProcessController.BalCheckConfiguration;
-import com.android.window.flags.Flags;
import java.lang.annotation.Retention;
import java.util.ArrayList;
@@ -1687,14 +1686,6 @@ public class BackgroundActivityStartController {
}
// Check the adjacent fragment.
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment();
- topActivity = adjacentTaskFragment.getActivity(topOfStackPredicate);
- if (topActivity == null) {
- return bas;
- }
- return checkCrossUidActivitySwitchFromBelow(topActivity, uid, bas);
- }
final BlockActivityStart[] out = { bas };
taskFragment.forOtherAdjacentTaskFragments(adjacentTaskFragment -> {
final ActivityRecord top = adjacentTaskFragment.getActivity(topOfStackPredicate);
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index f473b7b7e4fb..fcc697242ff6 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -18,7 +18,7 @@ package com.android.server.wm;
import static android.view.WindowManager.TRANSIT_CHANGE;
-import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS;
+import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS_MIN;
import static com.android.server.wm.ActivityTaskManagerService.POWER_MODE_REASON_CHANGE_DISPLAY;
import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELDS;
import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields;
@@ -140,8 +140,9 @@ class DeferredDisplayUpdater {
if (displayInfoDiff == DIFF_EVERYTHING
|| !mDisplayContent.getLastHasContent()
|| !mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
- ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
- "DeferredDisplayUpdater: applying DisplayInfo immediately");
+ ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+ "DeferredDisplayUpdater: applying DisplayInfo(%d x %d) immediately",
+ displayInfo.logicalWidth, displayInfo.logicalHeight);
mLastWmDisplayInfo = displayInfo;
applyLatestDisplayInfo();
@@ -151,17 +152,23 @@ class DeferredDisplayUpdater {
// If there are non WM-specific display info changes, apply only these fields immediately
if ((displayInfoDiff & DIFF_NOT_WM_DEFERRABLE) > 0) {
- ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
- "DeferredDisplayUpdater: partially applying DisplayInfo immediately");
+ ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+ "DeferredDisplayUpdater: partially applying DisplayInfo(%d x %d) immediately",
+ displayInfo.logicalWidth, displayInfo.logicalHeight);
applyLatestDisplayInfo();
}
// If there are WM-specific display info changes, apply them through a Shell transition
if ((displayInfoDiff & DIFF_WM_DEFERRABLE) > 0) {
- ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
- "DeferredDisplayUpdater: deferring DisplayInfo update");
+ ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+ "DeferredDisplayUpdater: deferring DisplayInfo(%d x %d) update",
+ displayInfo.logicalWidth, displayInfo.logicalHeight);
requestDisplayChangeTransition(physicalDisplayUpdated, () -> {
+ ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS_MIN,
+ "DeferredDisplayUpdater: applying DisplayInfo(%d x %d) after deferring",
+ displayInfo.logicalWidth, displayInfo.logicalHeight);
+
// Apply deferrable fields to DisplayContent only when the transition
// starts collecting, non-deferrable fields are ignored in mLastWmDisplayInfo
mLastWmDisplayInfo = displayInfo;
@@ -199,7 +206,7 @@ class DeferredDisplayUpdater {
mDisplayContent.getDisplayPolicy().getNotificationShade();
if (notificationShade != null && notificationShade.isVisible()
&& mDisplayContent.mAtmService.mKeyguardController.isKeyguardOrAodShowing(
- mDisplayContent.mDisplayId)) {
+ mDisplayContent.mDisplayId)) {
Slog.i(TAG, notificationShade + " uses blast for display switch");
notificationShade.mSyncMethodOverride = BLASTSyncEngine.METHOD_BLAST;
}
@@ -209,9 +216,6 @@ class DeferredDisplayUpdater {
try {
onStartCollect.run();
- ProtoLog.d(WM_DEBUG_WINDOW_TRANSITIONS,
- "DeferredDisplayUpdater: applied DisplayInfo after deferring");
-
if (physicalDisplayUpdated) {
onDisplayUpdated(transition, fromRotation, startBounds);
} else {
diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java
index dc42b32967e2..d91fca9e2816 100644
--- a/services/core/java/com/android/server/wm/DesktopModeHelper.java
+++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java
@@ -17,6 +17,7 @@
package com.android.server.wm;
import static android.app.Flags.enableConnectedDisplaysWallpaper;
+import static android.window.DesktopExperienceFlags.ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE;
import android.annotation.NonNull;
import android.content.Context;
@@ -66,7 +67,7 @@ public final class DesktopModeHelper {
* Return {@code true} if the current device can hosts desktop sessions on its internal display.
*/
@VisibleForTesting
- static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
+ private static boolean canInternalDisplayHostDesktops(@NonNull Context context) {
return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops);
}
@@ -83,8 +84,11 @@ public final class DesktopModeHelper {
if (!shouldEnforceDeviceRestrictions()) {
return true;
}
- final boolean desktopModeSupported = isDesktopModeSupported(context)
- && canInternalDisplayHostDesktops(context);
+ // If projected display is enabled, #canInternalDisplayHostDesktops is no longer a
+ // requirement.
+ final boolean desktopModeSupported = ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE.isTrue()
+ ? isDesktopModeSupported(context) : (isDesktopModeSupported(context)
+ && canInternalDisplayHostDesktops(context));
final boolean desktopModeSupportedByDevOptions =
Flags.enableDesktopModeThroughDevOption()
&& isDesktopModeDevOptionsSupported(context);
diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
index d466a646c7dd..ddcb5eccb1d8 100644
--- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java
@@ -19,6 +19,12 @@ package com.android.server.wm;
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.content.Intent.FLAG_ACTIVITY_CLEAR_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
+import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK;
+import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -131,6 +137,18 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
return RESULT_SKIP;
}
+ if (DesktopModeFlags.INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES.isTrue()) {
+ ActivityRecord topVisibleFreeformActivity =
+ task.getDisplayContent().getTopMostVisibleFreeformActivity();
+ if (shouldInheritExistingTaskBounds(topVisibleFreeformActivity, activity, task)) {
+ appendLog("inheriting bounds from existing closing instance");
+ outParams.mBounds.set(topVisibleFreeformActivity.getBounds());
+ appendLog("final desktop mode task bounds set to %s", outParams.mBounds);
+ // Return result done to prevent other modifiers from changing or cascading bounds.
+ return RESULT_DONE;
+ }
+ }
+
DesktopModeBoundsCalculator.updateInitialBounds(task, layout, activity, options,
outParams.mBounds, this::appendLog);
appendLog("final desktop mode task bounds set to %s", outParams.mBounds);
@@ -159,7 +177,7 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
// activity will also enter desktop mode. On this same relationship, we can also assume
// if there are not visible freeform tasks but a freeform activity is now launching, it
// will force the device into desktop mode.
- return (task.getDisplayContent().getTopMostVisibleFreeformActivity() != null
+ return (task.getDisplayContent().getTopMostFreeformActivity() != null
&& checkSourceWindowModesCompatible(task, options, currentParams))
|| isRequestingFreeformWindowMode(task, options, currentParams);
}
@@ -201,6 +219,40 @@ class DesktopModeLaunchParamsModifier implements LaunchParamsModifier {
};
}
+ /**
+ * Whether the launching task should inherit the task bounds of an existing closing instance.
+ */
+ private boolean shouldInheritExistingTaskBounds(
+ @Nullable ActivityRecord existingTaskActivity,
+ @Nullable ActivityRecord launchingActivity,
+ @NonNull Task launchingTask) {
+ if (existingTaskActivity == null || launchingActivity == null) return false;
+ return (existingTaskActivity.packageName == launchingActivity.packageName)
+ && isLaunchingNewTask(launchingActivity.launchMode,
+ launchingTask.getBaseIntent().getFlags())
+ && isClosingExitingInstance(launchingTask.getBaseIntent().getFlags());
+ }
+
+ /**
+ * Returns true if the launch mode or intent will result in a new task being created for the
+ * activity.
+ */
+ private boolean isLaunchingNewTask(int launchMode, int intentFlags) {
+ return launchMode == LAUNCH_SINGLE_TASK
+ || launchMode == LAUNCH_SINGLE_INSTANCE
+ || launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK
+ || (intentFlags & FLAG_ACTIVITY_NEW_TASK) != 0;
+ }
+
+ /**
+ * Returns true if the intent will result in an existing task instance being closed if a new
+ * one appears.
+ */
+ private boolean isClosingExitingInstance(int intentFlags) {
+ return (intentFlags & FLAG_ACTIVITY_CLEAR_TASK) != 0
+ || (intentFlags & FLAG_ACTIVITY_MULTIPLE_TASK) == 0;
+ }
+
private void initLogBuilder(Task task, ActivityRecord activity) {
if (DEBUG) {
mLogBuilder = new StringBuilder(
diff --git a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
index a017a1173d97..e508a6d23178 100644
--- a/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
+++ b/services/core/java/com/android/server/wm/EnsureActivitiesVisibleHelper.java
@@ -23,8 +23,6 @@ import static com.android.server.wm.Task.TAG_VISIBILITY;
import android.annotation.Nullable;
import android.util.Slog;
-import com.android.window.flags.Flags;
-
import java.util.ArrayList;
/** Helper class to ensure activities are in the right visible state for a container. */
@@ -112,18 +110,11 @@ class EnsureActivitiesVisibleHelper {
if (adjacentTaskFragments != null && adjacentTaskFragments.contains(
childTaskFragment)) {
- final boolean isTranslucent;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- isTranslucent = childTaskFragment.isTranslucent(starting)
- || childTaskFragment.forOtherAdjacentTaskFragments(
- adjacentTaskFragment -> {
- return adjacentTaskFragment.isTranslucent(starting);
- });
- } else {
- isTranslucent = childTaskFragment.isTranslucent(starting)
- || childTaskFragment.getAdjacentTaskFragment()
- .isTranslucent(starting);
- }
+ final boolean isTranslucent = childTaskFragment.isTranslucent(starting)
+ || childTaskFragment.forOtherAdjacentTaskFragments(
+ adjacentTaskFragment -> {
+ return adjacentTaskFragment.isTranslucent(starting);
+ });
if (!isTranslucent) {
// Everything behind two adjacent TaskFragments are occluded.
mBehindFullyOccludedContainer = true;
@@ -135,14 +126,10 @@ class EnsureActivitiesVisibleHelper {
if (adjacentTaskFragments == null) {
adjacentTaskFragments = new ArrayList<>();
}
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final ArrayList<TaskFragment> adjacentTfs = adjacentTaskFragments;
- childTaskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
- adjacentTfs.add(adjacentTf);
- });
- } else {
- adjacentTaskFragments.add(childTaskFragment.getAdjacentTaskFragment());
- }
+ final ArrayList<TaskFragment> adjacentTfs = adjacentTaskFragments;
+ childTaskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
+ adjacentTfs.add(adjacentTf);
+ });
}
} else if (child.asActivityRecord() != null) {
setActivityVisibilityState(child.asActivityRecord(), starting, resumeTopActivity);
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 2cac63c1e5e9..a937691e7998 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -263,8 +263,8 @@ final class ImeInsetsSourceProvider extends InsetsSourceProvider {
boolean oldVisibility = mSource.isVisible();
super.updateVisibility();
if (Flags.refactorInsetsController()) {
- if (mSource.isVisible() && !oldVisibility && mImeRequester != null) {
- reportImeDrawnForOrganizerIfNeeded(mImeRequester);
+ if (mSource.isVisible() && !oldVisibility && mControlTarget != null) {
+ reportImeDrawnForOrganizerIfNeeded(mControlTarget);
}
}
onSourceChanged();
diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java
index 7751ac3f9fc6..a4bc5cbcb5d3 100644
--- a/services/core/java/com/android/server/wm/InputManagerCallback.java
+++ b/services/core/java/com/android/server/wm/InputManagerCallback.java
@@ -343,6 +343,13 @@ final class InputManagerCallback implements InputManagerService.WindowManagerCal
}
}
+ @Override
+ public boolean isKeyguardLocked(int displayId) {
+ synchronized (mService.mGlobalLock) {
+ return mService.mAtmService.mKeyguardController.isKeyguardLocked(displayId);
+ }
+ }
+
/** Waits until the built-in input devices have been configured. */
public boolean waitForInputDevicesReady(long timeoutMillis) {
synchronized (mInputDevicesReadyMonitor) {
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index cf201c9f34f0..609302ce3f56 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -1732,26 +1732,14 @@ class RootWindowContainer extends WindowContainer<DisplayContent>
activityAssistInfos.clear();
activityAssistInfos.add(new ActivityAssistInfo(top));
// Check if the activity on the split screen.
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- top.getTask().forOtherAdjacentTasks(task -> {
- final ActivityRecord adjacentActivityRecord =
- task.getTopNonFinishingActivity();
- if (adjacentActivityRecord != null) {
- activityAssistInfos.add(
- new ActivityAssistInfo(adjacentActivityRecord));
- }
- });
- } else {
- final Task adjacentTask = top.getTask().getAdjacentTask();
- if (adjacentTask != null) {
- final ActivityRecord adjacentActivityRecord =
- adjacentTask.getTopNonFinishingActivity();
- if (adjacentActivityRecord != null) {
- activityAssistInfos.add(
- new ActivityAssistInfo(adjacentActivityRecord));
- }
+ top.getTask().forOtherAdjacentTasks(task -> {
+ final ActivityRecord adjacentActivityRecord =
+ task.getTopNonFinishingActivity();
+ if (adjacentActivityRecord != null) {
+ activityAssistInfos.add(
+ new ActivityAssistInfo(adjacentActivityRecord));
}
- }
+ });
if (rootTask == topFocusedRootTask) {
topVisibleActivities.addAll(0, activityAssistInfos);
} else {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index d16c301cec40..e98b2b749af8 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -2461,21 +2461,6 @@ class Task extends TaskFragment {
return parentTask == null ? null : parentTask.getCreatedByOrganizerTask();
}
- /** @deprecated b/373709676 replace with {@link #forOtherAdjacentTasks(Consumer)} ()}. */
- @Deprecated
- @Nullable
- Task getAdjacentTask() {
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments is enabled. "
- + "Use #forOtherAdjacentTasks instead");
- }
- final Task taskWithAdjacent = getTaskWithAdjacent();
- if (taskWithAdjacent == null) {
- return null;
- }
- return taskWithAdjacent.getAdjacentTaskFragment().asTask();
- }
-
/** Finds the first Task parent (or itself) that has adjacent. */
@Nullable
Task getTaskWithAdjacent() {
@@ -2499,11 +2484,6 @@ class Task extends TaskFragment {
* Tasks. The invoke order is not guaranteed.
*/
void forOtherAdjacentTasks(@NonNull Consumer<Task> callback) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments is not enabled. "
- + "Use #getAdjacentTask instead");
- }
-
final Task taskWithAdjacent = getTaskWithAdjacent();
if (taskWithAdjacent == null) {
return;
@@ -2521,10 +2501,6 @@ class Task extends TaskFragment {
* guaranteed.
*/
boolean forOtherAdjacentTasks(@NonNull Predicate<Task> callback) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments is not enabled. "
- + "Use getAdjacentTask instead");
- }
final Task taskWithAdjacent = getTaskWithAdjacent();
if (taskWithAdjacent == null) {
return false;
@@ -3651,20 +3627,13 @@ class Task extends TaskFragment {
final TaskFragment taskFragment = wc.asTaskFragment();
if (taskFragment != null && taskFragment.isEmbedded()
&& taskFragment.hasAdjacentTaskFragment()) {
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final int[] nextLayer = { layer };
- taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
- if (adjacentTf.shouldBoostDimmer()) {
- adjacentTf.assignLayer(t, nextLayer[0]++);
- }
- });
- layer = nextLayer[0];
- } else {
- final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment();
+ final int[] nextLayer = { layer };
+ taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
if (adjacentTf.shouldBoostDimmer()) {
- adjacentTf.assignLayer(t, layer++);
+ adjacentTf.assignLayer(t, nextLayer[0]++);
}
- }
+ });
+ layer = nextLayer[0];
}
// Place the decor surface just above the owner TaskFragment.
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 1966ecf57c73..fb7bab4b3e26 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -60,7 +60,6 @@ import com.android.internal.util.function.pooled.PooledLambda;
import com.android.internal.util.function.pooled.PooledPredicate;
import com.android.server.pm.UserManagerInternal;
import com.android.server.wm.LaunchParamsController.LaunchParams;
-import com.android.window.flags.Flags;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -1089,19 +1088,14 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> {
// Use launch-adjacent-flag-root if launching with launch-adjacent flag.
if ((launchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0
&& mLaunchAdjacentFlagRootTask != null) {
- final Task launchAdjacentRootAdjacentTask;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final Task[] tmpTask = new Task[1];
- mLaunchAdjacentFlagRootTask.forOtherAdjacentTasks(task -> {
- // TODO(b/382208145): enable FLAG_ACTIVITY_LAUNCH_ADJACENT for 3+.
- // Find the first adjacent for now.
- tmpTask[0] = task;
- return true;
- });
- launchAdjacentRootAdjacentTask = tmpTask[0];
- } else {
- launchAdjacentRootAdjacentTask = mLaunchAdjacentFlagRootTask.getAdjacentTask();
- }
+ final Task[] tmpTask = new Task[1];
+ mLaunchAdjacentFlagRootTask.forOtherAdjacentTasks(task -> {
+ // TODO(b/382208145): enable FLAG_ACTIVITY_LAUNCH_ADJACENT for 3+.
+ // Find the first adjacent for now.
+ tmpTask[0] = task;
+ return true;
+ });
+ final Task launchAdjacentRootAdjacentTask = tmpTask[0];
if (sourceTask != null && (sourceTask == candidateTask
|| sourceTask.topRunningActivity() == null)) {
// Do nothing when task that is getting opened is same as the source or when
@@ -1129,14 +1123,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> {
if (launchRootTask == null || sourceTask == null) {
return launchRootTask;
}
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final Task adjacentRootTask = launchRootTask.getAdjacentTask();
- if (adjacentRootTask != null && (sourceTask == adjacentRootTask
- || sourceTask.isDescendantOf(adjacentRootTask))) {
- return adjacentRootTask;
- }
- return launchRootTask;
- }
final Task[] adjacentRootTask = new Task[1];
launchRootTask.forOtherAdjacentTasks(task -> {
if (sourceTask == task || sourceTask.isDescendantOf(task)) {
@@ -1163,24 +1149,16 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> {
return sourceTask.getCreatedByOrganizerTask();
}
// Check if the candidate is already positioned in the adjacent Task.
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final Task[] adjacentRootTask = new Task[1];
- sourceTask.forOtherAdjacentTasks(task -> {
- if (candidateTask == task || candidateTask.isDescendantOf(task)) {
- adjacentRootTask[0] = task;
- return true;
- }
- return false;
- });
- if (adjacentRootTask[0] != null) {
- return adjacentRootTask[0];
- }
- } else {
- final Task adjacentTarget = taskWithAdjacent.getAdjacentTask();
- if (candidateTask == adjacentTarget
- || candidateTask.isDescendantOf(adjacentTarget)) {
- return adjacentTarget;
+ final Task[] adjacentRootTask = new Task[1];
+ sourceTask.forOtherAdjacentTasks(task -> {
+ if (candidateTask == task || candidateTask.isDescendantOf(task)) {
+ adjacentRootTask[0] = task;
+ return true;
}
+ return false;
+ });
+ if (adjacentRootTask[0] != null) {
+ return adjacentRootTask[0];
}
return sourceTask.getCreatedByOrganizerTask();
}
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 5183c6b57f15..2dabb253744a 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -235,11 +235,6 @@ class TaskFragment extends WindowContainer<WindowContainer> {
/** This task fragment will be removed when the cleanup of its children are done. */
private boolean mIsRemovalRequested;
- /** @deprecated b/373709676 replace with {@link #mAdjacentTaskFragments} */
- @Deprecated
- @Nullable
- private TaskFragment mAdjacentTaskFragment;
-
/**
* The TaskFragments that are adjacent to each other, including this TaskFragment.
* All TaskFragments in this set share the same set instance.
@@ -455,22 +450,6 @@ class TaskFragment extends WindowContainer<WindowContainer> {
return service.mWindowOrganizerController.getTaskFragment(token);
}
- /** @deprecated b/373709676 replace with {@link #setAdjacentTaskFragments}. */
- @Deprecated
- void setAdjacentTaskFragment(@NonNull TaskFragment taskFragment) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- if (mAdjacentTaskFragment == taskFragment) {
- return;
- }
- resetAdjacentTaskFragment();
- mAdjacentTaskFragment = taskFragment;
- taskFragment.setAdjacentTaskFragment(this);
- return;
- }
-
- setAdjacentTaskFragments(new AdjacentSet(this, taskFragment));
- }
-
void setAdjacentTaskFragments(@NonNull AdjacentSet adjacentTaskFragments) {
adjacentTaskFragments.setAsAdjacent();
}
@@ -483,56 +462,18 @@ class TaskFragment extends WindowContainer<WindowContainer> {
return mCompanionTaskFragment;
}
- /** @deprecated b/373709676 replace with {@link #clearAdjacentTaskFragments()}. */
- @Deprecated
- private void resetAdjacentTaskFragment() {
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("resetAdjacentTaskFragment shouldn't be called when"
- + " allowMultipleAdjacentTaskFragments is enabled. Use either"
- + " #clearAdjacentTaskFragments or #removeFromAdjacentTaskFragments");
- }
- // Reset the adjacent TaskFragment if its adjacent TaskFragment is also this TaskFragment.
- if (mAdjacentTaskFragment != null && mAdjacentTaskFragment.mAdjacentTaskFragment == this) {
- mAdjacentTaskFragment.mAdjacentTaskFragment = null;
- mAdjacentTaskFragment.mDelayLastActivityRemoval = false;
- }
- mAdjacentTaskFragment = null;
- mDelayLastActivityRemoval = false;
- }
-
void clearAdjacentTaskFragments() {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- resetAdjacentTaskFragment();
- return;
- }
-
if (mAdjacentTaskFragments != null) {
mAdjacentTaskFragments.clear();
}
}
void removeFromAdjacentTaskFragments() {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- resetAdjacentTaskFragment();
- return;
- }
-
if (mAdjacentTaskFragments != null) {
mAdjacentTaskFragments.remove(this);
}
}
- /** @deprecated b/373709676 replace with {@link #getAdjacentTaskFragments()}. */
- @Deprecated
- @Nullable
- TaskFragment getAdjacentTaskFragment() {
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments is enabled. "
- + "Use #getAdjacentTaskFragments instead");
- }
- return mAdjacentTaskFragment;
- }
-
@Nullable
AdjacentSet getAdjacentTaskFragments() {
return mAdjacentTaskFragments;
@@ -561,16 +502,10 @@ class TaskFragment extends WindowContainer<WindowContainer> {
}
boolean hasAdjacentTaskFragment() {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- return mAdjacentTaskFragment != null;
- }
return mAdjacentTaskFragments != null;
}
boolean isAdjacentTo(@NonNull TaskFragment other) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- return mAdjacentTaskFragment == other;
- }
return other != this
&& mAdjacentTaskFragments != null
&& mAdjacentTaskFragments.contains(other);
@@ -1377,21 +1312,12 @@ class TaskFragment extends WindowContainer<WindowContainer> {
if (taskFragment.isAdjacentTo(this)) {
continue;
}
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final boolean isOccluding = mTmpRect.intersect(taskFragment.getBounds())
- || taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
- return mTmpRect.intersect(adjacentTf.getBounds());
- });
- if (isOccluding) {
- return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
- }
- } else {
- final TaskFragment adjacentTaskFragment =
- taskFragment.mAdjacentTaskFragment;
- if (mTmpRect.intersect(taskFragment.getBounds())
- || mTmpRect.intersect(adjacentTaskFragment.getBounds())) {
- return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
- }
+ final boolean isOccluding = mTmpRect.intersect(taskFragment.getBounds())
+ || taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> {
+ return mTmpRect.intersect(adjacentTf.getBounds());
+ });
+ if (isOccluding) {
+ return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
}
}
}
@@ -1427,37 +1353,22 @@ class TaskFragment extends WindowContainer<WindowContainer> {
// 2. Adjacent TaskFragments do not overlap, so that if this TaskFragment is behind
// any translucent TaskFragment in the adjacent set, then this TaskFragment is
// visible behind translucent.
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- final boolean hasTraversedAdj = otherTaskFrag.forOtherAdjacentTaskFragments(
- adjacentTaskFragments::contains);
- if (hasTraversedAdj) {
- final boolean isTranslucent =
- isBehindTransparentTaskFragment(otherTaskFrag, starting)
- || otherTaskFrag.forOtherAdjacentTaskFragments(
- (Predicate<TaskFragment>) tf ->
- isBehindTransparentTaskFragment(tf, starting));
- if (isTranslucent) {
- // Can be visible behind a translucent adjacent TaskFragments.
- gotTranslucentFullscreen = true;
- gotTranslucentAdjacent = true;
- continue;
- }
- // Can not be visible behind adjacent TaskFragments.
- return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
- }
- } else {
- if (adjacentTaskFragments.contains(otherTaskFrag.mAdjacentTaskFragment)) {
- if (isBehindTransparentTaskFragment(otherTaskFrag, starting)
- || isBehindTransparentTaskFragment(
- otherTaskFrag.mAdjacentTaskFragment, starting)) {
- // Can be visible behind a translucent adjacent TaskFragments.
- gotTranslucentFullscreen = true;
- gotTranslucentAdjacent = true;
- continue;
- }
- // Can not be visible behind adjacent TaskFragments.
- return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
+ final boolean hasTraversedAdj = otherTaskFrag.forOtherAdjacentTaskFragments(
+ adjacentTaskFragments::contains);
+ if (hasTraversedAdj) {
+ final boolean isTranslucent =
+ isBehindTransparentTaskFragment(otherTaskFrag, starting)
+ || otherTaskFrag.forOtherAdjacentTaskFragments(
+ (Predicate<TaskFragment>) tf ->
+ isBehindTransparentTaskFragment(tf, starting));
+ if (isTranslucent) {
+ // Can be visible behind a translucent adjacent TaskFragments.
+ gotTranslucentFullscreen = true;
+ gotTranslucentAdjacent = true;
+ continue;
}
+ // Can not be visible behind adjacent TaskFragments.
+ return TASK_FRAGMENT_VISIBILITY_INVISIBLE;
}
adjacentTaskFragments.add(otherTaskFrag);
}
@@ -3299,40 +3210,23 @@ class TaskFragment extends WindowContainer<WindowContainer> {
final ArrayList<WindowContainer> siblings = getParent().mChildren;
final int zOrder = siblings.indexOf(this);
-
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- if (siblings.indexOf(getAdjacentTaskFragment()) < zOrder) {
- // early return if this TF already has higher z-ordering.
- return false;
- }
- } else {
- final boolean hasAdjacentOnTop = forOtherAdjacentTaskFragments(
- tf -> siblings.indexOf(tf) > zOrder);
- if (!hasAdjacentOnTop) {
- // early return if this TF already has higher z-ordering.
- return false;
- }
+ final boolean hasAdjacentOnTop = forOtherAdjacentTaskFragments(
+ tf -> siblings.indexOf(tf) > zOrder);
+ if (!hasAdjacentOnTop) {
+ // early return if this TF already has higher z-ordering.
+ return false;
}
final ToBooleanFunction<WindowState> getDimBehindWindow =
(w) -> (w.mAttrs.flags & FLAG_DIM_BEHIND) != 0 && w.mActivityRecord != null
&& w.mActivityRecord.isEmbedded() && (w.mActivityRecord.isVisibleRequested()
|| w.mActivityRecord.isVisible());
-
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment adjacentTf = getAdjacentTaskFragment();
- if (adjacentTf.forAllWindows(getDimBehindWindow, true)) {
- // early return if the adjacent Tf has a dimming window.
- return false;
- }
- } else {
- final boolean adjacentHasDimmingWindow = forOtherAdjacentTaskFragments(tf -> {
- return tf.forAllWindows(getDimBehindWindow, true);
- });
- if (adjacentHasDimmingWindow) {
- // early return if the adjacent Tf has a dimming window.
- return false;
- }
+ final boolean adjacentHasDimmingWindow = forOtherAdjacentTaskFragments(tf -> {
+ return tf.forAllWindows(getDimBehindWindow, true);
+ });
+ if (adjacentHasDimmingWindow) {
+ // early return if the adjacent Tf has a dimming window.
+ return false;
}
// boost if there's an Activity window that has FLAG_DIM_BEHIND flag.
@@ -3456,16 +3350,9 @@ class TaskFragment extends WindowContainer<WindowContainer> {
sb.append(" organizerProc=");
sb.append(mTaskFragmentOrganizerProcessName);
}
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- if (mAdjacentTaskFragments != null) {
- sb.append(" adjacent=");
- sb.append(mAdjacentTaskFragments);
- }
- } else {
- if (mAdjacentTaskFragment != null) {
- sb.append(" adjacent=");
- sb.append(mAdjacentTaskFragment);
- }
+ if (mAdjacentTaskFragments != null) {
+ sb.append(" adjacent=");
+ sb.append(mAdjacentTaskFragments);
}
sb.append('}');
return sb.toString();
@@ -3591,10 +3478,6 @@ class TaskFragment extends WindowContainer<WindowContainer> {
}
AdjacentSet(@NonNull ArraySet<TaskFragment> taskFragments) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- throw new IllegalStateException("allowMultipleAdjacentTaskFragments must be"
- + " enabled to set more than two TaskFragments adjacent to each other.");
- }
final int size = taskFragments.size();
if (size < 2) {
throw new IllegalArgumentException("Adjacent TaskFragments must contain at least"
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index c78cdaa10df2..803c21ccab6e 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -2589,9 +2589,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener {
}
// When the TaskFragment has an adjacent TaskFragment, sibling behind them should be
// hidden unless any of them are translucent.
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- return taskFragment.getAdjacentTaskFragment().isTranslucentForTransition();
- }
return taskFragment.forOtherAdjacentTaskFragments(TaskFragment::isTranslucentForTransition);
}
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 3a4d9d27f65a..e1553cd37d03 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -57,7 +57,8 @@ import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.util.ToBooleanFunction;
-import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
+import com.android.server.wallpaper.WallpaperCropper;
+import com.android.server.wallpaper.WallpaperDefaultDisplayInfo;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -71,7 +72,6 @@ import java.util.function.Consumer;
class WallpaperController {
private static final String TAG = TAG_WITH_CLASS_NAME ? "WallpaperController" : TAG_WM;
private WindowManagerService mService;
- private WallpaperCropUtils mWallpaperCropUtils = null;
private DisplayContent mDisplayContent;
// Larger index has higher z-order.
@@ -116,6 +116,10 @@ class WallpaperController {
private boolean mShouldOffsetWallpaperCenter;
+ // This is for WallpaperCropper, which has cropping logic for the default display only.
+ // TODO(b/400685784) make the WallpaperCropper operate on every display independently
+ private final WallpaperDefaultDisplayInfo mDefaultDisplayInfo;
+
private final ToBooleanFunction<WindowState> mFindWallpaperTargetFunction = w -> {
final ActivityRecord ar = w.mActivityRecord;
// The animating window can still be visible on screen if it is in transition, so we
@@ -198,12 +202,14 @@ class WallpaperController {
WallpaperController(WindowManagerService service, DisplayContent displayContent) {
mService = service;
mDisplayContent = displayContent;
+ WindowManager windowManager = service.mContext.getSystemService(WindowManager.class);
Resources resources = service.mContext.getResources();
mMinWallpaperScale =
resources.getFloat(com.android.internal.R.dimen.config_wallpaperMinScale);
mMaxWallpaperScale = resources.getFloat(R.dimen.config_wallpaperMaxScale);
mShouldOffsetWallpaperCenter = resources.getBoolean(
com.android.internal.R.bool.config_offsetWallpaperToCenterOfLargestDisplay);
+ mDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(windowManager, resources);
}
void resetLargestDisplay(Display display) {
@@ -246,10 +252,6 @@ class WallpaperController {
return largestDisplaySize;
}
- void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) {
- mWallpaperCropUtils = wallpaperCropUtils;
- }
-
WindowState getWallpaperTarget() {
return mWallpaperTarget;
}
@@ -352,16 +354,12 @@ class WallpaperController {
int offsetY;
if (multiCrop()) {
- if (mWallpaperCropUtils == null) {
- Slog.e(TAG, "Update wallpaper offsets before the system is ready. Aborting");
- return false;
- }
Point bitmapSize = new Point(
wallpaperWin.mRequestedWidth, wallpaperWin.mRequestedHeight);
SparseArray<Rect> cropHints = token.getCropHints();
wallpaperFrame = bitmapSize.x <= 0 || bitmapSize.y <= 0 ? wallpaperWin.getFrame()
- : mWallpaperCropUtils.getCrop(screenSize, bitmapSize, cropHints,
- wallpaperWin.isRtl());
+ : WallpaperCropper.getCrop(screenSize, mDefaultDisplayInfo, bitmapSize,
+ cropHints, wallpaperWin.isRtl());
int frameWidth = wallpaperFrame.width();
int frameHeight = wallpaperFrame.height();
float frameRatio = (float) frameWidth / frameHeight;
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 466ed7863c84..772a7fdfc684 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -2006,11 +2006,16 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
return getActivity(r -> !r.finishing, true /* traverseTopToBottom */);
}
- ActivityRecord getTopMostVisibleFreeformActivity() {
+ ActivityRecord getTopMostFreeformActivity() {
return getActivity(r -> r.isVisibleRequested() && r.inFreeformWindowingMode(),
true /* traverseTopToBottom */);
}
+ ActivityRecord getTopMostVisibleFreeformActivity() {
+ return getActivity(r -> r.isVisible() && r.inFreeformWindowingMode(),
+ true /* traverseTopToBottom */);
+ }
+
ActivityRecord getTopActivity(boolean includeFinishing, boolean includeOverlays) {
// Break down into 4 calls to avoid object creation due to capturing input params.
if (includeFinishing) {
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index 4b5a3a031931..5f2a2ad7f0eb 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -54,7 +54,6 @@ import android.window.ScreenCapture.ScreenshotHardwareBuffer;
import com.android.internal.policy.KeyInterceptionInfo;
import com.android.server.input.InputManagerService;
import com.android.server.policy.WindowManagerPolicy;
-import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
import com.android.server.wm.SensitiveContentPackages.PackageInfo;
import java.lang.annotation.Retention;
@@ -772,12 +771,6 @@ public abstract class WindowManagerInternal {
public abstract void setWallpaperCropHints(IBinder windowToken, SparseArray<Rect> cropHints);
/**
- * Transmits the {@link WallpaperCropUtils} instance to {@link WallpaperController}.
- * {@link WallpaperCropUtils} contains the helpers to properly position the wallpaper.
- */
- public abstract void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils);
-
- /**
* Returns {@code true} if a Window owned by {@code uid} has focus.
*/
public abstract boolean isUidFocused(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 9fc0339c52a2..c078d67b6cc6 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -356,7 +356,6 @@ import com.android.server.policy.WindowManagerPolicy;
import com.android.server.policy.WindowManagerPolicy.ScreenOffListener;
import com.android.server.power.ShutdownThread;
import com.android.server.utils.PriorityDump;
-import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
import com.android.window.flags.Flags;
import dalvik.annotation.optimization.NeverCompile;
@@ -8100,12 +8099,6 @@ public class WindowManagerService extends IWindowManager.Stub
}
@Override
- public void setWallpaperCropUtils(WallpaperCropUtils wallpaperCropUtils) {
- mRoot.getDisplayContent(DEFAULT_DISPLAY).mWallpaperController
- .setWallpaperCropUtils(wallpaperCropUtils);
- }
-
- @Override
public boolean isUidFocused(int uid) {
synchronized (mGlobalLock) {
for (int i = mRoot.getChildCount() - 1; i >= 0; i--) {
@@ -9374,23 +9367,6 @@ public class WindowManagerService extends IWindowManager.Stub
return focusedActivity;
}
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment();
- final ActivityRecord adjacentTopActivity = adjacentTaskFragment.topRunningActivity();
- if (adjacentTopActivity == null) {
- // Return if no adjacent activity.
- return focusedActivity;
- }
-
- if (adjacentTopActivity.getLastWindowCreateTime()
- < focusedActivity.getLastWindowCreateTime()) {
- // Return if the current focus activity has more recently active window.
- return focusedActivity;
- }
-
- return adjacentTopActivity;
- }
-
// Find the adjacent activity with more recently active window.
final ActivityRecord[] mostRecentActiveActivity = { focusedActivity };
final long[] mostRecentActiveTime = { focusedActivity.getLastWindowCreateTime() };
@@ -9461,20 +9437,15 @@ public class WindowManagerService extends IWindowManager.Stub
// No adjacent window.
return false;
}
- final TaskFragment adjacentFragment;
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- if (fromFragment.getAdjacentTaskFragments().size() > 2) {
- throw new IllegalStateException("Not yet support 3+ adjacent for non-Task TFs");
- }
- final TaskFragment[] tmpAdjacent = new TaskFragment[1];
- fromFragment.forOtherAdjacentTaskFragments(adjacentTF -> {
- tmpAdjacent[0] = adjacentTF;
- return true;
- });
- adjacentFragment = tmpAdjacent[0];
- } else {
- adjacentFragment = fromFragment.getAdjacentTaskFragment();
+ if (fromFragment.getAdjacentTaskFragments().size() > 2) {
+ throw new IllegalStateException("Not yet support 3+ adjacent for non-Task TFs");
}
+ final TaskFragment[] tmpAdjacent = new TaskFragment[1];
+ fromFragment.forOtherAdjacentTaskFragments(adjacentTF -> {
+ tmpAdjacent[0] = adjacentTF;
+ return true;
+ });
+ final TaskFragment adjacentFragment = tmpAdjacent[0];
if (adjacentFragment.isIsolatedNav()) {
// Don't move the focus if the adjacent TF is isolated navigation.
return false;
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index ea1f35a130b0..a012ec137892 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1671,13 +1671,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
}
if (!taskFragment.isAdjacentTo(secondaryTaskFragment)) {
// Only have lifecycle effect if the adjacent changed.
- if (Flags.allowMultipleAdjacentTaskFragments()) {
- // Activity Embedding only set two TFs adjacent.
- taskFragment.setAdjacentTaskFragments(
- new TaskFragment.AdjacentSet(taskFragment, secondaryTaskFragment));
- } else {
- taskFragment.setAdjacentTaskFragment(secondaryTaskFragment);
- }
+ // Activity Embedding only set two TFs adjacent.
+ taskFragment.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(taskFragment, secondaryTaskFragment));
effects |= TRANSACT_EFFECTS_LIFECYCLE;
}
@@ -2220,30 +2216,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub
}
private int setAdjacentRootsHierarchyOp(WindowContainerTransaction.HierarchyOp hop) {
- if (!Flags.allowMultipleAdjacentTaskFragments()) {
- final WindowContainer wc1 = WindowContainer.fromBinder(hop.getContainer());
- if (wc1 == null || !wc1.isAttached()) {
- Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc1);
- return TRANSACT_EFFECTS_NONE;
- }
- final TaskFragment root1 = wc1.asTaskFragment();
- final WindowContainer wc2 = WindowContainer.fromBinder(hop.getAdjacentRoot());
- if (wc2 == null || !wc2.isAttached()) {
- Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc2);
- return TRANSACT_EFFECTS_NONE;
- }
- final TaskFragment root2 = wc2.asTaskFragment();
- if (!root1.mCreatedByOrganizer || !root2.mCreatedByOrganizer) {
- throw new IllegalArgumentException("setAdjacentRootsHierarchyOp: Not created by"
- + " organizer root1=" + root1 + " root2=" + root2);
- }
- if (root1.isAdjacentTo(root2)) {
- return TRANSACT_EFFECTS_NONE;
- }
- root1.setAdjacentTaskFragment(root2);
- return TRANSACT_EFFECTS_LIFECYCLE;
- }
-
final IBinder[] containers = hop.getContainers();
final ArraySet<TaskFragment> adjacentRoots = new ArraySet<>();
for (IBinder container : containers) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 0ad976c38565..51ed6bb2aa40 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -3806,9 +3806,10 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
// Update user switcher message to activity manager.
ActivityManagerInternal activityManagerInternal =
mInjector.getActivityManagerInternal();
- activityManagerInternal.setSwitchingFromSystemUserMessage(
+ int deviceOwnerUserId = UserHandle.getUserId(deviceOwner.getUid());
+ activityManagerInternal.setSwitchingFromUserMessage(deviceOwnerUserId,
deviceOwner.startUserSessionMessage);
- activityManagerInternal.setSwitchingToSystemUserMessage(
+ activityManagerInternal.setSwitchingToUserMessage(deviceOwnerUserId,
deviceOwner.endUserSessionMessage);
}
@@ -19716,7 +19717,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
mInjector.getActivityManagerInternal()
- .setSwitchingFromSystemUserMessage(startUserSessionMessageString);
+ .setSwitchingFromUserMessage(caller.getUserId(), startUserSessionMessageString);
}
@Override
@@ -19741,7 +19742,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub {
}
mInjector.getActivityManagerInternal()
- .setSwitchingToSystemUserMessage(endUserSessionMessageString);
+ .setSwitchingToUserMessage(caller.getUserId(), endUserSessionMessageString);
}
@Override
diff --git a/services/tests/DynamicInstrumentationManagerServiceTests/OWNERS b/services/tests/DynamicInstrumentationManagerServiceTests/OWNERS
new file mode 100644
index 000000000000..2522426d93f8
--- /dev/null
+++ b/services/tests/DynamicInstrumentationManagerServiceTests/OWNERS
@@ -0,0 +1 @@
+include platform/packages/modules/UprobeStats:/OWNERS \ No newline at end of file
diff --git a/services/tests/InputMethodSystemServerTests/Android.bp b/services/tests/InputMethodSystemServerTests/Android.bp
index ae9a34efc222..c1d8382fcd0e 100644
--- a/services/tests/InputMethodSystemServerTests/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/Android.bp
@@ -73,10 +73,7 @@ android_ravenwood_test {
static_libs: [
"androidx.annotation_annotation",
"androidx.test.rules",
- "framework",
- "ravenwood-runtime",
- "ravenwood-utils",
- "services",
+ "services.core",
],
libs: [
"android.test.base.stubs.system",
diff --git a/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java
index 586ff52aa78c..7c239ef02e58 100644
--- a/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java
+++ b/services/tests/dreamservicetests/src/com/android/server/dreams/TestDreamEnvironment.java
@@ -328,7 +328,6 @@ public class TestDreamEnvironment {
case DREAM_STATE_STARTED -> startDream();
case DREAM_STATE_WOKEN -> wakeDream();
}
- mTestableLooper.processAllMessages();
} while (mCurrentDreamState < state);
return true;
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 5d8f57866f7d..e094111c327a 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -746,6 +746,36 @@ public class MockingOomAdjusterTests {
@SuppressWarnings("GuardedBy")
@Test
@EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY)
+ public void testUpdateOomAdjFreezeState_bindingWithAllowFreeze() {
+ ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
+ MOCKAPP_PROCESSNAME, MOCKAPP_PACKAGENAME, true));
+ WindowProcessController wpc = app.getWindowProcessController();
+ doReturn(true).when(wpc).hasVisibleActivities();
+
+ final ProcessRecord app2 = spy(makeDefaultProcessRecord(MOCKAPP2_PID, MOCKAPP2_UID,
+ MOCKAPP2_PROCESSNAME, MOCKAPP2_PACKAGENAME, false));
+
+ // App with a visible activity binds to app2 without any special flag.
+ bindService(app2, app, null, null, 0, mock(IBinder.class));
+
+ final ProcessRecord app3 = spy(makeDefaultProcessRecord(MOCKAPP3_PID, MOCKAPP3_UID,
+ MOCKAPP3_PROCESSNAME, MOCKAPP3_PACKAGENAME, false));
+
+ // App with a visible activity binds to app3 with ALLOW_FREEZE.
+ bindService(app3, app, null, null, Context.BIND_ALLOW_FREEZE, mock(IBinder.class));
+
+ setProcessesToLru(app, app2, app3);
+
+ updateOomAdj(app);
+
+ assertCpuTime(app);
+ assertCpuTime(app2);
+ assertNoCpuTime(app3);
+ }
+
+ @SuppressWarnings("GuardedBy")
+ @Test
+ @EnableFlags(Flags.FLAG_USE_CPU_TIME_CAPABILITY)
@DisableFlags(Flags.FLAG_PROTOTYPE_AGGRESSIVE_FREEZING)
public void testUpdateOomAdjFreezeState_bindingFromFgs() {
final ProcessRecord app = spy(makeDefaultProcessRecord(MOCKAPP_PID, MOCKAPP_UID,
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java
index 49c37f163ff2..241ffdc19ce4 100644
--- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java
@@ -36,24 +36,29 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
+import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayInfo;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
import androidx.test.runner.AndroidJUnit4;
import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.internal.R;
import org.junit.AfterClass;
import org.junit.Before;
@@ -70,10 +75,11 @@ import java.io.File;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
+import java.util.Set;
/**
* Unit tests for the most important helpers of {@link WallpaperCropper}, in particular
- * {@link WallpaperCropper#getCrop(Point, Point, SparseArray, boolean)}.
+ * {@link WallpaperCropper#getCrop(Point, WallpaperDefaultDisplayInfo, Point, SparseArray, boolean)}.
*/
@Presubmit
@RunWith(AndroidJUnit4.class)
@@ -83,6 +89,12 @@ public class WallpaperCropperTest {
@Mock
private WallpaperDisplayHelper mWallpaperDisplayHelper;
+
+ @Mock
+ private WindowManager mWindowManager;
+
+ @Mock
+ private Resources mResources;
private WallpaperCropper mWallpaperCropper;
private static final Point PORTRAIT_ONE = new Point(500, 800);
@@ -175,14 +187,21 @@ public class WallpaperCropperTest {
return tempDir;
}
- private void setUpWithDisplays(List<Point> displaySizes) {
+ private WallpaperDefaultDisplayInfo setUpWithDisplays(List<Point> displaySizes) {
mDisplaySizes = new SparseArray<>();
displaySizes.forEach(size -> {
mDisplaySizes.put(getOrientation(size), size);
Point rotated = new Point(size.y, size.x);
mDisplaySizes.put(getOrientation(rotated), rotated);
});
+ Set<WindowMetrics> windowMetrics = new ArraySet<>();
+ for (Point displaySize : displaySizes) {
+ windowMetrics.add(
+ new WindowMetrics(new Rect(0, 0, displaySize.x, displaySize.y),
+ new WindowInsets.Builder().build()));
+ }
when(mWallpaperDisplayHelper.getDefaultDisplaySizes()).thenReturn(mDisplaySizes);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt())).thenReturn(windowMetrics);
if (displaySizes.size() == 2) {
Point largestDisplay = displaySizes.stream().max(
Comparator.comparingInt(p -> p.x * p.y)).get();
@@ -192,11 +211,16 @@ public class WallpaperCropperTest {
mFolded = getOrientation(smallestDisplay);
mUnfoldedRotated = getRotatedOrientation(mUnfolded);
mFoldedRotated = getRotatedOrientation(mFolded);
+ // foldable
+ doReturn(new int[]{0}).when(mResources).getIntArray(R.array.config_foldedDeviceStates);
+ } else {
+ // no foldable
+ doReturn(new int[]{}).when(mResources).getIntArray(R.array.config_foldedDeviceStates);
}
- doAnswer(invocation -> getFoldedOrientation(invocation.getArgument(0)))
- .when(mWallpaperDisplayHelper).getFoldedOrientation(anyInt());
- doAnswer(invocation -> getUnfoldedOrientation(invocation.getArgument(0)))
- .when(mWallpaperDisplayHelper).getUnfoldedOrientation(anyInt());
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ when(mWallpaperDisplayHelper.getDefaultDisplayInfo()).thenReturn(defaultDisplayInfo);
+ return defaultDisplayInfo;
}
private int getFoldedOrientation(int orientation) {
@@ -435,7 +459,7 @@ public class WallpaperCropperTest {
*/
@Test
public void testGetCrop_noSuggestedCrops() {
- setUpWithDisplays(STANDARD_DISPLAY);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY);
Point bitmapSize = new Point(800, 1000);
Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
SparseArray<Rect> suggestedCrops = new SparseArray<>();
@@ -455,8 +479,9 @@ public class WallpaperCropperTest {
for (boolean rtl : List.of(false, true)) {
Rect expectedCrop = rtl ? rightOf(bitmapRect, expectedCropSize)
: leftOf(bitmapRect, expectedCropSize);
- assertThat(mWallpaperCropper.getCrop(
- displaySize, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(
+ displaySize, defaultDisplayInfo, bitmapSize, suggestedCrops, rtl))
.isEqualTo(expectedCrop);
}
}
@@ -469,7 +494,7 @@ public class WallpaperCropperTest {
*/
@Test
public void testGetCrop_hasSuggestedCrop() {
- setUpWithDisplays(STANDARD_DISPLAY);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY);
Point bitmapSize = new Point(800, 1000);
SparseArray<Rect> suggestedCrops = new SparseArray<>();
suggestedCrops.put(ORIENTATION_PORTRAIT, new Rect(0, 0, 400, 800));
@@ -479,11 +504,13 @@ public class WallpaperCropperTest {
}
for (boolean rtl : List.of(false, true)) {
- assertThat(mWallpaperCropper.getCrop(
- new Point(300, 800), bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(new Point(300, 800), defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(suggestedCrops.get(ORIENTATION_PORTRAIT));
- assertThat(mWallpaperCropper.getCrop(
- new Point(500, 800), bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(new Point(500, 800), defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(new Rect(0, 0, 500, 800));
}
}
@@ -499,7 +526,7 @@ public class WallpaperCropperTest {
*/
@Test
public void testGetCrop_hasRotatedSuggestedCrop() {
- setUpWithDisplays(STANDARD_DISPLAY);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(STANDARD_DISPLAY);
Point bitmapSize = new Point(2000, 1800);
Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
SparseArray<Rect> suggestedCrops = new SparseArray<>();
@@ -510,12 +537,14 @@ public class WallpaperCropperTest {
suggestedCrops.put(ORIENTATION_PORTRAIT, centerOf(bitmapRect, portrait));
suggestedCrops.put(ORIENTATION_SQUARE_LANDSCAPE, centerOf(bitmapRect, squareLandscape));
for (boolean rtl : List.of(false, true)) {
- assertThat(mWallpaperCropper.getCrop(
- landscape, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(landscape, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(centerOf(bitmapRect, landscape));
- assertThat(mWallpaperCropper.getCrop(
- squarePortrait, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(squarePortrait, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(centerOf(bitmapRect, squarePortrait));
}
}
@@ -532,7 +561,7 @@ public class WallpaperCropperTest {
@Test
public void testGetCrop_hasUnfoldedSuggestedCrop() {
for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
- setUpWithDisplays(displaySizes);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes);
Point bitmapSize = new Point(2000, 2400);
Rect bitmapRect = new Rect(0, 0, bitmapSize.x, bitmapSize.y);
@@ -569,8 +598,9 @@ public class WallpaperCropperTest {
expectedCrop.right = Math.min(
unfoldedCrop.right, unfoldedCrop.right + maxParallax);
}
- assertThat(mWallpaperCropper.getCrop(
- foldedDisplay, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(foldedDisplay, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl))
.isEqualTo(expectedCrop);
}
}
@@ -588,7 +618,7 @@ public class WallpaperCropperTest {
@Test
public void testGetCrop_hasFoldedSuggestedCrop() {
for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
- setUpWithDisplays(displaySizes);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes);
Point bitmapSize = new Point(2000, 2000);
Rect bitmapRect = new Rect(0, 0, 2000, 2000);
@@ -610,12 +640,14 @@ public class WallpaperCropperTest {
Point unfoldedDisplayTwo = mDisplaySizes.get(unfoldedTwo);
for (boolean rtl : List.of(false, true)) {
- assertThat(centerOf(mWallpaperCropper.getCrop(
- unfoldedDisplayOne, bitmapSize, suggestedCrops, rtl), foldedDisplayOne))
+ assertThat(centerOf(
+ WallpaperCropper.getCrop(unfoldedDisplayOne, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl), foldedDisplayOne))
.isEqualTo(foldedCropOne);
- assertThat(centerOf(mWallpaperCropper.getCrop(
- unfoldedDisplayTwo, bitmapSize, suggestedCrops, rtl), foldedDisplayTwo))
+ assertThat(centerOf(
+ WallpaperCropper.getCrop(unfoldedDisplayTwo, defaultDisplayInfo, bitmapSize,
+ suggestedCrops, rtl), foldedDisplayTwo))
.isEqualTo(foldedCropTwo);
}
}
@@ -633,7 +665,7 @@ public class WallpaperCropperTest {
@Test
public void testGetCrop_hasRotatedUnfoldedSuggestedCrop() {
for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
- setUpWithDisplays(displaySizes);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes);
Point bitmapSize = new Point(2000, 2000);
Rect bitmapRect = new Rect(0, 0, 2000, 2000);
Point largestDisplay = displaySizes.stream().max(
@@ -650,8 +682,9 @@ public class WallpaperCropperTest {
Point rotatedFoldedDisplay = mDisplaySizes.get(rotatedFolded);
for (boolean rtl : List.of(false, true)) {
- assertThat(mWallpaperCropper.getCrop(
- rotatedFoldedDisplay, bitmapSize, suggestedCrops, rtl))
+ assertThat(
+ WallpaperCropper.getCrop(rotatedFoldedDisplay, defaultDisplayInfo,
+ bitmapSize, suggestedCrops, rtl))
.isEqualTo(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay));
}
}
@@ -670,7 +703,7 @@ public class WallpaperCropperTest {
@Test
public void testGetCrop_hasRotatedFoldedSuggestedCrop() {
for (List<Point> displaySizes : ALL_FOLDABLE_DISPLAYS) {
- setUpWithDisplays(displaySizes);
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = setUpWithDisplays(displaySizes);
Point bitmapSize = new Point(2000, 2000);
Rect bitmapRect = new Rect(0, 0, 2000, 2000);
@@ -689,8 +722,8 @@ public class WallpaperCropperTest {
Point rotatedUnfoldedDisplay = mDisplaySizes.get(rotatedUnfolded);
for (boolean rtl : List.of(false, true)) {
- Rect rotatedUnfoldedCrop = mWallpaperCropper.getCrop(
- rotatedUnfoldedDisplay, bitmapSize, suggestedCrops, rtl);
+ Rect rotatedUnfoldedCrop = WallpaperCropper.getCrop(rotatedUnfoldedDisplay,
+ defaultDisplayInfo, bitmapSize, suggestedCrops, rtl);
assertThat(centerOf(rotatedUnfoldedCrop, rotatedFoldedDisplay))
.isEqualTo(rotatedFoldedCrop);
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java
new file mode 100644
index 000000000000..312db91afb12
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperDefaultDisplayInfoTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wallpaper;
+
+import static android.app.WallpaperManager.ORIENTATION_LANDSCAPE;
+import static android.app.WallpaperManager.ORIENTATION_PORTRAIT;
+import static android.app.WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE;
+import static android.app.WallpaperManager.ORIENTATION_SQUARE_PORTRAIT;
+import static android.app.WallpaperManager.ORIENTATION_UNKNOWN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.Presubmit;
+import android.util.SparseArray;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.server.wallpaper.WallpaperDefaultDisplayInfo.FoldableOrientations;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+import java.util.Set;
+
+/** Unit tests for {@link WallpaperDefaultDisplayInfo}. */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class WallpaperDefaultDisplayInfoTest {
+ @Mock
+ private WindowManager mWindowManager;
+
+ @Mock
+ private Resources mResources;
+
+ @Before
+ public void setUp() {
+ initMocks(this);
+ }
+
+ @Test
+ public void defaultDisplayInfo_foldable_shouldHaveExpectedContent() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ SparseArray<Point> displaySizes = new SparseArray<>();
+ displaySizes.put(ORIENTATION_PORTRAIT, new Point(1080, 2424));
+ displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2424, 1080));
+ displaySizes.put(ORIENTATION_SQUARE_PORTRAIT, new Point(2076, 2152));
+ displaySizes.put(ORIENTATION_SQUARE_LANDSCAPE, new Point(2152, 2076));
+ assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue();
+ assertThat(defaultDisplayInfo.isFoldable).isTrue();
+ assertThat(defaultDisplayInfo.isLargeScreen).isFalse();
+ assertThat(defaultDisplayInfo.foldableOrientations).containsExactly(
+ new FoldableOrientations(
+ /* foldedOrientation= */ ORIENTATION_PORTRAIT,
+ /* unfoldedOrientation= */ ORIENTATION_SQUARE_PORTRAIT),
+ new FoldableOrientations(
+ /* foldedOrientation= */ ORIENTATION_LANDSCAPE,
+ /* unfoldedOrientation= */ ORIENTATION_SQUARE_LANDSCAPE));
+ }
+
+ @Test
+ public void defaultDisplayInfo_tablet_shouldHaveExpectedContent() {
+ doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect displayBounds = new Rect(0, 0, 2560, 1600);
+ WindowMetrics displayMetrics =
+ new WindowMetrics(displayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(displayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ SparseArray<Point> displaySizes = new SparseArray<>();
+ displaySizes.put(ORIENTATION_PORTRAIT, new Point(1600, 2560));
+ displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2560, 1600));
+ assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue();
+ assertThat(defaultDisplayInfo.isFoldable).isFalse();
+ assertThat(defaultDisplayInfo.isLargeScreen).isTrue();
+ assertThat(defaultDisplayInfo.foldableOrientations).isEmpty();
+ }
+
+ @Test
+ public void defaultDisplayInfo_phone_shouldHaveExpectedContent() {
+ doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect displayBounds = new Rect(0, 0, 1280, 2856);
+ WindowMetrics displayMetrics =
+ new WindowMetrics(displayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 3f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(displayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ SparseArray<Point> displaySizes = new SparseArray<>();
+ displaySizes.put(ORIENTATION_PORTRAIT, new Point(1280, 2856));
+ displaySizes.put(ORIENTATION_LANDSCAPE, new Point(2856, 1280));
+ assertThat(defaultDisplayInfo.defaultDisplaySizes.contentEquals(displaySizes)).isTrue();
+ assertThat(defaultDisplayInfo.isFoldable).isFalse();
+ assertThat(defaultDisplayInfo.isLargeScreen).isFalse();
+ assertThat(defaultDisplayInfo.foldableOrientations).isEmpty();
+ }
+
+ @Test
+ public void defaultDisplayInfo_equals_sameContent_shouldEqual() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo).isEqualTo(otherDefaultDisplayInfo);
+ }
+
+ @Test
+ public void defaultDisplayInfo_equals_differentBounds_shouldNotEqual() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ // For the first call
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics))
+ // For the second+ call
+ .thenReturn(Set.of(innerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo).isNotEqualTo(otherDefaultDisplayInfo);
+ }
+
+ @Test
+ public void defaultDisplayInfo_hashCode_sameContent_shouldEqual() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.hashCode()).isEqualTo(otherDefaultDisplayInfo.hashCode());
+ }
+
+ @Test
+ public void defaultDisplayInfo_hashCode_differentBounds_shouldNotEqual() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ // For the first call
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics))
+ // For the second+ call
+ .thenReturn(Set.of(innerDisplayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+ WallpaperDefaultDisplayInfo otherDefaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.hashCode()).isNotEqualTo(otherDefaultDisplayInfo.hashCode());
+ }
+
+ @Test
+ public void getFoldedOrientation_foldable_shouldReturnExpectedOrientation() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_PORTRAIT))
+ .isEqualTo(ORIENTATION_PORTRAIT);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE))
+ .isEqualTo(ORIENTATION_LANDSCAPE);
+ // Use a folded orientation for a folded orientation should return unknown.
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ }
+
+ @Test
+ public void getUnfoldedOrientation_foldable_shouldReturnExpectedOrientation() {
+ doReturn(new int[]{0}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect innerDisplayBounds = new Rect(0, 0, 2076, 2152);
+ Rect outerDisplayBounds = new Rect(0, 0, 1080, 2424);
+ WindowMetrics innerDisplayMetrics =
+ new WindowMetrics(innerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ WindowMetrics outerDisplayMetrics =
+ new WindowMetrics(outerDisplayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2.4375f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(innerDisplayMetrics, outerDisplayMetrics));
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_PORTRAIT))
+ .isEqualTo(ORIENTATION_SQUARE_PORTRAIT);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_LANDSCAPE))
+ .isEqualTo(ORIENTATION_SQUARE_LANDSCAPE);
+ // Use an unfolded orientation for an unfolded orientation should return unknown.
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ }
+
+ @Test
+ public void getFoldedOrientation_nonFoldable_shouldReturnUnknown() {
+ doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect displayBounds = new Rect(0, 0, 2560, 1600);
+ WindowMetrics displayMetrics =
+ new WindowMetrics(displayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(displayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getFoldedOrientation(ORIENTATION_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ }
+
+ @Test
+ public void getUnFoldedOrientation_nonFoldable_shouldReturnUnknown() {
+ doReturn(new int[]{}).when(mResources).getIntArray(eq(R.array.config_foldedDeviceStates));
+ Rect displayBounds = new Rect(0, 0, 2560, 1600);
+ WindowMetrics displayMetrics =
+ new WindowMetrics(displayBounds, new WindowInsets.Builder().build(),
+ /* density= */ 2f);
+ when(mWindowManager.getPossibleMaximumWindowMetrics(anyInt()))
+ .thenReturn(Set.of(displayMetrics));
+
+ WallpaperDefaultDisplayInfo defaultDisplayInfo = new WallpaperDefaultDisplayInfo(
+ mWindowManager, mResources);
+
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_SQUARE_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_PORTRAIT))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ assertThat(defaultDisplayInfo.getUnfoldedOrientation(ORIENTATION_LANDSCAPE))
+ .isEqualTo(ORIENTATION_UNKNOWN);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
index efea21428937..63c572af37b2 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/HearingDevicePhoneCallNotificationControllerTest.java
@@ -171,7 +171,7 @@ public class HearingDevicePhoneCallNotificationControllerTest {
HearingDevicePhoneCallNotificationController.CallStateListener {
TestCallStateListener(@NonNull Context context) {
- super(context);
+ super(context, context.getMainExecutor());
}
@Override
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 69877c372442..ea25e7992dd9 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
@@ -43,6 +43,7 @@ import android.view.accessibility.AccessibilityManager;
import com.android.internal.accessibility.util.AccessibilityUtils;
import com.android.server.accessibility.AccessibilityTraceManager;
+import com.android.server.accessibility.BaseEventStreamTransformation;
import org.junit.After;
import org.junit.Before;
@@ -70,6 +71,19 @@ public class AutoclickControllerTest {
@Mock private WindowManager mMockWindowManager;
private AutoclickController mController;
+ private static class MotionEventCaptor extends BaseEventStreamTransformation {
+ public MotionEvent downEvent;
+
+ @Override
+ public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ downEvent = event;
+ break;
+ }
+ }
+ }
+
@Before
public void setUp() {
mTestableLooper = TestableLooper.get(this);
@@ -713,6 +727,100 @@ public class AutoclickControllerTest {
assertThat(mController.mAutoclickScrollPanel.isVisible()).isFalse();
}
+ @Test
+ public void sendClick_clickType_leftClick() {
+ MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
+ mController.setNext(motionEventCaptor);
+
+ injectFakeMouseActionHoverMoveEvent();
+ // Set delay to zero so click is scheduled to run immediately.
+ mController.mClickScheduler.updateDelay(0);
+
+ // Send hover move event.
+ MotionEvent hoverMove = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 100,
+ /* action= */ MotionEvent.ACTION_HOVER_MOVE,
+ /* x= */ 30f,
+ /* y= */ 0f,
+ /* metaState= */ 0);
+ hoverMove.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ mTestableLooper.processAllMessages();
+
+ // Verify left click sent.
+ assertThat(motionEventCaptor.downEvent).isNotNull();
+ assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo(
+ MotionEvent.BUTTON_PRIMARY);
+ }
+
+ @Test
+ public void sendClick_clickType_rightClick() {
+ MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
+ mController.setNext(motionEventCaptor);
+
+ injectFakeMouseActionHoverMoveEvent();
+ // Set delay to zero so click is scheduled to run immediately.
+ mController.mClickScheduler.updateDelay(0);
+
+ // Set click type to right click.
+ mController.clickPanelController.handleAutoclickTypeChange(
+ AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK);
+
+ // Send hover move event.
+ MotionEvent hoverMove = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 100,
+ /* action= */ MotionEvent.ACTION_HOVER_MOVE,
+ /* x= */ 30f,
+ /* y= */ 0f,
+ /* metaState= */ 0);
+ hoverMove.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ mTestableLooper.processAllMessages();
+
+ // Verify right click sent.
+ assertThat(motionEventCaptor.downEvent).isNotNull();
+ assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo(
+ MotionEvent.BUTTON_SECONDARY);
+ }
+
+ @Test
+ @EnableFlags(com.android.server.accessibility.Flags.FLAG_ENABLE_AUTOCLICK_INDICATOR)
+ public void hoverOnAutoclickPanel_rightClickType_forceTriggerLeftClick() {
+ MotionEventCaptor motionEventCaptor = new MotionEventCaptor();
+ mController.setNext(motionEventCaptor);
+
+ injectFakeMouseActionHoverMoveEvent();
+ // Set delay to zero so click is scheduled to run immediately.
+ mController.mClickScheduler.updateDelay(0);
+
+ // Set click type to right click.
+ mController.clickPanelController.handleAutoclickTypeChange(
+ AutoclickTypePanel.AUTOCLICK_TYPE_RIGHT_CLICK);
+ // Set mouse to hover panel.
+ AutoclickTypePanel mockAutoclickTypePanel = mock(AutoclickTypePanel.class);
+ when(mockAutoclickTypePanel.isHovered()).thenReturn(true);
+ mController.mAutoclickTypePanel = mockAutoclickTypePanel;
+
+ // Send hover move event.
+ MotionEvent hoverMove = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 100,
+ /* action= */ MotionEvent.ACTION_HOVER_MOVE,
+ /* x= */ 30f,
+ /* y= */ 0f,
+ /* metaState= */ 0);
+ hoverMove.setSource(InputDevice.SOURCE_MOUSE);
+ mController.onMotionEvent(hoverMove, hoverMove, /* policyFlags= */ 0);
+ mTestableLooper.processAllMessages();
+
+ // Verify left click is sent due to the mouse hovering the panel.
+ assertThat(motionEventCaptor.downEvent).isNotNull();
+ assertThat(motionEventCaptor.downEvent.getButtonState()).isEqualTo(
+ MotionEvent.BUTTON_PRIMARY);
+ }
+
private void injectFakeMouseActionHoverMoveEvent() {
MotionEvent event = getFakeMotionHoverMoveEvent();
event.setSource(InputDevice.SOURCE_MOUSE);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java
index f445b50c7d9c..02361ff259c2 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickScrollPanelTest.java
@@ -28,7 +28,12 @@ import android.content.Context;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
+import android.view.MotionEvent;
+import android.view.View;
import android.view.WindowManager;
+import android.widget.ImageButton;
+
+import com.android.internal.R;
import org.junit.Before;
import org.junit.Rule;
@@ -49,12 +54,31 @@ public class AutoclickScrollPanelTest {
new TestableContext(getInstrumentation().getContext());
@Mock private WindowManager mMockWindowManager;
+ @Mock private AutoclickScrollPanel.ScrollPanelControllerInterface mMockScrollPanelController;
+
private AutoclickScrollPanel mScrollPanel;
+ // Scroll panel buttons.
+ private ImageButton mUpButton;
+ private ImageButton mDownButton;
+ private ImageButton mLeftButton;
+ private ImageButton mRightButton;
+ private ImageButton mExitButton;
+
@Before
public void setUp() {
mTestableContext.addMockSystemService(Context.WINDOW_SERVICE, mMockWindowManager);
- mScrollPanel = new AutoclickScrollPanel(mTestableContext, mMockWindowManager);
+ mScrollPanel = new AutoclickScrollPanel(mTestableContext, mMockWindowManager,
+ mMockScrollPanelController);
+
+ View contentView = mScrollPanel.getContentViewForTesting();
+
+ // Initialize buttons.
+ mUpButton = contentView.findViewById(R.id.scroll_up);
+ mDownButton = contentView.findViewById(R.id.scroll_down);
+ mLeftButton = contentView.findViewById(R.id.scroll_left);
+ mRightButton = contentView.findViewById(R.id.scroll_right);
+ mExitButton = contentView.findViewById(R.id.scroll_exit);
}
@Test
@@ -89,4 +113,55 @@ public class AutoclickScrollPanelTest {
// Verify scroll panel is hidden.
assertThat(mScrollPanel.isVisible()).isFalse();
}
+
+ @Test
+ public void initialState_correctButtonVisibility() {
+ // Verify all expected buttons exist in the view.
+ assertThat(mUpButton.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mDownButton.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mLeftButton.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mRightButton.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mExitButton.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void directionButtons_onHover_callsHandleScroll() {
+ // Test up button.
+ triggerHoverEvent(mUpButton);
+ verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_UP);
+
+ // Test down button.
+ triggerHoverEvent(mDownButton);
+ verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_DOWN);
+
+ // Test left button.
+ triggerHoverEvent(mLeftButton);
+ verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_LEFT);
+
+ // Test right button.
+ triggerHoverEvent(mRightButton);
+ verify(mMockScrollPanelController).handleScroll(AutoclickScrollPanel.DIRECTION_RIGHT);
+ }
+
+ @Test
+ public void exitButton_onHover_callsExitScrollMode() {
+ // Test exit button.
+ triggerHoverEvent(mExitButton);
+ verify(mMockScrollPanelController).exitScrollMode();
+ }
+
+ // Helper method to simulate a hover event on a view.
+ private void triggerHoverEvent(View view) {
+ MotionEvent event = MotionEvent.obtain(
+ /* downTime= */ 0,
+ /* eventTime= */ 0,
+ /* action= */ MotionEvent.ACTION_HOVER_ENTER,
+ /* x= */ 0,
+ /* y= */ 0,
+ /* metaState= */ 0);
+
+ // Dispatch the event to the view's OnHoverListener.
+ view.dispatchGenericMotionEvent(event);
+ event.recycle();
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
index 1af59daa9c78..5922b12edc1e 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchExplorerTest.java
@@ -37,6 +37,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -46,6 +47,7 @@ import android.content.Context;
import android.graphics.PointF;
import android.os.Looper;
import android.os.SystemClock;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.testing.DexmakerShareClassLoaderRule;
@@ -504,6 +506,36 @@ public class TouchExplorerTest {
assertThat(sentRawEvent.getDisplayId()).isEqualTo(rawDisplayId);
}
+ @Test
+ @DisableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ public void handleMotionEventStateTouchExploring_pointerUp_doesNotSendToManager() {
+ mTouchExplorer.getState().setServiceDetectsGestures(true);
+ mTouchExplorer.getState().clear();
+
+ mLastEvent = pointerDownEvent();
+ mTouchExplorer.getState().startTouchExploring();
+ MotionEvent event = fromTouchscreen(pointerUpEvent());
+
+ mTouchExplorer.onMotionEvent(event, event, /*policyFlags=*/0);
+
+ verify(mMockAms, never()).sendMotionEventToListeningServices(event);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ public void handleMotionEventStateTouchExploring_pointerUp_sendsToManager() {
+ mTouchExplorer.getState().setServiceDetectsGestures(true);
+ mTouchExplorer.getState().clear();
+
+ mLastEvent = pointerDownEvent();
+ mTouchExplorer.getState().startTouchExploring();
+ MotionEvent event = fromTouchscreen(pointerUpEvent());
+
+ mTouchExplorer.onMotionEvent(event, event, /*policyFlags=*/0);
+
+ verify(mMockAms).sendMotionEventToListeningServices(event);
+ }
+
/**
* Used to play back event data of a gesture by parsing the log into MotionEvents and sending
* them to TouchExplorer.
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java
new file mode 100644
index 000000000000..3e7d9fd05327
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/gestures/TouchStateTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.accessibility.gestures;
+
+import static android.view.accessibility.AccessibilityEvent.TYPE_TOUCH_INTERACTION_END;
+
+import static com.android.server.accessibility.gestures.TouchState.STATE_CLEAR;
+import static com.android.server.accessibility.gestures.TouchState.STATE_TOUCH_EXPLORING;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.Display;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.accessibility.AccessibilityManagerService;
+import com.android.server.accessibility.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class TouchStateTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ private TouchState mTouchState;
+ @Mock private AccessibilityManagerService mMockAms;
+
+ @Before
+ public void setup() {
+ mTouchState = new TouchState(Display.DEFAULT_DISPLAY, mMockAms);
+ }
+
+ @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ @Test
+ public void injectedEvent_interactionEnd_pointerDown_startsTouchExploring() {
+ mTouchState.mReceivedPointerTracker.mReceivedPointersDown = 1;
+ mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END);
+ assertThat(mTouchState.getState()).isEqualTo(STATE_TOUCH_EXPLORING);
+ }
+
+ @EnableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ @Test
+ public void injectedEvent_interactionEnd_pointerUp_clears() {
+ mTouchState.mReceivedPointerTracker.mReceivedPointersDown = 0;
+ mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END);
+ assertThat(mTouchState.getState()).isEqualTo(STATE_CLEAR);
+ }
+
+ @DisableFlags(Flags.FLAG_POINTER_UP_MOTION_EVENT_IN_TOUCH_EXPLORATION)
+ @Test
+ public void injectedEvent_interactionEnd_clears() {
+ mTouchState.onInjectedAccessibilityEvent(TYPE_TOUCH_INTERACTION_END);
+ assertThat(mTouchState.getState()).isEqualTo(STATE_CLEAR);
+ }
+}
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 b0ffebb973a1..aa1d5835bfc8 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -2295,6 +2295,38 @@ public class HdmiCecLocalDeviceTvTest {
.hasSize(1);
}
+ @Test
+ public void onOneTouchPlay_wakeUp_exist_device() {
+ HdmiCecMessage requestActiveSource =
+ HdmiCecMessageBuilder.buildRequestActiveSource(ADDR_TV);
+
+ // Go to standby to trigger RequestActiveSourceAction for playback_1
+ mHdmiControlService.onStandby(STANDBY_SCREEN_OFF);
+ mTestLooper.dispatchAll();
+
+ mNativeWrapper.setPollAddressResponse(ADDR_PLAYBACK_1, SendMessageResult.SUCCESS);
+ mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON);
+ mTestLooper.dispatchAll();
+
+ // Skip the LauncherX API timeout.
+ mTestLooper.moveTimeForward(TIMEOUT_WAIT_FOR_TV_ASSERT_ACTIVE_SOURCE_MS);
+ mTestLooper.dispatchAll();
+
+ assertThat(mNativeWrapper.getResultMessages()).contains(requestActiveSource);
+ mNativeWrapper.clearResultMessages();
+
+ // turn off TV and wake up with one touch play
+ mHdmiControlService.onStandby(STANDBY_SCREEN_OFF);
+ mTestLooper.dispatchAll();
+
+ // FakePowerManagerWrapper#wakeUp() doesn't broadcast Intent.ACTION_SCREEN_ON
+ // manually trigger onWakeUp to mock OTP
+ mHdmiControlService.onWakeUp(WAKE_UP_SCREEN_ON);
+ mTestLooper.dispatchAll();
+
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(requestActiveSource);
+ }
+
@Test
public void handleReportAudioStatus_SamOnAvrStandby_startSystemAudioActionFromTv() {
diff --git a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
index 3ca019728c2b..fcdf88f16550 100644
--- a/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/KeyGestureEventTests.java
@@ -605,29 +605,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase {
}
@Test
- public void testKeyGestureAccessibilityShortcutChord() {
- Assert.assertTrue(
- sendKeyGestureEventStart(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.moveTimeForward(5000);
- Assert.assertTrue(
- sendKeyGestureEventCancel(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.assertAccessibilityKeychordCalled();
- }
-
- @Test
- public void testKeyGestureAccessibilityShortcutChordCancelled() {
- Assert.assertTrue(
- sendKeyGestureEventStart(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
- Assert.assertTrue(
- sendKeyGestureEventCancel(
- KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
- }
-
- @Test
public void testKeyGestureRingerToggleChord() {
mPhoneWindowManager.overridePowerVolumeUp(POWER_VOLUME_UP_BEHAVIOR_MUTE);
Assert.assertTrue(
@@ -670,29 +647,6 @@ public class KeyGestureEventTests extends ShortcutKeyTestBase {
}
@Test
- public void testKeyGestureAccessibilityTvShortcutChord() {
- Assert.assertTrue(
- sendKeyGestureEventStart(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.moveTimeForward(5000);
- Assert.assertTrue(
- sendKeyGestureEventCancel(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.assertAccessibilityKeychordCalled();
- }
-
- @Test
- public void testKeyGestureAccessibilityTvShortcutChordCancelled() {
- Assert.assertTrue(
- sendKeyGestureEventStart(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
- Assert.assertTrue(
- sendKeyGestureEventCancel(
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD));
- mPhoneWindowManager.assertAccessibilityKeychordNotCalled();
- }
-
- @Test
public void testKeyGestureTvTriggerBugReport() {
Assert.assertTrue(
sendKeyGestureEventStart(KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT));
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index f88492477487..e56fd3c6272d 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -750,11 +750,6 @@ class TestPhoneWindowManager {
verify(mAccessibilityShortcutController).performAccessibilityShortcut();
}
- void assertAccessibilityKeychordNotCalled() {
- mTestLooper.dispatchAll();
- verify(mAccessibilityShortcutController, never()).performAccessibilityShortcut();
- }
-
void assertCloseAllDialogs() {
verify(mContext).closeSystemDialogs();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index e3e9cc426bb3..08b0077c49b3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -797,7 +797,7 @@ public class ActivityStarterTests extends WindowTestsBase {
// Create adjacent tasks and put one activity under it
final Task parent = new TaskBuilder(mSupervisor).build();
final Task adjacentParent = new TaskBuilder(mSupervisor).build();
- parent.setAdjacentTaskFragment(adjacentParent);
+ parent.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(parent, adjacentParent));
final ActivityRecord activity = new ActivityBuilder(mAtm)
.setParentTask(parent)
.setCreateTask(true).build();
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 a9be47d71213..86d901b640ff 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -488,14 +488,13 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase {
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);
+ tf1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf1, tf2));
assertThat(mSupervisor.mOpaqueContainerHelper.isOpaque(rootTask)).isTrue();
}
@Test
- @EnableFlags({Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
- Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS})
+ @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
public void testOpaque_rootTask_nonFillingOpaqueAdjacentChildren_multipleAdjacent_isOpaque() {
final Task rootTask = new TaskBuilder(mSupervisor).setOnTop(true).build();
final TaskFragment tf1 = createChildTaskFragment(/* parent */ rootTask,
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
index 8fe08553db95..cb98b9a490d8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java
@@ -243,6 +243,11 @@ class AppCompatActivityRobot {
.getAspectRatioOverrides()).getUserMinAspectRatio();
}
+ void setShouldRefreshActivityForCameraCompat(boolean enabled) {
+ doReturn(enabled).when(mActivityStack.top().mAppCompatController.getCameraOverrides())
+ .shouldRefreshActivityForCameraCompat();
+ }
+
void setIgnoreOrientationRequest(boolean enabled) {
mDisplayContent.setIgnoreOrientationRequest(enabled);
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
index 05f6ed644632..7ef85262dfc2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatConfigurationRobot.java
@@ -64,6 +64,19 @@ class AppCompatConfigurationRobot {
.isCameraCompatTreatmentEnabledAtBuildTime();
}
+ void setCameraCompatAspectRatio(float aspectRatio) {
+ doReturn(aspectRatio).when(mAppCompatConfiguration).getCameraCompatAspectRatio();
+ }
+
+ void enableCameraCompatRefresh(boolean enabled) {
+ doReturn(enabled).when(mAppCompatConfiguration).isCameraCompatRefreshEnabled();
+ }
+
+ void enableCameraCompatRefreshCycleThroughStop(boolean enabled) {
+ doReturn(enabled).when(mAppCompatConfiguration)
+ .isCameraCompatRefreshCycleThroughStopEnabled();
+ }
+
void enableUserAppAspectRatioFullscreen(boolean enabled) {
doReturn(enabled).when(mAppCompatConfiguration).isUserAppAspectRatioFullscreenEnabled();
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index bdee3c323549..dd3e9fcbbdaf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -343,8 +343,7 @@ public class BackNavigationControllerTests extends WindowTestsBase {
// Adjacent + no companion => unable to predict
// TF1 | TF2
- tf1.setAdjacentTaskFragment(tf2);
- tf2.setAdjacentTaskFragment(tf1);
+ tf1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf1, tf2));
predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr,
outPrevActivities);
assertTrue(outPrevActivities.isEmpty());
@@ -393,8 +392,7 @@ public class BackNavigationControllerTests extends WindowTestsBase {
// Adjacent => predict for previous activity.
// TF2 | TF3
// TF1
- tf2.setAdjacentTaskFragment(tf3);
- tf3.setAdjacentTaskFragment(tf2);
+ tf2.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf2, tf3));
predictable = BackNavigationController.getAnimatablePrevActivities(task, topAr,
outPrevActivities);
assertTrue(outPrevActivities.contains(prevAr));
@@ -657,8 +655,7 @@ public class BackNavigationControllerTests extends WindowTestsBase {
final TaskFragment secondaryTf = createTaskFragmentWithEmbeddedActivity(task, organizer);
final ActivityRecord primaryActivity = primaryTf.getTopMostActivity();
final ActivityRecord secondaryActivity = secondaryTf.getTopMostActivity();
- primaryTf.setAdjacentTaskFragment(secondaryTf);
- secondaryTf.setAdjacentTaskFragment(primaryTf);
+ primaryTf.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primaryTf, secondaryTf));
final WindowState primaryWindow = mock(WindowState.class);
final WindowState secondaryWindow = mock(WindowState.class);
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 f5bec04a98d5..6f959812d742 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java
@@ -21,13 +21,13 @@ import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_LANDSCAPE_
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE;
import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT;
+import static android.app.CameraCompatTaskInfo.FreeformCameraCompatMode;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_SIMULATE_REQUESTED_ORIENTATION;
import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT;
-import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
@@ -40,18 +40,15 @@ import static android.view.Surface.ROTATION_90;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING;
import static com.android.window.flags.Flags.FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
@@ -59,13 +56,11 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.annotation.NonNull;
-import android.app.CameraCompatTaskInfo;
import android.app.IApplicationThread;
import android.app.WindowConfiguration.WindowingMode;
import android.app.servertransaction.RefreshCallbackItem;
import android.app.servertransaction.ResumeActivityItem;
import android.compat.testing.PlatformCompatChangeRule;
-import android.content.ComponentName;
import android.content.pm.ActivityInfo.ScreenOrientation;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
@@ -73,17 +68,16 @@ import android.content.res.Configuration.Orientation;
import android.graphics.Rect;
import android.hardware.camera2.CameraManager;
import android.os.Handler;
+import android.os.RemoteException;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
-import android.view.DisplayInfo;
import android.view.Surface;
import androidx.test.filters.SmallTest;
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
-import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
@@ -91,6 +85,7 @@ import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
* Tests for {@link CameraCompatFreeformPolicy}.
@@ -109,30 +104,18 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
private static final String TEST_PACKAGE_1 = "com.android.frameworks.wmtests";
private static final String TEST_PACKAGE_2 = "com.test.package.two";
private static final String CAMERA_ID_1 = "camera-1";
- private AppCompatConfiguration mAppCompatConfiguration;
-
- private CameraManager.AvailabilityCallback mCameraAvailabilityCallback;
- private CameraCompatFreeformPolicy mCameraCompatFreeformPolicy;
- private ActivityRecord mActivity;
-
- // TODO(b/384465100): use a robot structure.
- @Before
- public void setUp() throws Exception {
- setupAppCompatConfiguration();
- setupCameraManager();
- setupHandler();
- doReturn(true).when(() -> DesktopModeHelper.canEnterDesktopMode(any()));
- }
@Test
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testFeatureDisabled_cameraCompatFreeformPolicyNotCreated() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertNull(mCameraCompatFreeformPolicy);
+ robot.checkCameraCompatPolicyNotCreated();
+ });
}
@Test
@@ -140,31 +123,37 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT})
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_SIMULATE_REQUESTED_ORIENTATION})
public void testIsCameraRunningAndWindowingModeEligible_disabledViaOverride_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsCameraRunningAndWindowingModeEligible_cameraNotRunning_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsCameraRunningAndWindowingModeEligible_notFreeformWindowing_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(false);
+ });
}
@Test
@@ -172,64 +161,76 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsCameraRunningAndWindowingModeEligible_optInFreeformCameraRunning_true() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(true);
+ });
}
@Test
@EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING,
FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT})
public void testIsCameraRunningAndWindowingModeEligible_freeformCameraRunning_true() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isCameraRunningAndWindowingModeEligible(mActivity));
+ robot.checkIsCameraRunningAndWindowingModeEligible(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT)
public void testIsFreeformLetterboxingForCameraAllowed_optInMechanism_notOptedIn_retFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(false);
+ });
}
@Test
@EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING,
FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT})
public void testIsFreeformLetterboxingForCameraAllowed_notOptedOut_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsFreeformLetterboxingForCameraAllowed_cameraNotRunning_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsFreeformLetterboxingForCameraAllowed_notFreeformWindowing_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(false);
+ });
}
@Test
@@ -237,519 +238,603 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase {
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testIsFreeformLetterboxingForCameraAllowed_optInFreeformCameraRunning_true() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isFreeformLetterboxingForCameraAllowed(mActivity));
+ robot.checkIsFreeformLetterboxingForCameraAllowed(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testFullscreen_doesNotActivateCameraCompatMode() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
- doReturn(false).when(mActivity).inFreeformWindowingMode();
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN);
+ robot.setInFreeformWindowingMode(false);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertNotInCameraCompatMode();
+ robot.assertNotInCameraCompatMode();
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testOrientationUnspecified_doesNotActivateCameraCompatMode() {
- configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
- assertNotInCameraCompatMode();
+ robot.assertNotInCameraCompatMode();
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testNoCameraConnection_doesNotActivateCameraCompatMode() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertNotInCameraCompatMode();
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ robot.assertNotInCameraCompatMode();
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- setDisplayRotation(ROTATION_0);
+ public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_0);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT);
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_PORTRAIT);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- setDisplayRotation(ROTATION_270);
+ public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_270);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() throws Exception {
- configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
- setDisplayRotation(ROTATION_0);
+ public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_0);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT);
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_PORTRAIT);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() throws Exception {
- configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
- setDisplayRotation(ROTATION_270);
+ public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_270);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE);
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_LANDSCAPE_DEVICE_IN_LANDSCAPE);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- setDisplayRotation(ROTATION_270);
+ public void testCameraReconnected_cameraCompatModeAndRefresh() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_270);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity, /* letterboxNew= */ true,
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging(/* letterboxNew= */ true,
/* lastLetterbox= */ false);
- assertActivityRefreshRequested(/* refreshRequested */ true);
- 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);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ true);
+ robot.onCameraClosed(CAMERA_ID_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ // Activity is letterboxed from the previous configuration change.
+ robot.callOnActivityConfigurationChanging(/* letterboxNew= */ true,
+ /* lastLetterbox= */ true);
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
- assertActivityRefreshRequested(/* refreshRequested */ true);
+ robot.assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_PORTRAIT_DEVICE_IN_LANDSCAPE);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testCameraOpenedForDifferentPackage_notInCameraCompatMode() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2);
- assertNotInCameraCompatMode();
+ robot.assertNotInCameraCompatMode();
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@DisableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT)
public void testShouldApplyCameraCompatFreeformTreatment_overrideNotEnabled_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity,
- /* checkOrientation */ true));
+ robot.checkIsCameraCompatTreatmentActiveForTopActivity(false);
+ });
}
@Test
@EnableFlags({FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING,
FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING_OPT_OUT})
public void testShouldApplyCameraCompatFreeformTreatment_notOptedOut_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity,
- /* checkOrientation */ true));
+ robot.checkIsCameraCompatTreatmentActiveForTopActivity(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT)
public void testShouldApplyCameraCompatFreeformTreatment_enabledByOverride_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mActivity.info
- .isChangeEnabled(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT));
- assertTrue(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity,
- /* checkOrientation */ true));
+ robot.checkIsCameraCompatTreatmentActiveForTopActivity(true);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testShouldRefreshActivity_appBoundsChanged_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- Configuration oldConfiguration = createConfiguration(/* letterbox= */ false);
- Configuration newConfiguration = createConfiguration(/* letterbox= */ true);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
- oldConfiguration));
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ robot.checkShouldRefreshActivity(/* expected= */ true,
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0),
+ robot.createConfiguration(/* letterbox= */ false, /* rotation= */ 0));
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testShouldRefreshActivity_displayRotationChanged_returnsTrue() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- Configuration oldConfiguration = createConfiguration(/* letterbox= */ true);
- Configuration newConfiguration = createConfiguration(/* letterbox= */ true);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- oldConfiguration.windowConfiguration.setDisplayRotation(0);
- newConfiguration.windowConfiguration.setDisplayRotation(90);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertTrue(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
- oldConfiguration));
+ robot.checkShouldRefreshActivity(/* expected= */ true,
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 90),
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0));
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testShouldRefreshActivity_appBoundsNorDisplayChanged_returnsFalse() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- Configuration oldConfiguration = createConfiguration(/* letterbox= */ true);
- Configuration newConfiguration = createConfiguration(/* letterbox= */ true);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- oldConfiguration.windowConfiguration.setDisplayRotation(0);
- newConfiguration.windowConfiguration.setDisplayRotation(0);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- assertFalse(mCameraCompatFreeformPolicy.shouldRefreshActivity(mActivity, newConfiguration,
- oldConfiguration));
+ robot.checkShouldRefreshActivity(/* expected= */ false,
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0),
+ robot.createConfiguration(/* letterbox= */ true, /* rotation= */ 0));
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh()
- throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
-
- doReturn(false).when(mActivity.mAppCompatController.getCameraOverrides())
- .shouldRefreshActivityForCameraCompat();
+ public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().setShouldRefreshActivityForCameraCompat(false);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertActivityRefreshRequested(/* refreshRequested */ false);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
- when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
- .thenReturn(false);
+ public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.conf().enableCameraCompatRefreshCycleThroughStop(false);
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
-
- assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ true,
+ /* cycleThroughStop */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp()
- throws Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides())
- .shouldRefreshActivityViaPauseForCameraCompat();
+ public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.setShouldRefreshActivityViaPause(true);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
+ robot.assertActivityRefreshRequested(/* refreshRequested */ true,
+ /* cycleThroughStop */ false);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testGetCameraCompatAspectRatio_activityNotInCameraCompat_returnsDefaultAspRatio() {
- configureActivity(SCREEN_ORIENTATION_FULL_USER);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_FULL_USER);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO,
- mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
- /* delta= */ 0.001);
+ robot.checkCameraCompatAspectRatioEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testGetCameraCompatAspectRatio_activityInCameraCompat_returnsConfigAspectRatio() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- final float configAspectRatio = 1.5f;
- mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio);
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ final float configAspectRatio = 1.5f;
+ robot.conf().setCameraCompatAspectRatio(configAspectRatio);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertEquals(configAspectRatio,
- mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
- /* delta= */ 0.001);
+ robot.checkCameraCompatAspectRatioEquals(configAspectRatio);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
public void testGetCameraCompatAspectRatio_inCameraCompatPerAppOverride_returnDefAspectRatio() {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- final float configAspectRatio = 1.5f;
- mWm.mAppCompatConfiguration.setCameraCompatAspectRatio(configAspectRatio);
- doReturn(true).when(mActivity.mAppCompatController.getCameraOverrides())
- .isOverrideMinAspectRatioForCameraEnabled();
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.conf().setCameraCompatAspectRatio(1.5f);
+ robot.setOverrideMinAspectRatioEnabled(true);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
- callOnActivityConfigurationChanging(mActivity);
+ robot.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.callOnActivityConfigurationChanging();
- assertEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO,
- mCameraCompatFreeformPolicy.getCameraCompatAspectRatio(mActivity),
- /* delta= */ 0.001);
+ robot.checkCameraCompatAspectRatioEquals(MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnCameraOpened_portraitActivity_sandboxesDisplayRotationAndUpdatesApp() throws
- Exception {
- configureActivity(SCREEN_ORIENTATION_PORTRAIT);
- setDisplayRotation(ROTATION_270);
+ public void testOnCameraOpened_portraitActivity_sandboxesDisplayRotationAndUpdatesApp() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_270);
- onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ robot.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).
- assertCompatibilityInfoSentWithDisplayRotation(ROTATION_0);
+ // This is a portrait rotation for a device with portrait natural orientation (most
+ // common, currently the only one supported).
+ robot.assertCompatibilityInfoSentWithDisplayRotation(ROTATION_0);
+ });
}
@Test
@EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING)
@EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT})
- public void testOnCameraOpened_landscapeActivity_sandboxesDisplayRotationAndUpdatesApp() throws
- Exception {
- configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
- setDisplayRotation(ROTATION_0);
-
- 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).
- assertCompatibilityInfoSentWithDisplayRotation(ROTATION_90);
- }
-
- private void setupAppCompatConfiguration() {
- mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration;
- spyOn(mAppCompatConfiguration);
- when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()).thenReturn(true);
- when(mAppCompatConfiguration.isCameraCompatTreatmentEnabledAtBuildTime()).thenReturn(true);
- when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()).thenReturn(true);
- when(mAppCompatConfiguration.isCameraCompatSplitScreenAspectRatioEnabled())
- .thenReturn(false);
- when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
- .thenReturn(true);
- }
-
- private void setupCameraManager() {
- final CameraManager mockCameraManager = mock(CameraManager.class);
- doAnswer(invocation -> {
- mCameraAvailabilityCallback = invocation.getArgument(1);
- return null;
- }).when(mockCameraManager).registerAvailabilityCallback(
- any(Executor.class), any(CameraManager.AvailabilityCallback.class));
-
- when(mContext.getSystemService(CameraManager.class)).thenReturn(mockCameraManager);
- }
-
- private void setupHandler() {
- final Handler handler = mDisplayContent.mWmService.mH;
- spyOn(handler);
-
- when(handler.postDelayed(any(Runnable.class), anyLong())).thenAnswer(
- invocation -> {
- ((Runnable) invocation.getArgument(0)).run();
- return null;
- });
- }
-
- private void configureActivity(@ScreenOrientation int activityOrientation) {
- configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM);
- }
-
- private void configureActivity(@ScreenOrientation int activityOrientation,
- @WindowingMode int windowingMode) {
- configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT, windowingMode);
- }
-
- private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation,
- @Orientation int naturalOrientation, @WindowingMode int windowingMode) {
- setupDisplayContent(naturalOrientation);
- final Task task = setupTask(windowingMode);
- setupActivity(task, activityOrientation, windowingMode);
- setupMockApplicationThread();
-
- mCameraCompatFreeformPolicy = mDisplayContent.mAppCompatCameraPolicy
- .mCameraCompatFreeformPolicy;
- }
-
- private void setupDisplayContent(@Orientation int naturalOrientation) {
- // Create a new DisplayContent so that the flag values create the camera freeform policy.
- mDisplayContent = new TestDisplayContent.Builder(mAtm, mDisplayContent.getSurfaceWidth(),
- mDisplayContent.getSurfaceHeight()).build();
- mDisplayContent.setIgnoreOrientationRequest(true);
- setDisplayRotation(ROTATION_90);
- doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation();
- }
-
- private Task setupTask(@WindowingMode int windowingMode) {
- final TaskDisplayArea tda = mDisplayContent.getDefaultTaskDisplayArea();
- spyOn(tda);
- doReturn(true).when(tda).supportsNonResizableMultiWindow();
-
- final Task task = new TaskBuilder(mSupervisor)
- .setDisplay(mDisplayContent)
- .setWindowingMode(windowingMode)
- .build();
- task.setBounds(0, 0, 1000, 500);
- return task;
- }
-
- private void setupActivity(@NonNull Task task, @ScreenOrientation int activityOrientation,
- @WindowingMode int windowingMode) {
- mActivity = new ActivityBuilder(mAtm)
- // Set the component to be that of the test class in order to enable compat changes
- .setComponent(ComponentName.createRelative(mContext,
- com.android.server.wm.CameraCompatFreeformPolicyTests.class.getName()))
- .setScreenOrientation(activityOrientation)
- .setResizeMode(RESIZE_MODE_RESIZEABLE)
- .setCreateTask(true)
- .setOnTop(true)
- .setTask(task)
- .build();
- mActivity.mAppCompatController.getSizeCompatModePolicy().clearSizeCompatMode();
-
- spyOn(mActivity.mAppCompatController.getCameraOverrides());
- spyOn(mActivity.info);
-
- doReturn(mActivity).when(mDisplayContent).topRunningActivity(anyBoolean());
- doReturn(windowingMode == WINDOWING_MODE_FREEFORM).when(mActivity)
- .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));
- }
-
- private void assertNotInCameraCompatMode() {
- assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_NONE);
- }
-
- private void assertActivityRefreshRequested(boolean refreshRequested) throws Exception {
- assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true);
- }
-
- private void assertActivityRefreshRequested(boolean refreshRequested,
- boolean cycleThroughStop) throws Exception {
- verify(mActivity.mAppCompatController.getCameraOverrides(),
- times(refreshRequested ? 1 : 0)).setIsRefreshRequested(true);
-
- final RefreshCallbackItem refreshCallbackItem =
- new RefreshCallbackItem(mActivity.token, cycleThroughStop ? ON_STOP : ON_PAUSE);
- final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(mActivity.token,
- /* isForward */ false, /* shouldSendCompatFakeFocus */ false);
-
- verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0))
- .scheduleTransactionItems(mActivity.app.getThread(),
- refreshCallbackItem, resumeActivityItem);
- }
-
- private void callOnActivityConfigurationChanging(ActivityRecord activity) {
- callOnActivityConfigurationChanging(activity, /* letterboxNew= */ true,
- /* lastLetterbox= */false);
- }
-
- private void callOnActivityConfigurationChanging(ActivityRecord activity, boolean letterboxNew,
- boolean lastLetterbox) {
- mDisplayContent.mAppCompatCameraPolicy.mActivityRefresher
- .onActivityConfigurationChanging(activity,
- /* newConfig */ createConfiguration(letterboxNew),
- /* lastReportedConfig */ createConfiguration(lastLetterbox));
- }
-
- private Configuration createConfiguration(boolean letterbox) {
- final Configuration configuration = new Configuration();
- Rect bounds = letterbox ? new Rect(/*left*/ 300, /*top*/ 0, /*right*/ 700, /*bottom*/ 600)
- : new Rect(/*left*/ 0, /*top*/ 0, /*right*/ 1000, /*bottom*/ 600);
- configuration.windowConfiguration.setAppBounds(bounds);
- return configuration;
- }
-
- private void setDisplayRotation(@Surface.Rotation int displayRotation) {
- doAnswer(invocation -> {
- DisplayInfo displayInfo = new DisplayInfo();
- mDisplayContent.getDisplay().getDisplayInfo(displayInfo);
- displayInfo.rotation = displayRotation;
- // Set height so that the natural orientation (rotation is 0) is portrait. This is the
- // case for most standard phones and tablets.
- // TODO(b/365725400): handle landscape natural orientation.
- displayInfo.logicalHeight = displayRotation % 180 == 0 ? 800 : 600;
- displayInfo.logicalWidth = displayRotation % 180 == 0 ? 600 : 800;
- return displayInfo;
- }).when(mDisplayContent.mWmService.mDisplayManagerInternal)
- .getDisplayInfo(anyInt());
- }
-
- private void setupMockApplicationThread() {
- IApplicationThread mockApplicationThread = mock(IApplicationThread.class);
- spyOn(mActivity.app);
- doReturn(mockApplicationThread).when(mActivity.app).getThread();
- }
-
- private void assertCompatibilityInfoSentWithDisplayRotation(@Surface.Rotation int
- expectedRotation) throws Exception {
- final ArgumentCaptor<CompatibilityInfo> compatibilityInfoArgumentCaptor =
- ArgumentCaptor.forClass(CompatibilityInfo.class);
- verify(mActivity.app.getThread()).updatePackageCompatibilityInfo(eq(mActivity.packageName),
- compatibilityInfoArgumentCaptor.capture());
-
- final CompatibilityInfo compatInfo = compatibilityInfoArgumentCaptor.getValue();
- assertTrue(compatInfo.isOverrideDisplayRotationRequired());
- assertEquals(expectedRotation, compatInfo.applicationDisplayRotation);
+ public void testOnCameraOpened_landscapeActivity_sandboxesDisplayRotationAndUpdatesApp() {
+ runTestScenario((robot) -> {
+ robot.configureActivity(SCREEN_ORIENTATION_LANDSCAPE);
+ robot.activity().rotateDisplayForTopActivity(ROTATION_0);
+
+ robot.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).
+ robot.assertCompatibilityInfoSentWithDisplayRotation(ROTATION_90);
+ });
+ }
+
+ /**
+ * Runs a test scenario providing a Robot.
+ */
+ void runTestScenario(@NonNull Consumer<CameraCompatFreeformPolicyRobotTests> consumer) {
+ final CameraCompatFreeformPolicyRobotTests robot =
+ new CameraCompatFreeformPolicyRobotTests(mWm, mAtm, mSupervisor, this);
+ consumer.accept(robot);
+ }
+
+ private static class CameraCompatFreeformPolicyRobotTests extends AppCompatRobotBase {
+ private final WindowTestsBase mWindowTestsBase;
+
+ private CameraManager.AvailabilityCallback mCameraAvailabilityCallback;
+
+ CameraCompatFreeformPolicyRobotTests(@NonNull WindowManagerService wm,
+ @NonNull ActivityTaskManagerService atm,
+ @NonNull ActivityTaskSupervisor supervisor,
+ @NonNull WindowTestsBase windowTestsBase) {
+ super(wm, atm, supervisor);
+ mWindowTestsBase = windowTestsBase;
+ setupCameraManager();
+ setupAppCompatConfiguration();
+ }
+
+ @Override
+ void onPostDisplayContentCreation(@NonNull DisplayContent displayContent) {
+ super.onPostDisplayContentCreation(displayContent);
+ spyOn(displayContent.mAppCompatCameraPolicy);
+ if (displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy != null) {
+ spyOn(displayContent.mAppCompatCameraPolicy.mCameraCompatFreeformPolicy);
+ }
+ }
+
+ @Override
+ void onPostActivityCreation(@NonNull ActivityRecord activity) {
+ super.onPostActivityCreation(activity);
+ setupCameraManager();
+ setupHandler();
+ setupMockApplicationThread();
+ }
+
+ private void setupMockApplicationThread() {
+ IApplicationThread mockApplicationThread = mock(IApplicationThread.class);
+ spyOn(activity().top().app);
+ doReturn(mockApplicationThread).when(activity().top().app).getThread();
+ }
+
+ private Configuration createConfiguration(boolean letterbox, int rotation) {
+ final Configuration configuration = createConfiguration(letterbox);
+ configuration.windowConfiguration.setDisplayRotation(rotation);
+ return configuration;
+ }
+
+ private Configuration createConfiguration(boolean letterbox) {
+ final Configuration configuration = new Configuration();
+ Rect bounds = letterbox ? new Rect(/*left*/ 300, /*top*/ 0, /*right*/ 700, /*bottom*/
+ 600)
+ : new Rect(/*left*/ 0, /*top*/ 0, /*right*/ 1000, /*bottom*/ 600);
+ configuration.windowConfiguration.setAppBounds(bounds);
+ return configuration;
+ }
+
+ private void setupAppCompatConfiguration() {
+ applyOnConf((c) -> {
+ c.enableCameraCompatTreatment(true);
+ c.enableCameraCompatTreatmentAtBuildTime(true);
+ c.enableCameraCompatRefresh(true);
+ c.enableCameraCompatRefreshCycleThroughStop(true);
+ c.enableCameraCompatSplitScreenAspectRatio(false);
+ });
+ }
+
+ private void setupCameraManager() {
+ final CameraManager mockCameraManager = mock(CameraManager.class);
+ doAnswer(invocation -> {
+ mCameraAvailabilityCallback = invocation.getArgument(1);
+ return null;
+ }).when(mockCameraManager).registerAvailabilityCallback(
+ any(Executor.class), any(CameraManager.AvailabilityCallback.class));
+
+ doReturn(mockCameraManager).when(mWindowTestsBase.mWm.mContext).getSystemService(
+ CameraManager.class);
+ }
+
+ private void setupHandler() {
+ final Handler handler = activity().top().mWmService.mH;
+ spyOn(handler);
+
+ doAnswer(invocation -> {
+ ((Runnable) invocation.getArgument(0)).run();
+ return null;
+ }).when(handler).postDelayed(any(Runnable.class), anyLong());
+ }
+
+ private void configureActivity(@ScreenOrientation int activityOrientation) {
+ configureActivity(activityOrientation, WINDOWING_MODE_FREEFORM);
+ }
+
+ private void configureActivity(@ScreenOrientation int activityOrientation,
+ @WindowingMode int windowingMode) {
+ configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT, windowingMode);
+ }
+
+ private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation,
+ @Orientation int naturalOrientation, @WindowingMode int windowingMode) {
+ applyOnActivity(a -> {
+ dw().allowEnterDesktopMode(true);
+ a.createActivityWithComponentInNewTaskAndDisplay();
+ a.setIgnoreOrientationRequest(true);
+ a.rotateDisplayForTopActivity(ROTATION_90);
+ a.configureTopActivity(/* minAspect */ -1, /* maxAspect */ -1,
+ activityOrientation, /* isUnresizable */ false);
+ a.top().setWindowingMode(windowingMode);
+ a.displayContent().setWindowingMode(windowingMode);
+ a.setDisplayNaturalOrientation(naturalOrientation);
+ spyOn(a.top().mAppCompatController.getCameraOverrides());
+ spyOn(a.top().info);
+ doReturn(a.displayContent().getDisplayInfo()).when(
+ a.displayContent().mWmService.mDisplayManagerInternal).getDisplayInfo(
+ a.displayContent().mDisplayId);
+ });
+ }
+
+ private void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) {
+ mCameraAvailabilityCallback.onCameraOpened(cameraId, packageName);
+ waitHandlerIdle();
+ }
+
+ private void onCameraClosed(@NonNull String cameraId) {
+ mCameraAvailabilityCallback.onCameraClosed(cameraId);
+ }
+
+ private void waitHandlerIdle() {
+ mWindowTestsBase.waitHandlerIdle(activity().displayContent().mWmService.mH);
+ }
+
+ void setInFreeformWindowingMode(boolean inFreeform) {
+ doReturn(inFreeform).when(activity().top()).inFreeformWindowingMode();
+ }
+
+ void setShouldRefreshActivityViaPause(boolean enabled) {
+ doReturn(enabled).when(activity().top().mAppCompatController.getCameraOverrides())
+ .shouldRefreshActivityViaPauseForCameraCompat();
+ }
+
+ void checkShouldRefreshActivity(boolean expected, Configuration newConfig,
+ Configuration oldConfig) {
+ assertEquals(expected, cameraCompatFreeformPolicy().shouldRefreshActivity(
+ activity().top(), newConfig, oldConfig));
+ }
+
+ void checkCameraCompatPolicyNotCreated() {
+ assertNull(cameraCompatFreeformPolicy());
+ }
+
+ void checkIsCameraRunningAndWindowingModeEligible(boolean expected) {
+ assertEquals(expected, cameraCompatFreeformPolicy()
+ .isCameraRunningAndWindowingModeEligible(activity().top()));
+ }
+
+ void checkIsFreeformLetterboxingForCameraAllowed(boolean expected) {
+ assertEquals(expected, cameraCompatFreeformPolicy()
+ .isFreeformLetterboxingForCameraAllowed(activity().top()));
+ }
+
+ void checkCameraCompatAspectRatioEquals(float aspectRatio) {
+ assertEquals(aspectRatio,
+ cameraCompatFreeformPolicy().getCameraCompatAspectRatio(activity().top()),
+ /* delta= */ 0.001);
+ }
+
+ private void assertInCameraCompatMode(@FreeformCameraCompatMode int mode) {
+ assertEquals(mode, cameraCompatFreeformPolicy().getCameraCompatMode(activity().top()));
+ }
+
+ private void assertNotInCameraCompatMode() {
+ assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_NONE);
+ }
+
+ private void assertActivityRefreshRequested(boolean refreshRequested) {
+ assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true);
+ }
+
+ private void assertActivityRefreshRequested(boolean refreshRequested,
+ boolean cycleThroughStop) {
+ verify(activity().top().mAppCompatController.getCameraOverrides(),
+ times(refreshRequested ? 1 : 0)).setIsRefreshRequested(true);
+
+ final RefreshCallbackItem refreshCallbackItem =
+ new RefreshCallbackItem(activity().top().token,
+ cycleThroughStop ? ON_STOP : ON_PAUSE);
+ final ResumeActivityItem resumeActivityItem = new ResumeActivityItem(
+ activity().top().token,
+ /* isForward */ false, /* shouldSendCompatFakeFocus */ false);
+ try {
+ verify(activity().top().mAtmService.getLifecycleManager(),
+ times(refreshRequested ? 1 : 0))
+ .scheduleTransactionItems(activity().top().app.getThread(),
+ refreshCallbackItem, resumeActivityItem);
+ } catch (RemoteException e) {
+ fail(e.getMessage());
+ }
+ }
+
+ private void callOnActivityConfigurationChanging() {
+ callOnActivityConfigurationChanging(/* letterboxNew= */ true,
+ /* lastLetterbox= */false);
+ }
+
+ private void callOnActivityConfigurationChanging(boolean letterboxNew,
+ boolean lastLetterbox) {
+ activity().displayContent().mAppCompatCameraPolicy.mActivityRefresher
+ .onActivityConfigurationChanging(activity().top(),
+ /* newConfig */ createConfiguration(letterboxNew),
+ /* lastReportedConfig */ createConfiguration(lastLetterbox));
+ }
+
+ void checkIsCameraCompatTreatmentActiveForTopActivity(boolean active) {
+ assertEquals(active,
+ cameraCompatFreeformPolicy().isTreatmentEnabledForActivity(activity().top(),
+ /* checkOrientation */ true));
+ }
+
+ void setOverrideMinAspectRatioEnabled(boolean enabled) {
+ doReturn(enabled).when(activity().top().mAppCompatController.getCameraOverrides())
+ .isOverrideMinAspectRatioForCameraEnabled();
+ }
+
+ void assertCompatibilityInfoSentWithDisplayRotation(@Surface.Rotation int
+ expectedRotation) {
+ final ArgumentCaptor<CompatibilityInfo> compatibilityInfoArgumentCaptor =
+ ArgumentCaptor.forClass(CompatibilityInfo.class);
+ try {
+ verify(activity().top().app.getThread()).updatePackageCompatibilityInfo(
+ eq(activity().top().packageName),
+ compatibilityInfoArgumentCaptor.capture());
+ } catch (RemoteException e) {
+ fail(e.getMessage());
+ }
+
+ final CompatibilityInfo compatInfo = compatibilityInfoArgumentCaptor.getValue();
+ assertTrue(compatInfo.isOverrideDisplayRotationRequired());
+ assertEquals(expectedRotation, compatInfo.applicationDisplayRotation);
+ }
+
+ CameraCompatFreeformPolicy cameraCompatFreeformPolicy() {
+ return activity().displayContent().mAppCompatCameraPolicy.mCameraCompatFreeformPolicy;
+ }
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
index 1e91bedb5c18..43755ea3165e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java
@@ -181,6 +181,7 @@ public class DesktopModeHelperTest {
assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue();
}
+ @DisableFlags(Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE)
@Test
public void isDeviceEligibleForDesktopMode_configDEModeOffAndIntDispHostsDesktop_returnsFalse() {
doReturn(true).when(mMockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported));
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 bc37496d14a7..e87e107cd793 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java
@@ -21,6 +21,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_MEDIUM_VALUE;
import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_SMALL_VALUE;
@@ -157,7 +158,7 @@ public class DesktopModeLaunchParamsModifierTests extends
@Test
@EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
Flags.FLAG_DISABLE_DESKTOP_LAUNCH_PARAMS_OUTSIDE_DESKTOP_BUG_FIX})
- public void testReturnsContinueIfVisibleFreeformTaskExists() {
+ public void testReturnsContinueIfFreeformTaskExists() {
setupDesktopModeLaunchParamsModifier();
when(mTarget.isEnteringDesktopMode(any(), any(), any())).thenCallRealMethod();
@@ -165,7 +166,7 @@ public class DesktopModeLaunchParamsModifierTests extends
final Task existingFreeformTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
.setWindowingMode(WINDOWING_MODE_FREEFORM).build();
doReturn(existingFreeformTask.getRootActivity()).when(dc)
- .getTopMostVisibleFreeformActivity();
+ .getTopMostFreeformActivity();
final Task launchingTask = new TaskBuilder(mSupervisor).build();
launchingTask.onDisplayChanged(dc);
@@ -269,6 +270,38 @@ public class DesktopModeLaunchParamsModifierTests extends
}
@Test
+ @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES})
+ public void testInheritTaskBoundsFromExistingInstanceIfClosing() {
+ setupDesktopModeLaunchParamsModifier();
+
+ final String packageName = "com.same.package";
+ // Setup existing task.
+ final DisplayContent dc = spy(createNewDisplay());
+ final Task existingFreeformTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
+ .setWindowingMode(WINDOWING_MODE_FREEFORM).setPackage(packageName).build();
+ existingFreeformTask.setBounds(
+ /* left */ 0,
+ /* top */ 0,
+ /* right */ 500,
+ /* bottom */ 500);
+ doReturn(existingFreeformTask.getRootActivity()).when(dc)
+ .getTopMostVisibleFreeformActivity();
+ // Set up new instance of already existing task. By default multi instance is not supported
+ // so first instance will close.
+ final Task launchingTask = new TaskBuilder(mSupervisor).setPackage(packageName)
+ .setCreateActivity(true).build();
+ launchingTask.onDisplayChanged(dc);
+ launchingTask.intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
+
+ // New instance should inherit task bounds of old instance.
+ assertEquals(RESULT_DONE,
+ new CalculateRequestBuilder().setTask(launchingTask)
+ .setActivity(launchingTask.getRootActivity()).calculate());
+ assertEquals(existingFreeformTask.getBounds(), mResult.mBounds);
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
@DisableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
public void testUsesDesiredBoundsIfEmptyLayoutAndActivityOptionsBounds() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java
new file mode 100644
index 000000000000..1445a6982c60
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayCompatTests.java
@@ -0,0 +1,71 @@
+/*
+ * 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.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.window.flags.Flags.FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS;
+
+import static junit.framework.Assert.assertFalse;
+
+import static org.junit.Assert.assertTrue;
+
+import android.compat.testing.PlatformCompatChangeRule;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.annotations.Presubmit;
+import android.view.DisplayInfo;
+
+import androidx.test.filters.MediumTest;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+/**
+ * Build/Install/Run:
+ * atest WmTests:DisplayCompatTests
+ */
+@MediumTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public class DisplayCompatTests extends WindowTestsBase {
+
+ @Rule
+ public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
+ @EnableFlags(FLAG_ENABLE_RESTART_MENU_FOR_CONNECTED_DISPLAYS)
+ @Test
+ public void testFixedMiscConfigurationWhenMovingToDisplay() {
+ // Create an app on the default display, at which point the restart menu isn't enabled.
+ final Task task = createTask(mDefaultDisplay);
+ final ActivityRecord activity = createActivityRecord(task);
+ assertFalse(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove());
+
+ // Move the app to a secondary display, and the restart menu must get enabled.
+ final DisplayInfo displayInfo = new DisplayInfo();
+ displayInfo.copyFrom(mDisplayInfo);
+ displayInfo.displayId = DEFAULT_DISPLAY + 1;
+ final DisplayContent secondaryDisplay = createNewDisplay(displayInfo);
+ task.reparent(secondaryDisplay.getDefaultTaskDisplayArea(), true);
+ assertTrue(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove());
+
+ // Once the app gets restarted, the restart menu must be gone.
+ activity.restartProcessIfVisible();
+ assertFalse(task.getTaskInfo().appCompatTaskInfo.isRestartMenuEnabledForDisplayMove());
+ }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
index 71e34ef220d3..3c6a89842af9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/InsetsPolicyTest.java
@@ -93,7 +93,7 @@ public class InsetsPolicyTest extends WindowTestsBase {
final Task task1 = createTask(mDisplayContent);
final Task task2 = createTask(mDisplayContent);
- task1.setAdjacentTaskFragment(task2);
+ task1.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(task1, task2));
final WindowState win = createAppWindow(task1, WINDOWING_MODE_MULTI_WINDOW, "app");
final InsetsSourceControl[] controls = addWindowAndGetControlsForDispatch(win);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 33a48aadbd70..e2c4a1d2dfea 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -572,8 +572,8 @@ public class SizeCompatTests extends WindowTestsBase {
new TestDisplayContent.Builder(mAtm, 1000, 2000).build();
final InputDevice device = new InputDevice.Builder()
.setAssociatedDisplayId(newDisplay.mDisplayId)
- .setSources(InputDevice.SOURCE_TOUCHSCREEN | InputDevice.SOURCE_TRACKBALL
- | InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+ .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC)
+ .setSources(InputDevice.SOURCE_TOUCHSCREEN | InputDevice.SOURCE_TRACKBALL)
.build();
final InputDevice[] devices = {device};
doReturn(true).when(newDisplay.mWmService.mInputManager)
@@ -596,6 +596,7 @@ public class SizeCompatTests extends WindowTestsBase {
assertEquals(originalTouchscreen, newConfiguration.touchscreen);
assertEquals(originalNavigation, newConfiguration.navigation);
assertEquals(originalKeyboard, newConfiguration.keyboard);
+ // TODO(b/399749909): assert keyboardHidden, hardkeyboardHidden, and navigationHidden too.
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index 3776b03695d5..b558fad84efa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -78,6 +78,7 @@ import android.view.SurfaceControl;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.internal.os.BackgroundThread;
+import com.android.internal.protolog.PerfettoProtoLogImpl;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.protolog.WmProtoLogGroups;
import com.android.server.AnimationThread;
@@ -187,7 +188,10 @@ public class SystemServicesTestRule implements TestRule {
}
private void setUp() {
- ProtoLog.init(WmProtoLogGroups.values());
+ if (ProtoLog.getSingleInstance() == null) {
+ ProtoLog.init(WmProtoLogGroups.values());
+ PerfettoProtoLogImpl.waitForInitialization();
+ }
if (mOnBeforeServicesCreated != null) {
mOnBeforeServicesCreated.run();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
index 986532ce5897..ec83c50e95aa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
@@ -87,7 +87,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
adjacentRootTask.mCreatedByOrganizer = true;
final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea();
- adjacentRootTask.setAdjacentTaskFragment(rootTask);
+ adjacentRootTask.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(adjacentRootTask, rootTask));
taskDisplayArea.setLaunchAdjacentFlagRootTask(adjacentRootTask);
Task actualRootTask = taskDisplayArea.getLaunchRootTask(
@@ -113,7 +114,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
final Task adjacentRootTask = createTask(
mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
adjacentRootTask.mCreatedByOrganizer = true;
- adjacentRootTask.setAdjacentTaskFragment(rootTask);
+ adjacentRootTask.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(adjacentRootTask, rootTask));
taskDisplayArea.setLaunchRootTask(rootTask,
new int[]{WINDOWING_MODE_MULTI_WINDOW}, new int[]{ACTIVITY_TYPE_STANDARD});
@@ -135,7 +137,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
adjacentRootTask.mCreatedByOrganizer = true;
createActivityRecord(adjacentRootTask);
final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea();
- adjacentRootTask.setAdjacentTaskFragment(rootTask);
+ adjacentRootTask.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(adjacentRootTask, rootTask));
taskDisplayArea.setLaunchAdjacentFlagRootTask(adjacentRootTask);
final Task actualRootTask = taskDisplayArea.getLaunchRootTask(
@@ -821,7 +824,8 @@ public class TaskDisplayAreaTests extends WindowTestsBase {
adjacentRootTask.mCreatedByOrganizer = true;
final Task candidateTask = createTaskInRootTask(rootTask, 0 /* userId*/);
final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea();
- adjacentRootTask.setAdjacentTaskFragment(rootTask);
+ adjacentRootTask.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(adjacentRootTask, rootTask));
// Verify the launch root with candidate task
Task actualRootTask = taskDisplayArea.getLaunchRootTask(WINDOWING_MODE_UNDEFINED,
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index ab76ae8e378a..76660bdc7355 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -784,7 +784,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
.setFragmentToken(fragmentToken2)
.build();
mWindowOrganizerController.mLaunchTaskFragments.put(fragmentToken2, taskFragment2);
- mTaskFragment.setAdjacentTaskFragment(taskFragment2);
+ mTaskFragment.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(mTaskFragment, taskFragment2));
mTransaction.clearAdjacentTaskFragments(mFragmentToken);
mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE,
@@ -1267,7 +1268,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
}
@Test
- public void testTaskFragmentInPip_setAdjacentTaskFragment() {
+ public void testTaskFragmentInPip_setAdjacentTaskFragments() {
setupTaskFragmentInPip();
spyOn(mWindowOrganizerController);
@@ -1279,7 +1280,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase {
verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer),
eq(mErrorToken), eq(mTaskFragment), eq(OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS),
any(IllegalArgumentException.class));
- verify(mTaskFragment, never()).setAdjacentTaskFragment(any());
+ verify(mTaskFragment, never()).setAdjacentTaskFragments(any());
}
@Test
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 cc2a76dcc9f2..7c1d7fec819b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -67,7 +67,6 @@ import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Binder;
-import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.view.SurfaceControl;
import android.view.View;
@@ -363,7 +362,7 @@ public class TaskFragmentTest extends WindowTestsBase {
doReturn(true).when(primaryActivity).supportsPictureInPicture();
doReturn(false).when(secondaryActivity).supportsPictureInPicture();
- primaryTf.setAdjacentTaskFragment(secondaryTf);
+ primaryTf.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primaryTf, secondaryTf));
primaryActivity.setState(RESUMED, "test");
secondaryActivity.setState(RESUMED, "test");
@@ -390,7 +389,8 @@ public class TaskFragmentTest extends WindowTestsBase {
task.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
taskFragment0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
taskFragment0.setBounds(taskFragmentBounds);
- taskFragment0.setAdjacentTaskFragment(taskFragment1);
+ taskFragment0.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(taskFragment0, taskFragment1));
taskFragment0.setCompanionTaskFragment(taskFragment1);
taskFragment0.setAnimationParams(new TaskFragmentAnimationParams.Builder()
.setAnimationBackgroundColor(Color.GREEN)
@@ -779,7 +779,7 @@ public class TaskFragmentTest extends WindowTestsBase {
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.build();
- tf0.setAdjacentTaskFragment(tf1);
+ tf0.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf0, tf1));
tf0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
tf1.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
task.setBounds(0, 0, 1200, 1000);
@@ -834,7 +834,7 @@ public class TaskFragmentTest extends WindowTestsBase {
final Task task = createTask(mDisplayContent);
final TaskFragment tf0 = createTaskFragmentWithActivity(task);
final TaskFragment tf1 = createTaskFragmentWithActivity(task);
- tf0.setAdjacentTaskFragment(tf1);
+ tf0.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(tf0, tf1));
tf0.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
tf1.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
task.setBounds(0, 0, 1200, 1000);
@@ -982,7 +982,8 @@ public class TaskFragmentTest extends WindowTestsBase {
.setOrganizer(mOrganizer)
.setFragmentToken(new Binder())
.build();
- taskFragmentLeft.setAdjacentTaskFragment(taskFragmentRight);
+ taskFragmentLeft.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(taskFragmentLeft, taskFragmentRight));
taskFragmentLeft.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
taskFragmentRight.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
task.setBounds(0, 0, 1200, 1000);
@@ -1051,8 +1052,8 @@ public class TaskFragmentTest extends WindowTestsBase {
.setParentTask(task)
.createActivityCount(1)
.build();
- taskFragmentRight.setAdjacentTaskFragment(taskFragmentLeft);
- taskFragmentLeft.setAdjacentTaskFragment(taskFragmentRight);
+ taskFragmentRight.setAdjacentTaskFragments(
+ new TaskFragment.AdjacentSet(taskFragmentLeft, taskFragmentRight));
final ActivityRecord appLeftTop = taskFragmentLeft.getTopMostActivity();
final ActivityRecord appRightTop = taskFragmentRight.getTopMostActivity();
@@ -1103,7 +1104,6 @@ public class TaskFragmentTest extends WindowTestsBase {
Math.min(outConfig.screenWidthDp, outConfig.screenHeightDp));
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testAdjacentSetForTaskFragments() {
final Task task = createTask(mDisplayContent);
@@ -1119,7 +1119,6 @@ public class TaskFragmentTest extends WindowTestsBase {
() -> new TaskFragment.AdjacentSet(tf0, tf1, tf2));
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testSetAdjacentTaskFragments() {
final Task task0 = createTask(mDisplayContent);
@@ -1148,7 +1147,6 @@ public class TaskFragmentTest extends WindowTestsBase {
assertFalse(task2.hasAdjacentTaskFragment());
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testClearAdjacentTaskFragments() {
final Task task0 = createTask(mDisplayContent);
@@ -1167,7 +1165,6 @@ public class TaskFragmentTest extends WindowTestsBase {
assertFalse(task2.hasAdjacentTaskFragment());
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testRemoveFromAdjacentTaskFragments() {
final Task task0 = createTask(mDisplayContent);
@@ -1190,7 +1187,6 @@ public class TaskFragmentTest extends WindowTestsBase {
assertFalse(task1.isAdjacentTo(task1));
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testRemoveFromAdjacentTaskFragmentsWhenRemove() {
final Task task0 = createTask(mDisplayContent);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 724d7e7c111c..044aacc4b988 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -1750,8 +1750,7 @@ public class TaskTests extends WindowTestsBase {
primary.mVisibleRequested = true;
secondary.mVisibleRequested = true;
- primary.setAdjacentTaskFragment(secondary);
- secondary.setAdjacentTaskFragment(primary);
+ primary.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(primary, secondary));
primary.setEmbeddedDimArea(EMBEDDED_DIM_AREA_PARENT_TASK);
doReturn(true).when(primary).shouldBoostDimmer();
task.assignChildLayers(t);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 7030d986494f..5401a44d7016 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -910,7 +910,7 @@ public class WindowOrganizerTests extends WindowTestsBase {
final RunningTaskInfo info2 = task2.getTaskInfo();
WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.setAdjacentRoots(info1.token, info2.token);
+ wct.setAdjacentRootSet(info1.token, info2.token);
mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
assertTrue(task1.isAdjacentTo(task2));
assertTrue(task2.isAdjacentTo(task1));
@@ -929,7 +929,6 @@ public class WindowOrganizerTests extends WindowTestsBase {
assertEquals(dc.getDefaultTaskDisplayArea().mLaunchAdjacentFlagRootTask, null);
}
- @EnableFlags(Flags.FLAG_ALLOW_MULTIPLE_ADJACENT_TASK_FRAGMENTS)
@Test
public void testSetAdjacentLaunchRootSet() {
final DisplayContent dc = mWm.mRoot.getDisplayContent(Display.DEFAULT_DISPLAY);
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 57ab13ffee89..471b065aebb4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -122,7 +122,6 @@ import android.window.WindowContainerTransaction;
import com.android.internal.policy.AttributeCache;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.server.wallpaper.WallpaperCropper.WallpaperCropUtils;
import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry;
import org.junit.After;
@@ -289,18 +288,6 @@ public class WindowTestsBase extends SystemServiceTestsBase {
mAtm.mWindowManager.mAppCompatConfiguration
.setIsDisplayAspectRatioEnabledForFixedOrientationLetterbox(false);
- // Setup WallpaperController crop utils with a simple center-align strategy
- WallpaperCropUtils cropUtils = (displaySize, bitmapSize, suggestedCrops, rtl) -> {
- Rect crop = new Rect(0, 0, displaySize.x, displaySize.y);
- crop.scale(Math.min(
- ((float) bitmapSize.x) / displaySize.x,
- ((float) bitmapSize.y) / displaySize.y));
- crop.offset((bitmapSize.x - crop.width()) / 2, (bitmapSize.y - crop.height()) / 2);
- return crop;
- };
- mDisplayContent.mWallpaperController.setWallpaperCropUtils(cropUtils);
- mDefaultDisplay.mWallpaperController.setWallpaperCropUtils(cropUtils);
-
checkDeviceSpecificOverridesNotApplied();
}
@@ -1890,7 +1877,7 @@ public class WindowTestsBase extends SystemServiceTestsBase {
mSecondary = mService.mTaskOrganizerController.createRootTask(
display, WINDOWING_MODE_MULTI_WINDOW, null);
- mPrimary.setAdjacentTaskFragment(mSecondary);
+ mPrimary.setAdjacentTaskFragments(new TaskFragment.AdjacentSet(mPrimary, mSecondary));
display.getDefaultTaskDisplayArea().setLaunchAdjacentFlagRootTask(mSecondary);
final Rect primaryBounds = new Rect();
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index fbba999bfb36..14d567d141cb 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -3371,14 +3371,13 @@ public class TelephonyManager {
return telephony.getDataNetworkTypeForSubscriber(subId, getOpPackageName(),
getAttributionTag());
} else {
- // This can happen when the ITelephony interface is not up yet.
+ Log.e(TAG, "getDataNetworkType: ITelephony interface is not up yet");
return NETWORK_TYPE_UNKNOWN;
}
- } catch(RemoteException ex) {
- // This shouldn't happen in the normal case
- return NETWORK_TYPE_UNKNOWN;
- } catch (NullPointerException ex) {
- // This could happen before phone restarts due to crashing
+ } catch (RemoteException // Shouldn't happen in the normal case
+ | NullPointerException ex // Could happen before phone restarts due to crashing
+ ) {
+ Log.e(TAG, "getDataNetworkType: " + ex.getMessage());
return NETWORK_TYPE_UNKNOWN;
}
}
diff --git a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
index 685ae9a5fef2..c5e7188e6efe 100644
--- a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
index 5f92d7fe830b..22bd458e2751 100644
--- a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
index 1b90e99a8ba2..541ce26d1435 100644
--- a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
index ffdbb02984a7..d2e02193f0fb 100644
--- a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml
index ac704e5e7c39..e112c82f0661 100644
--- a/tests/FlickerTests/IME/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml
@@ -49,6 +49,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/Notification/AndroidTestTemplate.xml b/tests/FlickerTests/Notification/AndroidTestTemplate.xml
index e2ac5a9579ae..e5700c03cf77 100644
--- a/tests/FlickerTests/Notification/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/Notification/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
index 1a4feb6e9eca..4c41a4c01180 100644
--- a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
index 1b2007deae27..27fc249e36b9 100644
--- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
<!-- Disable AOD -->
<option name="run-command" value="settings put secure doze_always_on 0"/>
+ <!-- Disable explore hub mode -->
+ <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
index e99c81493394..794fd0255726 100644
--- a/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
+++ b/tests/Input/src/android/hardware/input/KeyGestureEventHandlerTest.kt
@@ -214,9 +214,5 @@ class KeyGestureEventHandlerTest {
): Boolean {
return handler(event, focusedToken)
}
-
- override fun isKeyGestureSupported(gestureType: Int): Boolean {
- return true
- }
}
}
diff --git a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt
index 044f11d6904c..890c346ea015 100644
--- a/tests/Input/src/com/android/server/input/BatteryControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/BatteryControllerTests.kt
@@ -184,6 +184,8 @@ class BatteryControllerTests {
@get:Rule
val rule = MockitoJUnit.rule()!!
@get:Rule
+ val context = TestableContext(ApplicationProvider.getApplicationContext())
+ @get:Rule
val inputManagerRule = MockInputManagerRule()
@Mock
@@ -194,7 +196,6 @@ class BatteryControllerTests {
private lateinit var bluetoothBatteryManager: BluetoothBatteryManager
private lateinit var batteryController: BatteryController
- private lateinit var context: TestableContext
private lateinit var testLooper: TestLooper
private lateinit var devicesChangedListener: IInputDevicesChangedListener
private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession
@@ -202,7 +203,6 @@ class BatteryControllerTests {
@Before
fun setup() {
- context = TestableContext(ApplicationProvider.getApplicationContext())
testLooper = TestLooper()
val inputManager = InputManager(context)
context.addMockSystemService(InputManager::class.java, inputManager)
diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
index 2799f6c79215..4f1fb6487b19 100644
--- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
+++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt
@@ -32,6 +32,7 @@ import android.hardware.input.InputGestureData
import android.hardware.input.InputManager
import android.hardware.input.InputManagerGlobal
import android.hardware.input.KeyGestureEvent
+import android.os.Handler
import android.os.IBinder
import android.os.Process
import android.os.SystemClock
@@ -48,9 +49,11 @@ import android.view.WindowManagerPolicyConstants.FLAG_INTERACTIVE
import androidx.test.core.app.ApplicationProvider
import com.android.dx.mockito.inline.extended.ExtendedMockito
import com.android.internal.R
+import com.android.internal.accessibility.AccessibilityShortcutController
import com.android.internal.annotations.Keep
import com.android.internal.util.FrameworkStatsLog
import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.server.input.InputManagerService.WindowManagerCallbacks
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
@@ -67,6 +70,8 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
/**
* Tests for {@link KeyGestureController}.
@@ -102,6 +107,7 @@ class KeyGestureControllerTests {
const val SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0
const val SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL = 1
const val SETTINGS_KEY_BEHAVIOR_NOTHING = 2
+ const val TEST_PID = 10
}
@JvmField
@@ -116,11 +122,10 @@ class KeyGestureControllerTests {
@Rule
val rule = SetFlagsRule()
- @Mock
- private lateinit var iInputManager: IInputManager
-
- @Mock
- private lateinit var packageManager: PackageManager
+ @Mock private lateinit var iInputManager: IInputManager
+ @Mock private lateinit var packageManager: PackageManager
+ @Mock private lateinit var wmCallbacks: WindowManagerCallbacks
+ @Mock private lateinit var accessibilityShortcutController: AccessibilityShortcutController
private var currentPid = 0
private lateinit var context: Context
@@ -207,8 +212,34 @@ class KeyGestureControllerTests {
private fun setupKeyGestureController() {
keyGestureController =
- KeyGestureController(context, testLooper.looper, testLooper.looper, inputDataStore)
- Mockito.`when`(iInputManager.getAppLaunchBookmarks())
+ KeyGestureController(
+ context,
+ testLooper.looper,
+ testLooper.looper,
+ inputDataStore,
+ object : KeyGestureController.Injector() {
+ override fun getAccessibilityShortcutController(
+ context: Context?,
+ handler: Handler?
+ ): AccessibilityShortcutController {
+ return accessibilityShortcutController
+ }
+ })
+ Mockito.`when`(iInputManager.registerKeyGestureHandler(Mockito.any()))
+ .thenAnswer {
+ val args = it.arguments
+ if (args[0] != null) {
+ keyGestureController.registerKeyGestureHandler(
+ args[0] as IKeyGestureHandler,
+ TEST_PID
+ )
+ }
+ }
+ keyGestureController.setWindowManagerCallbacks(wmCallbacks)
+ Mockito.`when`(wmCallbacks.isKeyguardLocked(Mockito.anyInt())).thenReturn(false)
+ Mockito.`when`(accessibilityShortcutController
+ .isAccessibilityShortcutAvailable(Mockito.anyBoolean())).thenReturn(true)
+ Mockito.`when`(iInputManager.appLaunchBookmarks)
.thenReturn(keyGestureController.appLaunchBookmarks)
keyGestureController.systemRunning()
testLooper.dispatchAll()
@@ -1270,9 +1301,9 @@ class KeyGestureControllerTests {
)
),
TestData(
- "BACK + DPAD_DOWN -> TV Accessibility Chord",
+ "BACK + DPAD_DOWN -> Accessibility Chord(for TV)",
intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
- KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD,
+ KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD,
intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
0,
intArrayOf(
@@ -1622,6 +1653,52 @@ class KeyGestureControllerTests {
)
}
+ @Test
+ fun testAccessibilityShortcutChordPressed() {
+ setupKeyGestureController()
+
+ sendKeys(
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN),
+ // Assuming this value is always greater than the accessibility shortcut timeout, which
+ // currently defaults to 3000ms
+ timeDelayMs = 10000
+ )
+ Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut()
+ }
+
+ @Test
+ fun testAccessibilityTvShortcutChordPressed() {
+ setupKeyGestureController()
+
+ sendKeys(
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+ timeDelayMs = 10000
+ )
+ Mockito.verify(accessibilityShortcutController, times(1)).performAccessibilityShortcut()
+ }
+
+ @Test
+ fun testAccessibilityShortcutChordPressedForLessThanTimeout() {
+ setupKeyGestureController()
+
+ sendKeys(
+ intArrayOf(KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN),
+ timeDelayMs = 0
+ )
+ Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut()
+ }
+
+ @Test
+ fun testAccessibilityTvShortcutChordPressedForLessThanTimeout() {
+ setupKeyGestureController()
+
+ sendKeys(
+ intArrayOf(KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN),
+ timeDelayMs = 0
+ )
+ Mockito.verify(accessibilityShortcutController, never()).performAccessibilityShortcut()
+ }
+
private fun testKeyGestureInternal(test: TestData) {
val handledEvents = mutableListOf<KeyGestureEvent>()
val handler = KeyGestureHandler { event, _ ->
@@ -1683,7 +1760,11 @@ class KeyGestureControllerTests {
assertEquals("Test: $testName should not produce Key gesture", 0, handledEvents.size)
}
- private fun sendKeys(testKeys: IntArray, assertNotSentToApps: Boolean = false) {
+ private fun sendKeys(
+ testKeys: IntArray,
+ assertNotSentToApps: Boolean = false,
+ timeDelayMs: Long = 0
+ ) {
var metaState = 0
val now = SystemClock.uptimeMillis()
for (key in testKeys) {
@@ -1699,6 +1780,11 @@ class KeyGestureControllerTests {
testLooper.dispatchAll()
}
+ if (timeDelayMs > 0) {
+ testLooper.moveTimeForward(timeDelayMs)
+ testLooper.dispatchAll()
+ }
+
for (key in testKeys.reversed()) {
val upEvent = KeyEvent(
now, now, KeyEvent.ACTION_UP, key, 0 /*repeat*/, metaState,
@@ -1742,9 +1828,5 @@ class KeyGestureControllerTests {
override fun handleKeyGesture(event: AidlKeyGestureEvent, token: IBinder?): Boolean {
return handler(event, token)
}
-
- override fun isKeyGestureSupported(gestureType: Int): Boolean {
- return true
- }
}
}
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java
index 5875520cd259..20528f23cc8c 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewControllerTests.java
@@ -23,7 +23,6 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.content.Context;
import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.testing.AndroidTestingRunner;
@@ -60,9 +59,12 @@ public class TouchpadDebugViewControllerTests {
private static final String TAG = "TouchpadDebugViewController";
@Rule
+ public final TestableContext mTestableContext =
+ new TestableContext(InstrumentationRegistry.getInstrumentation().getContext());
+
+ @Rule
public final MockitoRule mockito = MockitoJUnit.rule();
- private Context mContext;
private TouchpadDebugViewController mTouchpadDebugViewController;
@Mock
private InputManager mInputManagerMock;
@@ -74,8 +76,6 @@ public class TouchpadDebugViewControllerTests {
@Before
public void setup() throws Exception {
- mContext = InstrumentationRegistry.getInstrumentation().getContext();
- TestableContext mTestableContext = new TestableContext(mContext);
mTestableContext.addMockSystemService(WindowManager.class, mWindowManagerMock);
Rect bounds = new Rect(0, 0, 2560, 1600);
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
index 60fa52f85e34..1c366a134300 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -26,7 +26,6 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
@@ -51,6 +50,7 @@ import com.android.server.input.TouchpadHardwareProperties;
import com.android.server.input.TouchpadHardwareState;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -70,6 +70,10 @@ public class TouchpadDebugViewTest {
private TouchpadDebugView mTouchpadDebugView;
private WindowManager.LayoutParams mWindowLayoutParams;
+ @Rule
+ public final TestableContext mTestableContext =
+ new TestableContext(InstrumentationRegistry.getInstrumentation().getContext());
+
@Mock
WindowManager mWindowManager;
@Mock
@@ -77,14 +81,10 @@ public class TouchpadDebugViewTest {
Rect mWindowBounds;
WindowMetrics mWindowMetrics;
- TestableContext mTestableContext;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- Context context = InstrumentationRegistry.getInstrumentation().getContext();
- mTestableContext = new TestableContext(context);
-
mTestableContext.addMockSystemService(WindowManager.class, mWindowManager);
mTestableContext.addMockSystemService(InputManager.class, mInputManager);
diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java
index 4d379e45a81a..bb54a26036db 100644
--- a/tests/utils/testutils/java/android/os/test/TestLooper.java
+++ b/tests/utils/testutils/java/android/os/test/TestLooper.java
@@ -68,16 +68,7 @@ public class TestLooper {
* Baklava introduces new {@link TestLooperManager} APIs that we can use instead of reflection.
*/
private static boolean isAtLeastBaklava() {
- Method[] methods = TestLooperManager.class.getMethods();
- for (Method method : methods) {
- if (method.getName().equals("peekWhen")) {
- return true;
- }
- }
- return false;
- // TODO(shayba): delete the above, uncomment the below.
- // SDK_INT has not yet ramped to Baklava in all 25Q2 builds.
- // return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA;
}
static {
diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h
index 0d261abd728d..e51477c668dd 100644
--- a/tools/aapt2/Resource.h
+++ b/tools/aapt2/Resource.h
@@ -249,6 +249,8 @@ struct ResourceFile {
// Flag
std::optional<FeatureFlagAttribute> flag;
+
+ bool uses_readwrite_feature_flags = false;
};
/**
diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp
index 5435cba290fc..db7dddc49a99 100644
--- a/tools/aapt2/ResourceTable.cpp
+++ b/tools/aapt2/ResourceTable.cpp
@@ -664,6 +664,7 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag)
if (!config_value->value) {
// Resource does not exist, add it now.
config_value->value = std::move(res.value);
+ config_value->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags;
} else {
// When validation is enabled, ensure that a resource cannot have multiple values defined for
// the same configuration unless protected by flags.
@@ -681,12 +682,14 @@ bool ResourceTable::AddResource(NewResource&& res, android::IDiagnostics* diag)
ConfigKey{&res.config, res.product}, lt_config_key_ref()),
util::make_unique<ResourceConfigValue>(res.config, res.product));
(*it)->value = std::move(res.value);
+ (*it)->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags;
break;
}
case CollisionResult::kTakeNew:
// Take the incoming value.
config_value->value = std::move(res.value);
+ config_value->uses_readwrite_feature_flags = res.uses_readwrite_feature_flags;
break;
case CollisionResult::kConflict:
@@ -843,6 +846,12 @@ NewResourceBuilder& NewResourceBuilder::SetAllowMangled(bool allow_mangled) {
return *this;
}
+NewResourceBuilder& NewResourceBuilder::SetUsesReadWriteFeatureFlags(
+ bool uses_readwrite_feature_flags) {
+ res_.uses_readwrite_feature_flags = uses_readwrite_feature_flags;
+ return *this;
+}
+
NewResource NewResourceBuilder::Build() {
return std::move(res_);
}
diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h
index b0e185536d16..778b43adb50b 100644
--- a/tools/aapt2/ResourceTable.h
+++ b/tools/aapt2/ResourceTable.h
@@ -104,6 +104,9 @@ class ResourceConfigValue {
// The actual Value.
std::unique_ptr<Value> value;
+ // Whether the value uses read/write feature flags
+ bool uses_readwrite_feature_flags = false;
+
ResourceConfigValue(const android::ConfigDescription& config, android::StringPiece product)
: config(config), product(product) {
}
@@ -284,6 +287,7 @@ struct NewResource {
std::optional<AllowNew> allow_new;
std::optional<StagedId> staged_id;
bool allow_mangled = false;
+ bool uses_readwrite_feature_flags = false;
};
struct NewResourceBuilder {
@@ -297,6 +301,7 @@ struct NewResourceBuilder {
NewResourceBuilder& SetAllowNew(AllowNew allow_new);
NewResourceBuilder& SetStagedId(StagedId id);
NewResourceBuilder& SetAllowMangled(bool allow_mangled);
+ NewResourceBuilder& SetUsesReadWriteFeatureFlags(bool uses_feature_flags);
NewResource Build();
private:
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 2a7921600477..755dbb6f8e42 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -673,11 +673,13 @@ bool ResourceFileFlattener::Flatten(ResourceTable* table, IArchiveWriter* archiv
// Update the output format of this XML file.
file_ref->type = XmlFileTypeForOutputFormat(options_.output_format);
- bool result = table->AddResource(NewResourceBuilder(file.name)
- .SetValue(std::move(file_ref), file.config)
- .SetAllowMangled(true)
- .Build(),
- context_->GetDiagnostics());
+ bool result = table->AddResource(
+ NewResourceBuilder(file.name)
+ .SetValue(std::move(file_ref), file.config)
+ .SetAllowMangled(true)
+ .SetUsesReadWriteFeatureFlags(doc->file.uses_readwrite_feature_flags)
+ .Build(),
+ context_->GetDiagnostics());
if (!result) {
return false;
}
diff --git a/tools/aapt2/format/binary/BinaryResourceParser.cpp b/tools/aapt2/format/binary/BinaryResourceParser.cpp
index 2e20e8175213..bac871b8bdc3 100644
--- a/tools/aapt2/format/binary/BinaryResourceParser.cpp
+++ b/tools/aapt2/format/binary/BinaryResourceParser.cpp
@@ -414,6 +414,8 @@ bool BinaryResourceParser::ParseType(const ResourceTablePackage* package,
.SetId(res_id, OnIdConflict::CREATE_ENTRY)
.SetAllowMangled(true);
+ res_builder.SetUsesReadWriteFeatureFlags(entry->uses_feature_flags());
+
if (entry->flags() & ResTable_entry::FLAG_PUBLIC) {
Visibility visibility{Visibility::Level::kPublic};
diff --git a/tools/aapt2/format/binary/ResEntryWriter.cpp b/tools/aapt2/format/binary/ResEntryWriter.cpp
index 9dc205f4c1ba..0be392164453 100644
--- a/tools/aapt2/format/binary/ResEntryWriter.cpp
+++ b/tools/aapt2/format/binary/ResEntryWriter.cpp
@@ -199,6 +199,10 @@ void WriteEntry(const FlatEntry* entry, T* out_result, bool compact = false) {
flags |= ResTable_entry::FLAG_WEAK;
}
+ if (entry->uses_readwrite_feature_flags) {
+ flags |= ResTable_entry::FLAG_USES_FEATURE_FLAGS;
+ }
+
if constexpr (std::is_same_v<ResTable_entry_ext, T>) {
flags |= ResTable_entry::FLAG_COMPLEX;
}
diff --git a/tools/aapt2/format/binary/ResEntryWriter.h b/tools/aapt2/format/binary/ResEntryWriter.h
index c11598ec12f7..f54b29aa8f2a 100644
--- a/tools/aapt2/format/binary/ResEntryWriter.h
+++ b/tools/aapt2/format/binary/ResEntryWriter.h
@@ -38,6 +38,8 @@ struct FlatEntry {
// The entry string pool index to the entry's name.
uint32_t entry_key;
+
+ bool uses_readwrite_feature_flags;
};
// Pair of ResTable_entry and Res_value. These pairs are stored sequentially in values buffer.
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 1a82021bce71..50144ae816b6 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -502,7 +502,8 @@ class PackageFlattener {
// Group values by configuration.
for (auto& config_value : entry.values) {
config_to_entry_list_map[config_value->config].push_back(
- FlatEntry{&entry, config_value->value.get(), local_key_index});
+ FlatEntry{&entry, config_value->value.get(), local_key_index,
+ config_value->uses_readwrite_feature_flags});
}
}
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index 0f1168514c4a..9156b96b67ec 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -1069,4 +1069,23 @@ TEST_F(TableFlattenerTest, FlattenTypeEntryWithNameCollapseInExemption) {
testing::IsTrue());
}
+TEST_F(TableFlattenerTest, UsesReadWriteFeatureFlagSerializesCorrectly) {
+ std::unique_ptr<ResourceTable> table =
+ test::ResourceTableBuilder()
+ .Add(NewResourceBuilder("com.app.a:color/foo")
+ .SetValue(util::make_unique<BinaryPrimitive>(
+ uint8_t(Res_value::TYPE_INT_COLOR_ARGB8), 0xffaabbcc))
+ .SetUsesReadWriteFeatureFlags(true)
+ .SetId(0x7f020000)
+ .Build())
+ .Build();
+ ResTable res_table;
+ TableFlattenerOptions options;
+ ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table));
+
+ uint32_t flags;
+ ASSERT_TRUE(res_table.getResourceEntryFlags(0x7f020000, &flags));
+ ASSERT_EQ(flags, ResTable_entry::FLAG_USES_FEATURE_FLAGS);
+}
+
} // namespace aapt
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
index dbef77615515..47a71fe36e9f 100644
--- a/tools/aapt2/link/FlaggedResources_test.cpp
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -14,6 +14,9 @@
* limitations under the License.
*/
+#include <regex>
+#include <string>
+
#include "LoadedApk.h"
#include "cmd/Dump.h"
#include "io/StringStream.h"
@@ -183,4 +186,49 @@ TEST_F(FlaggedResourcesTest, ReadWriteFlagInPathFails) {
"Only read only flags may be used with resources: test.package.rwFlag"));
}
+TEST_F(FlaggedResourcesTest, ReadWriteFlagInXmlGetsFlagged) {
+ auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"});
+ auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
+
+ std::string output;
+ DumpChunksToString(loaded_apk.get(), &output);
+
+ // The actual line looks something like:
+ // [ResTable_entry] id: 0x0000 name: layout1 keyIndex: 14 size: 8 flags: 0x0010
+ //
+ // This regex matches that line and captures the name and the flag value for checking.
+ std::regex regex("[0-9a-zA-Z:_\\]\\[ ]+name: ([0-9a-zA-Z]+)[0-9a-zA-Z: ]+flags: (0x\\d{4})");
+ std::smatch match;
+
+ std::stringstream ss(output);
+ std::string line;
+ bool found = false;
+ int fields_flagged = 0;
+ while (std::getline(ss, line)) {
+ bool first_line = false;
+ if (line.contains("config: v36")) {
+ std::getline(ss, line);
+ first_line = true;
+ }
+ if (!line.contains("flags")) {
+ continue;
+ }
+ if (std::regex_search(line, match, regex) && (match.size() == 3)) {
+ unsigned int hex_value;
+ std::stringstream hex_ss;
+ hex_ss << std::hex << match[2];
+ hex_ss >> hex_value;
+ if (hex_value & android::ResTable_entry::FLAG_USES_FEATURE_FLAGS) {
+ fields_flagged++;
+ if (first_line && match[1] == "layout1") {
+ found = true;
+ }
+ }
+ }
+ }
+ ASSERT_TRUE(found) << "No entry for layout1 at v36 with FLAG_USES_FEATURE_FLAGS bit set";
+ // There should only be 1 entry that has the FLAG_USES_FEATURE_FLAGS bit of flags set to 1
+ ASSERT_EQ(fields_flagged, 1);
+}
+
} // namespace aapt
diff --git a/tools/aapt2/link/FlaggedXmlVersioner.cpp b/tools/aapt2/link/FlaggedXmlVersioner.cpp
index 75c6f17dcb51..8a3337c446cb 100644
--- a/tools/aapt2/link/FlaggedXmlVersioner.cpp
+++ b/tools/aapt2/link/FlaggedXmlVersioner.cpp
@@ -66,6 +66,28 @@ class AllDisabledFlagsVisitor : public xml::Visitor {
bool had_flags_ = false;
};
+// An xml visitor that goes through the a doc and determines if any elements are behind a flag.
+class FindFlagsVisitor : public xml::Visitor {
+ public:
+ void Visit(xml::Element* node) override {
+ if (had_flags_) {
+ return;
+ }
+ auto* attr = node->FindAttribute(xml::kSchemaAndroid, xml::kAttrFeatureFlag);
+ if (attr != nullptr) {
+ had_flags_ = true;
+ return;
+ }
+ VisitChildren(node);
+ }
+
+ bool HadFlags() const {
+ return had_flags_;
+ }
+
+ bool had_flags_ = false;
+};
+
std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAaptContext* context,
xml::XmlResource* doc) {
std::vector<std::unique_ptr<xml::XmlResource>> docs;
@@ -74,15 +96,20 @@ std::vector<std::unique_ptr<xml::XmlResource>> FlaggedXmlVersioner::Process(IAap
// Support for read/write flags was added in baklava so if the doc will only get used on
// baklava or later we can just return the original doc.
docs.push_back(doc->Clone());
+ FindFlagsVisitor visitor;
+ doc->root->Accept(&visitor);
+ docs.back()->file.uses_readwrite_feature_flags = visitor.HadFlags();
} else {
auto preBaklavaVersion = doc->Clone();
AllDisabledFlagsVisitor visitor;
preBaklavaVersion->root->Accept(&visitor);
+ preBaklavaVersion->file.uses_readwrite_feature_flags = false;
docs.push_back(std::move(preBaklavaVersion));
if (visitor.HadFlags()) {
auto baklavaVersion = doc->Clone();
baklavaVersion->file.config.sdkVersion = SDK_BAKLAVA;
+ baklavaVersion->file.uses_readwrite_feature_flags = true;
docs.push_back(std::move(baklavaVersion));
}
}