summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java28
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java138
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java36
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java24
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java9
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java14
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java251
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java16
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java6
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java84
-rw-r--r--libs/WindowManager/Shell/Android.bp2
-rw-r--r--libs/WindowManager/Shell/AndroidManifest.xml25
-rw-r--r--libs/WindowManager/Shell/aconfig/multitasking.aconfig20
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp61
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifest.xml35
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml25
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/AndroidTest.xml35
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.pngbin0 -> 56102 bytes
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.pngbin0 -> 56102 bytes
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/robolectric/config/robolectric.properties2
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt58
-rw-r--r--libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/testing/goldenpathmanager/WMShellGoldenPathManager.kt53
l---------libs/WindowManager/Shell/multivalentScreenshotTestsForDevice1
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt2
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml1
-rw-r--r--libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget.xml (renamed from libs/WindowManager/Shell/res/drawable/desktop_mode_header_background.xml)17
-rw-r--r--libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_background.xml24
-rw-r--r--libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml36
-rw-r--r--libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml2
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml (renamed from libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml)0
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml (renamed from libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml)1
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml22
-rw-r--r--libs/WindowManager/Shell/res/values-af/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-am/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ar/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-as/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-az/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-be/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-bg/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-bn/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-bs/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ca/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-cs/strings.xml6
-rw-r--r--libs/WindowManager/Shell/res/values-da/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-de/strings.xml8
-rw-r--r--libs/WindowManager/Shell/res/values-el/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-en-rAU/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-en-rCA/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-en-rGB/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-en-rIN/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-en-rXC/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-es-rUS/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-es/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-et/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-eu/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-fa/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-fi/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-fr-rCA/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-fr/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-gl/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-gu/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-hi/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-hr/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-hu/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-hy/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-in/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-is/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-it/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-iw/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ja/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ka/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-kk/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-km/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-kn/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ko/strings.xml6
-rw-r--r--libs/WindowManager/Shell/res/values-ky/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-lo/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-lt/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-lv/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-mk/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ml/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-mn/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-mr/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ms/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-my/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-nb/strings.xml6
-rw-r--r--libs/WindowManager/Shell/res/values-ne/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-nl/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-or/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-pa/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-pl/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-pt-rBR/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-pt-rPT/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-pt/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ro/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ru/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-si/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-sk/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-sl/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-sq/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-sr/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-sv/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-sw/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ta/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-te/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-th/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-tl/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-tr/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-uk/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ur/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-uz/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-vi/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rCN/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rHK/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rTW/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-zu/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml12
-rw-r--r--libs/WindowManager/Shell/res/values/strings.xml6
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java73
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java43
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java41
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java45
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java84
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java68
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java93
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/BubbleShortcutHelper.kt40
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt52
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt59
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java118
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java25
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/ExecutorUtils.java64
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java40
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/RemoteCallable.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.aidl19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt54
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt26
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt26
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt106
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt58
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt95
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt227
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt153
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java35
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java59
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java117
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java154
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java46
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java31
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java93
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/protolog/OWNERS1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java46
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt143
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java152
-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.java236
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java92
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java45
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java41
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java191
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt20
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt612
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt52
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java265
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt66
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewContainer.kt35
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainer.kt46
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt94
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt)23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt)319
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt)17
-rw-r--r--libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp88
-rw-r--r--libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt67
-rw-r--r--libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt3
-rw-r--r--libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt75
-rw-r--r--libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/Android.bp1
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java163
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java20
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java42
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java19
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java24
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java23
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt13
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt919
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt107
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt73
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt3751
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt161
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java11
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java8
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java39
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt217
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt24
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java180
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt225
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java174
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt16
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt212
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt20
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt9
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java55
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt90
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainerTest.kt68
-rw-r--r--libs/androidfw/AssetManager.cpp4
-rw-r--r--libs/androidfw/LocaleDataTables.cpp2
-rw-r--r--libs/androidfw/ResourceTypes.cpp41
-rw-r--r--libs/androidfw/ZipFileRO.cpp8
-rw-r--r--libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp5
-rw-r--r--libs/androidfw/include/androidfw/ZipFileRO.h4
-rw-r--r--libs/hwui/Android.bp1
-rw-r--r--libs/hwui/Properties.cpp7
-rw-r--r--libs/hwui/Properties.h7
-rw-r--r--libs/hwui/aconfig/hwui_flags.aconfig7
-rw-r--r--libs/hwui/apex/jni_runtime.cpp9
-rw-r--r--libs/hwui/effects/GainmapRenderer.cpp14
-rw-r--r--libs/hwui/jni/Graphics.cpp10
-rw-r--r--libs/hwui/renderthread/VulkanManager.cpp7
-rw-r--r--libs/hwui/renderthread/VulkanManager.h7
-rw-r--r--libs/input/Android.bp1
-rw-r--r--libs/nativehelper_jvm/Android.bp19
-rw-r--r--libs/nativehelper_jvm/JNIPlatformHelp.c104
-rw-r--r--libs/nativehelper_jvm/JniConstants.c199
-rw-r--r--libs/nativehelper_jvm/JniConstants.h63
-rw-r--r--libs/nativehelper_jvm/OWNERS7
-rw-r--r--libs/nativehelper_jvm/README2
-rw-r--r--libs/nativehelper_jvm/file_descriptor_jni.c47
277 files changed, 10325 insertions, 3736 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index 16c77d0c3c81..ecf47209a802 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -24,6 +24,7 @@ import android.app.Application;
import android.app.compat.CompatChanges;
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager;
+import android.os.SystemProperties;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -50,6 +51,11 @@ class WindowExtensionsImpl implements WindowExtensions {
private static final String TAG = "WindowExtensionsImpl";
/**
+ * The value of the system property that indicates no override is set.
+ */
+ private static final int NO_LEVEL_OVERRIDE = -1;
+
+ /**
* The min version of the WM Extensions that must be supported in the current platform version.
*/
@VisibleForTesting
@@ -66,14 +72,30 @@ class WindowExtensionsImpl implements WindowExtensions {
WindowExtensionsImpl() {
mIsActivityEmbeddingEnabled = isActivityEmbeddingEnabled();
- Log.i(TAG, "Initializing Window Extensions, vendor API level=" + mVersion
- + ", activity embedding enabled=" + mIsActivityEmbeddingEnabled);
+
+ Log.i(TAG, generateLogMessage());
+ }
+
+ private String generateLogMessage() {
+ final StringBuilder logBuilder = new StringBuilder("Initializing Window Extensions, "
+ + "vendor API level=" + mVersion);
+ final int levelOverride = getLevelOverride();
+ if (levelOverride != NO_LEVEL_OVERRIDE) {
+ logBuilder.append(", override to ").append(levelOverride);
+ }
+ logBuilder.append(", activity embedding enabled=").append(mIsActivityEmbeddingEnabled);
+ return logBuilder.toString();
}
// TODO(b/241126279) Introduce constants to better version functionality
@Override
public int getVendorApiLevel() {
- return mVersion;
+ final int levelOverride = getLevelOverride();
+ return (levelOverride != NO_LEVEL_OVERRIDE) ? levelOverride : mVersion;
+ }
+
+ private int getLevelOverride() {
+ return SystemProperties.getInt("persist.wm.debug.ext_version_override", NO_LEVEL_OVERRIDE);
}
@NonNull
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
index 23dc96c39bde..822a07c23950 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -16,6 +16,9 @@
package androidx.window.extensions.embedding;
+import static android.content.pm.ActivityInfo.CONFIG_DENSITY;
+import static android.content.pm.ActivityInfo.CONFIG_LAYOUT_DIRECTION;
+import static android.content.pm.ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
import static android.util.TypedValue.COMPLEX_UNIT_DIP;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
@@ -40,7 +43,6 @@ import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityThread;
import android.content.Context;
-import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Color;
import android.graphics.PixelFormat;
@@ -167,6 +169,11 @@ class DividerPresenter implements View.OnTouchListener {
@GuardedBy("mLock")
private int mDividerPosition;
+ /** Indicates if there are containers to be finished since the divider has appeared. */
+ @GuardedBy("mLock")
+ @VisibleForTesting
+ private boolean mHasContainersToFinish = false;
+
DividerPresenter(int taskId, @NonNull DragEventCallback dragEventCallback,
@NonNull Executor callbackExecutor) {
mTaskId = taskId;
@@ -178,7 +185,8 @@ class DividerPresenter implements View.OnTouchListener {
void updateDivider(
@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentParentInfo parentInfo,
- @Nullable SplitContainer topSplitContainer) {
+ @Nullable SplitContainer topSplitContainer,
+ boolean isTaskFragmentVanished) {
if (!Flags.activityEmbeddingInteractiveDividerFlag()) {
return;
}
@@ -186,6 +194,18 @@ class DividerPresenter implements View.OnTouchListener {
synchronized (mLock) {
// Clean up the decor surface if top SplitContainer is null.
if (topSplitContainer == null) {
+ // Check if there are containers to finish but the TaskFragment hasn't vanished yet.
+ // Don't remove the decor surface and divider if so as the removal should happen in
+ // a following step when the TaskFragment has vanished. This ensures that the decor
+ // surface is removed only after the resulting Activity is ready to be shown,
+ // otherwise there may be flicker.
+ if (mHasContainersToFinish) {
+ if (isTaskFragmentVanished) {
+ setHasContainersToFinish(false);
+ } else {
+ return;
+ }
+ }
removeDecorSurfaceAndDivider(wct);
return;
}
@@ -683,52 +703,72 @@ class DividerPresenter implements View.OnTouchListener {
? taskBounds.width() - mProperties.mDividerWidthPx
: taskBounds.height() - mProperties.mDividerWidthPx;
- if (isDraggingToFullscreenAllowed(mProperties.mDividerAttributes)) {
- final float displayDensity = getDisplayDensity();
- return dividerPositionWithDraggingToFullscreenAllowed(
- dividerPosition,
- minPosition,
- maxPosition,
- fullyExpandedPosition,
- velocity,
- displayDensity);
- }
- return Math.clamp(dividerPosition, minPosition, maxPosition);
+ final float displayDensity = getDisplayDensity();
+ final boolean isDraggingToFullscreenAllowed =
+ isDraggingToFullscreenAllowed(mProperties.mDividerAttributes);
+ return dividerPositionWithPositionOptions(
+ dividerPosition,
+ minPosition,
+ maxPosition,
+ fullyExpandedPosition,
+ velocity,
+ displayDensity,
+ isDraggingToFullscreenAllowed);
}
/**
- * Returns the divider position given a set of position options. A snap algorithm is used to
- * adjust the ending position to either fully expand one container or move the divider back to
- * the specified min/max ratio depending on the dragging velocity.
+ * Returns the divider position given a set of position options. A snap algorithm can adjust
+ * the ending position to either fully expand one container or move the divider back to
+ * the specified min/max ratio depending on the dragging velocity and if dragging to fullscreen
+ * is allowed.
*/
@VisibleForTesting
- static int dividerPositionWithDraggingToFullscreenAllowed(int dividerPosition, int minPosition,
- int maxPosition, int fullyExpandedPosition, float velocity, float displayDensity) {
- final float minDismissVelocityPxPerSecond =
- MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity;
+ static int dividerPositionWithPositionOptions(int dividerPosition, int minPosition,
+ int maxPosition, int fullyExpandedPosition, float velocity, float displayDensity,
+ boolean isDraggingToFullscreenAllowed) {
+ if (isDraggingToFullscreenAllowed) {
+ final float minDismissVelocityPxPerSecond =
+ MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity;
+ if (dividerPosition < minPosition && velocity < -minDismissVelocityPxPerSecond) {
+ return 0;
+ }
+ if (dividerPosition > maxPosition && velocity > minDismissVelocityPxPerSecond) {
+ return fullyExpandedPosition;
+ }
+ }
final float minFlingVelocityPxPerSecond =
MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity;
- if (dividerPosition < minPosition && velocity < -minDismissVelocityPxPerSecond) {
- return 0;
+ if (Math.abs(velocity) >= minFlingVelocityPxPerSecond) {
+ return dividerPositionForFling(
+ dividerPosition, minPosition, maxPosition, velocity);
}
- if (dividerPosition > maxPosition && velocity > minDismissVelocityPxPerSecond) {
- return fullyExpandedPosition;
+ if (dividerPosition >= minPosition && dividerPosition <= maxPosition) {
+ return dividerPosition;
}
- if (Math.abs(velocity) < minFlingVelocityPxPerSecond) {
- if (dividerPosition >= minPosition && dividerPosition <= maxPosition) {
- return dividerPosition;
- }
- int[] possiblePositions = {0, minPosition, maxPosition, fullyExpandedPosition};
- return snap(dividerPosition, possiblePositions);
- }
- if (velocity < 0) {
- return minPosition;
+ return snap(
+ dividerPosition,
+ isDraggingToFullscreenAllowed
+ ? new int[] {0, minPosition, maxPosition, fullyExpandedPosition}
+ : new int[] {minPosition, maxPosition});
+ }
+
+ /**
+ * Returns the closest position that is in the fling direction.
+ */
+ private static int dividerPositionForFling(int dividerPosition, int minPosition,
+ int maxPosition, float velocity) {
+ final boolean isBackwardDirection = velocity < 0;
+ if (isBackwardDirection) {
+ return dividerPosition < maxPosition ? minPosition : maxPosition;
} else {
- return maxPosition;
+ return dividerPosition > minPosition ? maxPosition : minPosition;
}
}
- /** Calculates the snapped divider position based on the possible positions and distance. */
+ /**
+ * Returns the snapped position from a list of possible positions. Currently, this method
+ * snaps to the closest position by distance from the divider position.
+ */
private static int snap(int dividerPosition, int[] possiblePositions) {
int snappedPosition = dividerPosition;
float minDistance = Float.MAX_VALUE;
@@ -846,6 +886,12 @@ class DividerPresenter implements View.OnTouchListener {
}
}
+ void setHasContainersToFinish(boolean hasContainersToFinish) {
+ synchronized (mLock) {
+ mHasContainersToFinish = hasContainersToFinish;
+ }
+ }
+
private static boolean isDraggingToFullscreenAllowed(
@NonNull DividerAttributes dividerAttributes) {
// TODO(b/293654166) Use DividerAttributes.isDraggingToFullscreenAllowed when extension is
@@ -946,7 +992,7 @@ class DividerPresenter implements View.OnTouchListener {
@VisibleForTesting
static class Properties {
private static final int CONFIGURATION_MASK_FOR_DIVIDER =
- ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
+ CONFIG_DENSITY | CONFIG_WINDOW_CONFIGURATION | CONFIG_LAYOUT_DIRECTION;
@NonNull
private final Configuration mConfiguration;
@NonNull
@@ -1215,6 +1261,12 @@ class DividerPresenter implements View.OnTouchListener {
FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY,
PixelFormat.TRANSLUCENT);
lp.setTitle(WINDOW_NAME);
+
+ // Ensure that the divider layout is always LTR regardless of the locale, because we
+ // already considered the locale when determining the split layout direction and the
+ // computed divider line position always starts from the left. This only affects the
+ // horizontal layout and does not have any effect on the top-to-bottom layout.
+ mDividerLayout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR);
mViewHost.setView(mDividerLayout, lp);
mViewHost.relayout(lp);
}
@@ -1366,10 +1418,16 @@ class DividerPresenter implements View.OnTouchListener {
primaryBounds = mProperties.mIsReversedLayout ? boundsBottom : boundsTop;
secondaryBounds = mProperties.mIsReversedLayout ? boundsTop : boundsBottom;
}
- t.setWindowCrop(mPrimaryVeil, primaryBounds.width(), primaryBounds.height());
- t.setWindowCrop(mSecondaryVeil, secondaryBounds.width(), secondaryBounds.height());
- t.setPosition(mPrimaryVeil, primaryBounds.left, primaryBounds.top);
- t.setPosition(mSecondaryVeil, secondaryBounds.left, secondaryBounds.top);
+ if (mPrimaryVeil != null) {
+ t.setWindowCrop(mPrimaryVeil, primaryBounds.width(), primaryBounds.height());
+ t.setPosition(mPrimaryVeil, primaryBounds.left, primaryBounds.top);
+ t.setVisibility(mPrimaryVeil, !primaryBounds.isEmpty());
+ }
+ if (mSecondaryVeil != null) {
+ t.setWindowCrop(mSecondaryVeil, secondaryBounds.width(), secondaryBounds.height());
+ t.setPosition(mSecondaryVeil, secondaryBounds.left, secondaryBounds.top);
+ t.setVisibility(mSecondaryVeil, !secondaryBounds.isEmpty());
+ }
}
private static float[] colorToFloatArray(@NonNull Color color) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index f78e2b5170fc..7ddda1f98809 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -673,7 +673,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
break;
case TYPE_TASK_FRAGMENT_VANISHED:
mPresenter.removeTaskFragmentInfo(info);
- onTaskFragmentVanished(wct, info);
+ onTaskFragmentVanished(wct, info, taskId);
break;
case TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED:
onTaskFragmentParentInfoChanged(wct, taskId,
@@ -834,7 +834,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@VisibleForTesting
@GuardedBy("mLock")
void onTaskFragmentVanished(@NonNull WindowContainerTransaction wct,
- @NonNull TaskFragmentInfo taskFragmentInfo) {
+ @NonNull TaskFragmentInfo taskFragmentInfo, int taskId) {
final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
if (container != null) {
// Cleanup if the TaskFragment vanished is not requested by the organizer.
@@ -843,6 +843,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
updateContainersInTaskIfVisible(wct, container.getTaskId());
}
cleanupTaskFragment(taskFragmentInfo.getFragmentToken());
+ final TaskContainer taskContainer = getTaskContainer(taskId);
+ if (taskContainer != null) {
+ // Update the divider to clean up any decor surfaces.
+ updateDivider(wct, taskContainer, true /* isTaskFragmentVanished */);
+ }
}
/**
@@ -884,7 +889,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// The divider need to be updated even if shouldUpdateContainer is false, because the decor
// surface may change in TaskFragmentParentInfo, which requires divider update but not
// container update.
- updateDivider(wct, taskContainer);
+ updateDivider(wct, taskContainer, false /* isTaskFragmentVanished */);
// If the last direct activity of the host task is dismissed and there's an always-on-top
// overlay container in the task, the overlay container should also be dismissed.
@@ -899,14 +904,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@GuardedBy("mLock")
void updateContainersInTaskIfVisible(@NonNull WindowContainerTransaction wct, int taskId) {
final TaskContainer taskContainer = getTaskContainer(taskId);
- if (taskContainer != null && taskContainer.isVisible()) {
+ if (taskContainer == null) {
+ return;
+ }
+
+ if (taskContainer.isVisible()) {
updateContainersInTask(wct, taskContainer);
+ } else if (Flags.fixNoContainerUpdateWithoutResize()) {
+ // the TaskFragmentContainers need to be updated when the task becomes visible
+ taskContainer.mTaskFragmentContainersNeedsUpdate = true;
}
}
@GuardedBy("mLock")
private void updateContainersInTask(@NonNull WindowContainerTransaction wct,
@NonNull TaskContainer taskContainer) {
+ taskContainer.mTaskFragmentContainersNeedsUpdate = false;
+
// Update all TaskFragments in the Task. Make a copy of the list since some may be
// removed on updating.
final List<TaskFragmentContainer> containers = taskContainer.getTaskFragmentContainers();
@@ -3257,12 +3271,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
@GuardedBy("mLock")
- void updateDivider(
- @NonNull WindowContainerTransaction wct, @NonNull TaskContainer taskContainer) {
+ void updateDivider(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskContainer taskContainer, boolean isTaskFragmentVanished) {
final DividerPresenter dividerPresenter = mDividerPresenters.get(taskContainer.getTaskId());
final TaskFragmentParentInfo parentInfo = taskContainer.getTaskFragmentParentInfo();
- dividerPresenter.updateDivider(
- wct, parentInfo, taskContainer.getTopNonFinishingSplitContainer());
+ final SplitContainer topSplitContainer = taskContainer.getTopNonFinishingSplitContainer();
+ if (dividerPresenter != null) {
+ dividerPresenter.updateDivider(
+ wct, parentInfo, topSplitContainer, isTaskFragmentVanished);
+ }
}
@Override
@@ -3292,6 +3309,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
final List<TaskFragmentContainer> containersToFinish = new ArrayList<>();
taskContainer.updateTopSplitContainerForDivider(
dividerPresenter, containersToFinish);
+ if (!containersToFinish.isEmpty()) {
+ dividerPresenter.setHasContainersToFinish(true);
+ }
for (final TaskFragmentContainer container : containersToFinish) {
mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index eade86e50659..d0e49d8c403f 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -374,7 +374,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode);
updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes);
updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes);
- mController.updateDivider(wct, taskContainer);
+ mController.updateDivider(wct, taskContainer, false /* isTaskFragmentVanished */);
}
private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
@@ -658,27 +658,28 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
}
/**
- * Returns the expanded bounds if the {@code bounds} violate minimum dimension or are not fully
- * covered by the task bounds. Otherwise, returns {@code bounds}.
+ * Returns the expanded bounds if the {@code relBounds} violate minimum dimension or are not
+ * fully covered by the task bounds. Otherwise, returns {@code relBounds}.
*/
@NonNull
- static Rect sanitizeBounds(@NonNull Rect bounds, @Nullable Size minDimension,
+ static Rect sanitizeBounds(@NonNull Rect relBounds, @Nullable Size minDimension,
@NonNull TaskFragmentContainer container) {
- if (bounds.isEmpty()) {
+ if (relBounds.isEmpty()) {
// Don't need to check if the bounds follows the task bounds.
- return bounds;
+ return relBounds;
}
- if (boundsSmallerThanMinDimensions(bounds, minDimension)) {
+ if (boundsSmallerThanMinDimensions(relBounds, minDimension)) {
// Expand the bounds if the bounds are smaller than minimum dimensions.
return new Rect();
}
final TaskContainer taskContainer = container.getTaskContainer();
- final Rect taskBounds = taskContainer.getBounds();
- if (!taskBounds.contains(bounds)) {
+ final Rect relTaskBounds = new Rect(taskContainer.getBounds());
+ relTaskBounds.offsetTo(0, 0);
+ if (!relTaskBounds.contains(relBounds)) {
// Expand the bounds if the bounds exceed the task bounds.
return new Rect();
}
- return bounds;
+ return relBounds;
}
@Override
@@ -756,7 +757,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
void expandTaskFragment(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container) {
super.expandTaskFragment(wct, container);
- mController.updateDivider(wct, container.getTaskContainer());
+ mController.updateDivider(
+ wct, container.getTaskContainer(), false /* isTaskFragmentVanished */);
}
static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
index ee00c4cd67eb..20ad53ee19a8 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -108,6 +108,12 @@ class TaskContainer {
private boolean mPlaceholderRuleSuppressed;
/**
+ * {@code true} if the TaskFragments in this Task needs to be updated next time the Task
+ * becomes visible. See {@link #shouldUpdateContainer(TaskFragmentParentInfo)}
+ */
+ boolean mTaskFragmentContainersNeedsUpdate;
+
+ /**
* The {@link TaskContainer} constructor
*
* @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with
@@ -185,7 +191,8 @@ class TaskContainer {
// If the task properties equals regardless of starting position, don't
// need to update the container.
- return mInfo.getConfiguration().diffPublicOnly(configuration) != 0
+ return mTaskFragmentContainersNeedsUpdate
+ || mInfo.getConfiguration().diffPublicOnly(configuration) != 0
|| mInfo.getDisplayId() != info.getDisplayId();
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
index 070fa5bcfae4..859bc2cc40f3 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -17,6 +17,7 @@
package androidx.window.extensions.layout;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_FLAT;
import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_HALF_OPENED;
@@ -41,6 +42,7 @@ import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiContext;
+import androidx.annotation.VisibleForTesting;
import androidx.window.common.CommonFoldingFeature;
import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
@@ -138,6 +140,10 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
throw new IllegalArgumentException("Context must be a UI Context, which should be"
+ " an Activity, WindowContext or InputMethodService");
}
+ if (context.getAssociatedDisplayId() == INVALID_DISPLAY) {
+ Log.w(TAG, "The registered Context is a UI Context but not associated with any"
+ + " display. This Context may not receive any WindowLayoutInfo update");
+ }
mFoldingFeatureProducer.getData((features) -> {
WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features);
consumer.accept(newWindowLayout);
@@ -257,7 +263,8 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
}
}
- private void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) {
+ @VisibleForTesting
+ void onDisplayFeaturesChanged(List<CommonFoldingFeature> storedFeatures) {
synchronized (mLock) {
mLastReportedFoldingFeatures.clear();
mLastReportedFoldingFeatures.addAll(storedFeatures);
@@ -409,9 +416,10 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
* @return true if the display features should be reported for the UI Context, false otherwise.
*/
private boolean shouldReportDisplayFeatures(@NonNull @UiContext Context context) {
- int displayId = context.getDisplay().getDisplayId();
+ int displayId = context.getAssociatedDisplayId();
if (displayId != DEFAULT_DISPLAY) {
- // Display features are not supported on secondary displays.
+ // Display features are not supported on secondary displays or the context is not
+ // associated with any display.
return false;
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
index 3f676079f080..bc18cd289e05 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
@@ -82,6 +82,7 @@ import java.util.concurrent.Executor;
*/
@Presubmit
@SmallTest
+@SuppressWarnings("GuardedBy")
@RunWith(AndroidJUnit4.class)
public class DividerPresenterTest {
@Rule
@@ -186,7 +187,8 @@ public class DividerPresenterTest {
mDividerPresenter.updateDivider(
mTransaction,
mParentInfo,
- mSplitContainer);
+ mSplitContainer,
+ false /* isTaskFragmentVanished */);
assertNotEquals(mProperties, mDividerPresenter.mProperties);
verify(mRenderer).update();
@@ -206,7 +208,8 @@ public class DividerPresenterTest {
mDividerPresenter.updateDivider(
mTransaction,
mParentInfo,
- mSplitContainer);
+ mSplitContainer,
+ false /* isTaskFragmentVanished */);
assertNotEquals(mProperties, mDividerPresenter.mProperties);
verify(mRenderer).update();
@@ -222,7 +225,8 @@ public class DividerPresenterTest {
mDividerPresenter.updateDivider(
mTransaction,
mParentInfo,
- mSplitContainer);
+ mSplitContainer,
+ false /* isTaskFragmentVanished */);
assertEquals(mProperties, mDividerPresenter.mProperties);
verify(mRenderer, never()).update();
@@ -234,7 +238,42 @@ public class DividerPresenterTest {
mDividerPresenter.updateDivider(
mTransaction,
mParentInfo,
- null /* splitContainer */);
+ null /* splitContainer */,
+ false /* isTaskFragmentVanished */);
+ final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder(
+ OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE)
+ .build();
+
+ verify(mTransaction).addTaskFragmentOperation(
+ mPrimaryContainerToken, taskFragmentOperation);
+ verify(mRenderer).release();
+ assertNull(mDividerPresenter.mRenderer);
+ assertNull(mDividerPresenter.mProperties);
+ assertNull(mDividerPresenter.mDecorSurfaceOwner);
+ }
+
+ @Test
+ public void testUpdateDivider_noChangeWhenHasContainersToFinishButTaskFragmentNotVanished() {
+ mDividerPresenter.setHasContainersToFinish(true);
+ mDividerPresenter.updateDivider(
+ mTransaction,
+ mParentInfo,
+ null /* splitContainer */,
+ false /* isTaskFragmentVanished */);
+
+ assertEquals(mProperties, mDividerPresenter.mProperties);
+ verify(mRenderer, never()).update();
+ verify(mTransaction, never()).addTaskFragmentOperation(any(), any());
+ }
+
+ @Test
+ public void testUpdateDivider_dividerRemovedWhenHasContainersToFinishAndTaskFragmentVanished() {
+ mDividerPresenter.setHasContainersToFinish(true);
+ mDividerPresenter.updateDivider(
+ mTransaction,
+ mParentInfo,
+ null /* splitContainer */,
+ true /* isTaskFragmentVanished */);
final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder(
OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE)
.build();
@@ -254,7 +293,8 @@ public class DividerPresenterTest {
mDividerPresenter.updateDivider(
mTransaction,
mParentInfo,
- mSplitContainer);
+ mSplitContainer,
+ false /* isTaskFragmentVanished */);
final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder(
OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE)
.build();
@@ -660,82 +700,241 @@ public class DividerPresenterTest {
// Divider position is less than minPosition and the velocity is enough to be dismissed
assertEquals(
0, // Closed position
- DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
+ DividerPresenter.dividerPositionWithPositionOptions(
10 /* dividerPosition */,
30 /* minPosition */,
900 /* maxPosition */,
1200 /* fullyExpandedPosition */,
-dismissVelocity,
- displayDensity));
+ displayDensity,
+ true /* isDraggingToFullscreenAllowed */));
// Divider position is greater than maxPosition and the velocity is enough to be dismissed
assertEquals(
1200, // Fully expanded position
- DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
+ DividerPresenter.dividerPositionWithPositionOptions(
1000 /* dividerPosition */,
30 /* minPosition */,
900 /* maxPosition */,
1200 /* fullyExpandedPosition */,
dismissVelocity,
- displayDensity));
+ displayDensity,
+ true /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is returned when the velocity is not fast enough for fling and is in
+ // between minPosition and maxPosition
+ assertEquals(
+ 500, // dividerPosition is not snapped
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 500 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ nonFlingVelocity,
+ displayDensity,
+ true /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is snapped when the velocity is not fast enough for fling and larger
+ // than maxPosition
+ assertEquals(
+ 900, // Closest position is maxPosition
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 950 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ nonFlingVelocity,
+ displayDensity,
+ true /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is snapped when the velocity is not fast enough for fling and smaller
+ // than minPosition
+ assertEquals(
+ 30, // Closest position is minPosition
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 20 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ nonFlingVelocity,
+ displayDensity,
+ true /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is in the closed to maxPosition bounds and the velocity is enough for
+ // backward fling
+ assertEquals(
+ 2000, // maxPosition
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 2200 /* dividerPosition */,
+ 1000 /* minPosition */,
+ 2000 /* maxPosition */,
+ 2500 /* fullyExpandedPosition */,
+ -flingVelocity,
+ displayDensity,
+ true /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is not in the closed to maxPosition bounds and the velocity is enough
+ // for backward fling
+ assertEquals(
+ 1000, // minPosition
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 1200 /* dividerPosition */,
+ 1000 /* minPosition */,
+ 2000 /* maxPosition */,
+ 2500 /* fullyExpandedPosition */,
+ -flingVelocity,
+ displayDensity,
+ true /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is in the closed to minPosition bounds and the velocity is enough for
+ // forward fling
+ assertEquals(
+ 1000, // minPosition
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 500 /* dividerPosition */,
+ 1000 /* minPosition */,
+ 2000 /* maxPosition */,
+ 2500 /* fullyExpandedPosition */,
+ flingVelocity,
+ displayDensity,
+ true /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is not in the closed to minPosition bounds and the velocity is enough
+ // for forward fling
+ assertEquals(
+ 2000, // maxPosition
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 1200 /* dividerPosition */,
+ 1000 /* minPosition */,
+ 2000 /* maxPosition */,
+ 2500 /* fullyExpandedPosition */,
+ flingVelocity,
+ displayDensity,
+ true /* isDraggingToFullscreenAllowed */));
+ }
+
+ @Test
+ public void testDividerPositionWithDraggingToFullscreenNotAllowed() {
+ final float displayDensity = 600F;
+ final float nonFlingVelocity = MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity - 10f;
+ final float flingVelocity = MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity + 10f;
// Divider position is returned when the velocity is not fast enough for fling and is in
// between minPosition and maxPosition
assertEquals(
500, // dividerPosition is not snapped
- DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
+ DividerPresenter.dividerPositionWithPositionOptions(
500 /* dividerPosition */,
30 /* minPosition */,
900 /* maxPosition */,
1200 /* fullyExpandedPosition */,
nonFlingVelocity,
- displayDensity));
+ displayDensity,
+ false /* isDraggingToFullscreenAllowed */));
// Divider position is snapped when the velocity is not fast enough for fling and larger
// than maxPosition
assertEquals(
900, // Closest position is maxPosition
- DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
+ DividerPresenter.dividerPositionWithPositionOptions(
950 /* dividerPosition */,
30 /* minPosition */,
900 /* maxPosition */,
1200 /* fullyExpandedPosition */,
nonFlingVelocity,
- displayDensity));
+ displayDensity,
+ false /* isDraggingToFullscreenAllowed */));
// Divider position is snapped when the velocity is not fast enough for fling and smaller
// than minPosition
assertEquals(
30, // Closest position is minPosition
- DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
+ DividerPresenter.dividerPositionWithPositionOptions(
20 /* dividerPosition */,
30 /* minPosition */,
900 /* maxPosition */,
1200 /* fullyExpandedPosition */,
nonFlingVelocity,
- displayDensity));
+ displayDensity,
+ false /* isDraggingToFullscreenAllowed */));
- // Divider position is greater than minPosition and the velocity is enough for fling
+ // Divider position is snapped when the velocity is not fast enough for fling and at the
+ // closed position
assertEquals(
- 30, // minPosition
- DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
- 50 /* dividerPosition */,
+ 30, // Closest position is minPosition
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 0 /* dividerPosition */,
30 /* minPosition */,
900 /* maxPosition */,
1200 /* fullyExpandedPosition */,
- -flingVelocity,
- displayDensity));
+ nonFlingVelocity,
+ displayDensity,
+ false /* isDraggingToFullscreenAllowed */));
- // Divider position is less than maxPosition and the velocity is enough for fling
+ // Divider position is snapped when the velocity is not fast enough for fling and at the
+ // fully expanded position
assertEquals(
- 900, // maxPosition
- DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
- 800 /* dividerPosition */,
+ 900, // Closest position is maxPosition
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 1200 /* dividerPosition */,
30 /* minPosition */,
900 /* maxPosition */,
1200 /* fullyExpandedPosition */,
+ nonFlingVelocity,
+ displayDensity,
+ false /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is in the closed to maxPosition bounds and the velocity is enough for
+ // backward fling
+ assertEquals(
+ 2000, // maxPosition
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 2200 /* dividerPosition */,
+ 1000 /* minPosition */,
+ 2000 /* maxPosition */,
+ 2500 /* fullyExpandedPosition */,
+ -flingVelocity,
+ displayDensity,
+ false /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is not in the closed to maxPosition bounds and the velocity is enough
+ // for backward fling
+ assertEquals(
+ 1000, // minPosition
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 1200 /* dividerPosition */,
+ 1000 /* minPosition */,
+ 2000 /* maxPosition */,
+ 2500 /* fullyExpandedPosition */,
+ -flingVelocity,
+ displayDensity,
+ false /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is in the closed to minPosition bounds and the velocity is enough for
+ // forward fling
+ assertEquals(
+ 1000, // minPosition
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 500 /* dividerPosition */,
+ 1000 /* minPosition */,
+ 2000 /* maxPosition */,
+ 2500 /* fullyExpandedPosition */,
+ flingVelocity,
+ displayDensity,
+ false /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is not in the closed to minPosition bounds and the velocity is enough
+ // for forward fling
+ assertEquals(
+ 2000, // maxPosition
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 1200 /* dividerPosition */,
+ 1000 /* minPosition */,
+ 2000 /* maxPosition */,
+ 2500 /* fullyExpandedPosition */,
flingVelocity,
- displayDensity));
+ displayDensity,
+ false /* isDraggingToFullscreenAllowed */));
}
private TaskFragmentContainer createMockTaskFragmentContainer(
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
index 0972d40f33e3..7a0b9a0ece6b 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
@@ -409,6 +409,22 @@ public class OverlayPresentationTest {
}
@Test
+ public void testSanitizeBounds_taskInSplitScreen() {
+ final TaskFragmentContainer overlayContainer =
+ createTestOverlayContainer(TASK_ID, "test1");
+ TaskContainer taskContainer = overlayContainer.getTaskContainer();
+ spyOn(taskContainer);
+ doReturn(new Rect(TASK_BOUNDS.left + TASK_BOUNDS.width() / 2, TASK_BOUNDS.top,
+ TASK_BOUNDS.right, TASK_BOUNDS.bottom)).when(taskContainer).getBounds();
+ final Rect taskBounds = taskContainer.getBounds();
+ final Rect bounds = new Rect(taskBounds.width() / 2, 0, taskBounds.width(),
+ taskBounds.height());
+
+ assertThat(sanitizeBounds(bounds, null, overlayContainer)
+ .isEmpty()).isFalse();
+ }
+
+ @Test
public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_createOverlay() {
final Rect bounds = new Rect(0, 0, 100, 100);
mSplitController.setActivityStackAttributesCalculator(params ->
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 640b1fced455..efeec82b782e 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -200,12 +200,14 @@ public class SplitControllerTest {
public void testOnTaskFragmentVanished() {
final TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity);
doReturn(tf.getTaskFragmentToken()).when(mInfo).getFragmentToken();
+ doReturn(createTestTaskContainer()).when(mSplitController).getTaskContainer(TASK_ID);
// The TaskFragment has been removed in the server, we only need to cleanup the reference.
- mSplitController.onTaskFragmentVanished(mTransaction, mInfo);
+ mSplitController.onTaskFragmentVanished(mTransaction, mInfo, TASK_ID);
verify(mSplitPresenter, never()).deleteTaskFragment(any(), any());
verify(mSplitController).removeContainer(tf);
+ verify(mSplitController).updateDivider(any(), any(), anyBoolean());
verify(mTransaction, never()).finishActivity(any());
}
@@ -1152,7 +1154,7 @@ public class SplitControllerTest {
.setTaskFragmentInfo(info));
mSplitController.onTransactionReady(transaction);
- verify(mSplitController).onTaskFragmentVanished(any(), eq(info));
+ verify(mSplitController).onTaskFragmentVanished(any(), eq(info), anyInt());
verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(),
anyInt(), anyBoolean());
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java
new file mode 100644
index 000000000000..ff0a82fe05d6
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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 androidx.window.extensions.layout;
+
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+
+/**
+ * Test class for {@link WindowLayoutComponentImpl}.
+ *
+ * Build/Install/Run:
+ * atest WMJetpackUnitTests:WindowLayoutComponentImplTest
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class WindowLayoutComponentImplTest {
+
+ private WindowLayoutComponentImpl mWindowLayoutComponent;
+
+ @Before
+ public void setUp() {
+ mWindowLayoutComponent = new WindowLayoutComponentImpl(
+ ApplicationProvider.getApplicationContext(),
+ mock(DeviceStateManagerFoldingFeatureProducer.class));
+ }
+
+ @Test
+ public void testAddWindowLayoutListenerOnFakeUiContext_noCrash() {
+ final Context fakeUiContext = createTestContext();
+
+ mWindowLayoutComponent.addWindowLayoutInfoListener(fakeUiContext, info -> {});
+
+ mWindowLayoutComponent.onDisplayFeaturesChanged(Collections.emptyList());
+ }
+
+ private static Context createTestContext() {
+ return new FakeUiContext(ApplicationProvider.getApplicationContext());
+ }
+
+ /**
+ * A {@link android.content.Context} overrides {@link android.content.Context#isUiContext} to
+ * {@code true}.
+ */
+ private static class FakeUiContext extends ContextWrapper {
+
+ FakeUiContext(Context base) {
+ super(base);
+ }
+
+ @Override
+ public boolean isUiContext() {
+ return true;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 89781fd650a4..25d3067a34bc 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -51,6 +51,7 @@ filegroup {
"src/com/android/wm/shell/common/split/SplitScreenConstants.java",
"src/com/android/wm/shell/common/TransactionPool.java",
"src/com/android/wm/shell/common/TriangleShape.java",
+ "src/com/android/wm/shell/common/desktopmode/*.kt",
"src/com/android/wm/shell/draganddrop/DragAndDropConstants.java",
"src/com/android/wm/shell/pip/PipContentOverlay.java",
"src/com/android/wm/shell/startingsurface/SplashScreenExitAnimationUtils.java",
@@ -205,6 +206,7 @@ android_library {
"androidx.core_core-animation",
"androidx.core_core-ktx",
"androidx.arch.core_core-runtime",
+ "androidx.compose.material3_material3",
"androidx-constraintlayout_constraintlayout",
"androidx.dynamicanimation_dynamicanimation",
"androidx.recyclerview_recyclerview",
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index 7a986835359a..52ae93f5ebf1 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -29,6 +29,31 @@
android:name=".desktopmode.DesktopWallpaperActivity"
android:excludeFromRecents="true"
android:launchMode="singleInstance"
+ android:showForAllUsers="true"
android:theme="@style/DesktopWallpaperTheme" />
+
+ <activity
+ android:name=".bubbles.shortcut.CreateBubbleShortcutActivity"
+ android:exported="true"
+ android:excludeFromRecents="true"
+ android:theme="@android:style/Theme.NoDisplay"
+ android:label="Bubbles"
+ android:icon="@drawable/ic_bubbles_shortcut_widget">
+ <intent-filter>
+ <action android:name="android.intent.action.CREATE_SHORTCUT" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".bubbles.shortcut.ShowBubblesActivity"
+ android:exported="true"
+ android:excludeFromRecents="true"
+ android:theme="@android:style/Theme.NoDisplay" >
+ <intent-filter>
+ <action android:name="com.android.wm.shell.bubbles.action.SHOW_BUBBLES"/>
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
</application>
</manifest>
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 15f8c328bb56..3b7eb292abc7 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -111,3 +111,23 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "animate_bubble_size_change"
+ namespace: "multitasking"
+ description: "Turns on the animation for bubble bar icons size change"
+ bug: "335575529"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
+ name: "enable_taskbar_on_phones"
+ namespace: "multitasking"
+ description: "Enables taskbar on phones"
+ bug: "348007377"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
new file mode 100644
index 000000000000..c6dbd9b25e7f
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp
@@ -0,0 +1,61 @@
+// 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+ default_team: "trendy_team_multitasking_windowing",
+}
+
+android_test {
+ name: "WMShellMultivalentScreenshotTestsOnDevice",
+ srcs: [
+ "src/**/*.kt",
+ ],
+ static_libs: [
+ "WindowManager-Shell",
+ "junit",
+ "androidx.test.runner",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "truth",
+ "platform-parametric-runner-lib",
+ "platform-screenshot-diff-core",
+ ],
+ libs: [
+ "android.test.base",
+ "android.test.runner",
+ ],
+ jni_libs: [
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+ optimize: {
+ enabled: false,
+ },
+ test_suites: ["device-tests"],
+ platform_apis: true,
+ certificate: "platform",
+ aaptflags: [
+ "--extra-packages",
+ "com.android.wm.shell",
+ ],
+ manifest: "AndroidManifest.xml",
+ asset_dirs: ["goldens/onDevice"],
+}
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifest.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifest.xml
new file mode 100644
index 000000000000..467dc6a5cb81
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.wm.shell.multivalentscreenshot">
+
+ <application android:debuggable="true" android:supportsRtl="true" >
+ <uses-library android:name="android.test.runner" />
+ <activity
+ android:name="platform.test.screenshot.ScreenshotActivity"
+ android:exported="true">
+ </activity>
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:label="Multivalent screenshot tests for WindowManager-Shell"
+ android:targetPackage="com.android.wm.shell.multivalentscreenshot">
+ </instrumentation>
+
+ <!-- this permission is required by Tuner Service in screenshot tests -->
+ <uses-permission android:name="android.permission.MANAGE_USERS" />
+</manifest>
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml
new file mode 100644
index 000000000000..a7a3f1313a9b
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidManifestRobolectric.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.wm.shell.multivalentscreenshot">
+ <application android:debuggable="true" android:supportsRtl="true">
+ <uses-library android:name="android.test.runner" />
+ <activity
+ android:name="platform.test.screenshot.ScreenshotActivity"
+ android:exported="true">
+ </activity>
+ </application>
+</manifest>
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidTest.xml b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidTest.xml
new file mode 100644
index 000000000000..75793ae69d27
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/AndroidTest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs Tests for WindowManagerShellLib">
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="install-arg" value="-t" />
+ <option name="test-file-name" value="WMShellMultivalentScreenshotTestsOnDevice.apk" />
+ </target_preparer>
+
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-tag" value="WMShellMultivalentScreenshotTestsOnDevice" />
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name="directory-keys" value="/data/user/0/com.android.wm.shell.multivalentscreenshot/files/wmshell_screenshots" />
+ <option name="collect-on-run-ended-only" value="true" />
+ </metrics_collector>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.wm.shell.multivalentscreenshot" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
new file mode 100644
index 000000000000..eb2888199ddf
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
new file mode 100644
index 000000000000..eb2888199ddf
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png
Binary files differ
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/robolectric/config/robolectric.properties b/libs/WindowManager/Shell/multivalentScreenshotTests/robolectric/config/robolectric.properties
new file mode 100644
index 000000000000..7a0527ccaafb
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/robolectric/config/robolectric.properties
@@ -0,0 +1,2 @@
+sdk=NEWEST_SDK
+
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt
new file mode 100644
index 000000000000..d35f493a8f60
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/bubbles/BubbleEducationViewScreenshotTest.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.wm.shell.bubbles
+
+import android.view.LayoutInflater
+import com.android.wm.shell.common.bubbles.BubblePopupView
+import com.android.wm.shell.testing.goldenpathmanager.WMShellGoldenPathManager
+import com.android.wm.shell.R
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays
+import platform.test.screenshot.ViewScreenshotTestRule
+import platform.test.screenshot.getEmulatedDevicePathConfig
+
+@RunWith(ParameterizedAndroidJunit4::class)
+class BubbleEducationViewScreenshotTest(emulationSpec: DeviceEmulationSpec) {
+ companion object {
+ @Parameters(name = "{0}")
+ @JvmStatic
+ fun getTestSpecs() = DeviceEmulationSpec.forDisplays(Displays.Phone, isLandscape = false)
+ }
+
+ @get:Rule
+ val screenshotRule =
+ ViewScreenshotTestRule(
+ emulationSpec,
+ WMShellGoldenPathManager(getEmulatedDevicePathConfig(emulationSpec))
+ )
+
+ @Test
+ fun bubblesEducation() {
+ screenshotRule.screenshotTest("bubbles_education") { activity ->
+ activity.actionBar?.hide()
+ val view =
+ LayoutInflater.from(activity)
+ .inflate(R.layout.bubble_bar_stack_education, null) as BubblePopupView
+ view.setup()
+ view
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/testing/goldenpathmanager/WMShellGoldenPathManager.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/testing/goldenpathmanager/WMShellGoldenPathManager.kt
new file mode 100644
index 000000000000..901b79b9b1a0
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/testing/goldenpathmanager/WMShellGoldenPathManager.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.wm.shell.testing.goldenpathmanager
+
+import android.os.Build
+import androidx.test.platform.app.InstrumentationRegistry
+import platform.test.screenshot.GoldenPathManager
+import platform.test.screenshot.PathConfig
+
+/** A WM Shell specific implementation of [GoldenPathManager]. */
+class WMShellGoldenPathManager(pathConfig: PathConfig) :
+ GoldenPathManager(
+ appContext = InstrumentationRegistry.getInstrumentation().context,
+ assetsPathRelativeToBuildRoot = assetPath,
+ deviceLocalPath = deviceLocalPath,
+ pathConfig = pathConfig,
+ ) {
+
+ private companion object {
+ private const val ASSETS_PATH =
+ "frameworks/base/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice"
+ private const val ASSETS_PATH_ROBO =
+ "frameworks/base/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/" +
+ "robolectric"
+ private val assetPath: String
+ get() = if (Build.FINGERPRINT.contains("robolectric")) ASSETS_PATH_ROBO else ASSETS_PATH
+ private val deviceLocalPath: String
+ get() =
+ InstrumentationRegistry.getInstrumentation()
+ .targetContext
+ .filesDir
+ .absolutePath
+ .toString() + "/wmshell_screenshots"
+ }
+ override fun toString(): String {
+ // This string is appended to all actual/expected screenshots on the device, so make sure
+ // it is a static value.
+ return "WMShellGoldenPathManager"
+ }
+}
diff --git a/libs/WindowManager/Shell/multivalentScreenshotTestsForDevice b/libs/WindowManager/Shell/multivalentScreenshotTestsForDevice
new file mode 120000
index 000000000000..e879efc81ec1
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentScreenshotTestsForDevice
@@ -0,0 +1 @@
+multivalentScreenshotTests \ No newline at end of file
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
index 0efdbdc9376c..327e2059557c 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
@@ -456,5 +456,7 @@ class BubbleStackViewTest {
override fun isStackExpanded(): Boolean = false
override fun isShowingAsBubbleBar(): Boolean = false
+
+ override fun hideCurrentInputMethod() {}
}
}
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml
index 04ad572e046f..a30cfb74bf4a 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml
@@ -20,5 +20,6 @@
android:shape="rectangle">
<corners
android:radius="@dimen/desktop_mode_maximize_menu_buttons_outline_radius"/>
+ <solid android:color="?androidprv:attr/materialColorSurfaceContainerLow"/>
<stroke android:width="1dp" android:color="?androidprv:attr/materialColorOutlineVariant"/>
</shape> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_header_background.xml b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget.xml
index 50c5ca936245..b208f2fea7b2 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_header_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget.xml
@@ -13,16 +13,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:id="@+id/backLayer">
- <shape android:shape="rectangle">
- <solid android:color="#000000" />
- </shape>
- </item>
-
- <item android:id="@+id/frontLayer">
- <shape android:shape="rectangle">
- <solid android:color="#000000" />
- </shape>
- </item>
-</layer-list> \ No newline at end of file
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_bubbles_shortcut_widget_background" />
+ <foreground android:drawable="@drawable/ic_bubbles_shortcut_widget_foreground" />
+</adaptive-icon>
diff --git a/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_background.xml b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_background.xml
new file mode 100644
index 000000000000..510221fb2859
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_background.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108">
+ <path
+ android:pathData="M0,0h108v108h-108z"
+ android:fillColor="#FFC20C"/>
+</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml
new file mode 100644
index 000000000000..a41b6a961bb2
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget_foreground.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="@android:color/white">
+ <group android:scaleX="0.58"
+ android:scaleY="0.58"
+ android:translateX="5.04"
+ android:translateY="5.04">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M7.2,14.4m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M14.8,18m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M15.2,8.8m-4.8,0a4.8,4.8 0,1 1,9.6 0a4.8,4.8 0,1 1,-9.6 0"/>
+ </group>
+</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
index 34f03c2f226b..501bedd50f55 100644
--- a/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_expanded_view.xml
@@ -19,7 +19,7 @@
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:orientation="vertical"
- android:id="@+id/bubble_bar_expanded_view">
+ android:id="@+id/bubble_expanded_view">
<com.android.wm.shell.bubbles.bar.BubbleBarHandleView
android:id="@+id/bubble_bar_handle_view"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml
index c0ff1922edc8..c0ff1922edc8 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
index 84e144972f38..7b31c1420a7c 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
@@ -19,7 +19,6 @@
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/desktop_mode_caption"
- android:background="@drawable/desktop_mode_header_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
index 9599658384f0..7d5f9cdbebc8 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml
@@ -31,23 +31,15 @@
android:layout_height="wrap_content"
android:orientation="vertical">
- <FrameLayout
- android:id="@+id/maximize_menu_maximize_button_layout"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@drawable/desktop_mode_maximize_menu_layout_background"
- android:padding="4dp"
+ <Button
+ android:layout_width="94dp"
+ android:layout_height="60dp"
+ android:id="@+id/maximize_menu_maximize_button"
+ style="?android:attr/buttonBarButtonStyle"
+ android:stateListAnimator="@null"
android:layout_marginRight="8dp"
android:layout_marginBottom="4dp"
- android:alpha="0">
- <Button
- android:id="@+id/maximize_menu_maximize_button"
- style="?android:attr/buttonBarButtonStyle"
- android:layout_width="86dp"
- android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
- android:background="@drawable/desktop_mode_maximize_menu_button_background"
- android:stateListAnimator="@null"/>
- </FrameLayout>
+ android:alpha="0"/>
<TextView
android:id="@+id/maximize_menu_maximize_window_text"
diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml
index 1c8f5e60c5c9..8b328e2c79cf 100644
--- a/libs/WindowManager/Shell/res/values-af/strings.xml
+++ b/libs/WindowManager/Shell/res/values-af/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Borrel"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Bestuur"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Borrel is toegemaak."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Tik om hierdie app te herbegin vir ’n beter aansig"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Verander hierdie app se aspekverhouding in Instellings"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Verander aspekverhouding"</string>
diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml
index 81ab3ab15aad..b005a01711eb 100644
--- a/libs/WindowManager/Shell/res/values-am/strings.xml
+++ b/libs/WindowManager/Shell/res/values-am/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"አረፋ"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"ያቀናብሩ"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"አረፋ ተሰናብቷል።"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"ለተሻለ ዕይታ ይህን መተግበሪያ እንደገና ለመጀመር መታ ያድርጉ"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"የዚህን መተግበሪያ ምጥጥነ ገፅታ በቅንብሮች ውስጥ ይለውጡ"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"የምጥጥነ ገፅታ ለውጥ"</string>
diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml
index 3974c39d4803..8c283d33eff6 100644
--- a/libs/WindowManager/Shell/res/values-ar/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ar/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"فقاعة"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"إدارة"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"تم إغلاق الفقاعة."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"انقر لإعادة تشغيل هذا التطبيق للحصول على تجربة عرض أفضل."</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"يمكنك تغيير نسبة العرض إلى الارتفاع لهذا التطبيق من خلال \"الإعدادات\"."</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"تغيير نسبة العرض إلى الارتفاع"</string>
diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml
index a1ce1b3b9513..ef92587ad274 100644
--- a/libs/WindowManager/Shell/res/values-as/strings.xml
+++ b/libs/WindowManager/Shell/res/values-as/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"বাবল"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"পৰিচালনা কৰক"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"বাবল অগ্ৰাহ্য কৰা হৈছে"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"উন্নত ভিউ পোৱাৰ বাবে এপ্‌টো ৰিষ্টাৰ্ট কৰিবলৈ টিপক"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ছেটিঙলৈ গৈ এই এপ্‌টোৰ আকাৰৰ অনুপাত সলনি কৰক"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"আকাৰৰ অনুপাত সলনি কৰক"</string>
diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml
index 71dfe5ac6bed..04b2f1c23bc4 100644
--- a/libs/WindowManager/Shell/res/values-az/strings.xml
+++ b/libs/WindowManager/Shell/res/values-az/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Qabarcıq"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"İdarə edin"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Qabarcıqdan imtina edilib."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Yaxşı görünüş üçün toxunaraq bu tətbiqi yenidən başladın"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Ayarlarda bu tətbiqin tərəflər nisbətini dəyişin"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Tərəflər nisbətini dəyişin"</string>
diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml
index f48360991d49..47bc105f19bc 100644
--- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Oblačić"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljajte"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić je odbačen."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Dodirnite da biste restartovali ovu aplikaciju radi boljeg prikaza"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Promenite razmeru ove aplikacije u Podešavanjima"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Promeni razmeru"</string>
diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml
index 532ecc64358f..6ad7553a8383 100644
--- a/libs/WindowManager/Shell/res/values-be/strings.xml
+++ b/libs/WindowManager/Shell/res/values-be/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Усплывальнае апавяшчэнне"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Кіраваць"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Усплывальнае апавяшчэнне адхілена."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Націсніце, каб перазапусціць гэту праграму для зручнейшага прагляду"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Змяніць суадносіны бакоў для гэтай праграмы ў наладах"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Змяніць суадносіны бакоў"</string>
diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml
index 8f828badcf47..a9e0bce81376 100644
--- a/libs/WindowManager/Shell/res/values-bg/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bg/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Балонче"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Управление"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Балончето е отхвърлено."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Докоснете, за да рестартирате това приложение с цел по-добър изглед"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Променете съотношението на това приложение в „Настройки“"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Промяна на съотношението"</string>
diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml
index e0a2ea824be0..29de1007e311 100644
--- a/libs/WindowManager/Shell/res/values-bn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bn/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"বাবল"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"ম্যানেজ করুন"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"বাবল বাতিল করা হয়েছে।"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"আরও ভাল ভিউয়ের জন্য এই অ্যাপ রিস্টার্ট করতে ট্যাপ করুন"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"সেটিংস থেকে এই অ্যাপের অ্যাস্পেক্ট রেশিও পরিবর্তন করুন"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"অ্যাস্পেক্ট রেশিও পরিবর্তন করুন"</string>
diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml
index 41c72c1d3a03..5f1da7571d95 100644
--- a/libs/WindowManager/Shell/res/values-bs/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bs/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Oblačić"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljaj"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić je odbačen."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Dodirnite da ponovo pokrenete ovu aplikaciju radi boljeg prikaza"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Promijenite format slike aplikacije u Postavkama"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Promijenite format slike"</string>
diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml
index 679227248ea5..d70de794ba17 100644
--- a/libs/WindowManager/Shell/res/values-ca/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ca/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bombolla"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Gestiona"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"La bombolla s\'ha ignorat."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Toca per reiniciar aquesta aplicació i obtenir una millor visualització"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Canvia la relació d\'aspecte d\'aquesta aplicació a Configuració"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Canvia la relació d\'aspecte"</string>
diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml
index aafb2e16b703..ca00fec12e86 100644
--- a/libs/WindowManager/Shell/res/values-cs/strings.xml
+++ b/libs/WindowManager/Shell/res/values-cs/strings.xml
@@ -84,7 +84,11 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bublina"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Spravovat"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bublina byla zavřena."</string>
- <string name="restart_button_description" msgid="4564728020654658478">"Klepnutím tuto aplikaci kvůli lepšímu zobrazení restartujete"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
+ <string name="restart_button_description" msgid="4564728020654658478">"Klepnutím tuto aplikaci restartujete kvůli lepšímu zobrazení"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Změnit v Nastavení poměr stran této aplikace"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Změnit poměr stran"</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Problémy s fotoaparátem?\nKlepnutím vyřešíte"</string>
diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml
index 8878910a4d2c..d50d2f0f135d 100644
--- a/libs/WindowManager/Shell/res/values-da/strings.xml
+++ b/libs/WindowManager/Shell/res/values-da/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Boble"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Administrer"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Boblen blev lukket."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Tryk for at genstarte denne app, så visningen forbedres"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Skift denne apps billedformat i Indstillinger"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Skift billedformat"</string>
diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml
index bcdc2a9c8539..7f44f83a1790 100644
--- a/libs/WindowManager/Shell/res/values-de/strings.xml
+++ b/libs/WindowManager/Shell/res/values-de/strings.xml
@@ -48,8 +48,8 @@
<string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % oben"</string>
<string name="accessibility_action_divider_top_30" msgid="3572788224908570257">"30 % oben"</string>
<string name="accessibility_action_divider_bottom_full" msgid="2831868345092314060">"Vollbild unten"</string>
- <string name="accessibility_split_left" msgid="1713683765575562458">"Links teilen"</string>
- <string name="accessibility_split_right" msgid="8441001008181296837">"Rechts teilen"</string>
+ <string name="accessibility_split_left" msgid="1713683765575562458">"Links positionieren"</string>
+ <string name="accessibility_split_right" msgid="8441001008181296837">"Rechts positionieren"</string>
<string name="accessibility_split_top" msgid="2789329702027147146">"Oben teilen"</string>
<string name="accessibility_split_bottom" msgid="8694551025220868191">"Unten teilen"</string>
<string name="one_handed_tutorial_title" msgid="4583241688067426350">"Einhandmodus wird verwendet"</string>
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Verwalten"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble verworfen."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Tippen, um diese App neu zu starten und die Ansicht zu verbessern"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Seitenverhältnis der App in den Einstellungen ändern"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Seitenverhältnis ändern"</string>
diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml
index 14e5e2f87ab8..a3a5ccd839e4 100644
--- a/libs/WindowManager/Shell/res/values-el/strings.xml
+++ b/libs/WindowManager/Shell/res/values-el/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Συννεφάκι"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Διαχείριση"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Το συννεφάκι παραβλέφθηκε."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Πατήστε για να επανεκκινήσετε αυτή την εφαρμογή για καλύτερη προβολή"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Αλλάξτε τον λόγο διαστάσεων αυτής της εφαρμογής στις Ρυθμίσεις"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Αλλαγή λόγου διαστάσεων"</string>
diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
index 7427b62679be..edc4f4e25c2a 100644
--- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Tap to restart this app for a better view"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Change this app\'s aspect ratio in Settings"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Change aspect ratio"</string>
diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
index cb9ee4f6b6b3..e537f0a80144 100644
--- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Tap to restart this app for a better view"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Change this app\'s aspect ratio in Settings"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Change aspect ratio"</string>
diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
index 7427b62679be..edc4f4e25c2a 100644
--- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Tap to restart this app for a better view"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Change this app\'s aspect ratio in Settings"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Change aspect ratio"</string>
diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
index 7427b62679be..edc4f4e25c2a 100644
--- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Manage"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubble dismissed."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Tap to restart this app for a better view"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Change this app\'s aspect ratio in Settings"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Change aspect ratio"</string>
diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
index 8498807f9fdb..bdcd275d9c14 100644
--- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‎‎‎‏‏‎‏‎‏‎‏‏‎‏‎‏‏‎‏‎‎‏‏‏‏‎‏‏‎‏‏‎‏‏‎‎‏‏‎‏‏‎‏‏‎‎‎‏‏‏‏‏‎‎‎‎‏‎‎Bubble‎‏‎‎‏‎"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‎‎‏‎‎‎‏‎‏‏‎‎‎‏‏‏‎‏‎‎‎‎‏‎‎‎‏‏‎‎‏‎‎‎‏‎‎‏‏‎‎‎‏‎‏‎‎‏‏‏‎‎‏‏‎Manage‎‏‎‎‏‎"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‏‏‎‏‏‏‎‎‎‏‎‏‎‎‏‎‎‏‎‎‎‏‎‏‏‎‏‏‎‏‎‏‎‏‎‏‏‏‏‏‎‏‎Bubble dismissed.‎‏‎‎‏‎"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‏‏‏‎‏‎‏‏‎‎‏‎‎‏‎‏‎‏‏‏‏‏‏‎‎‏‏‎‎‏‎‎‏‏‏‎‏‏‎‎‏‎‎‏‎‎‏‎‎‏‏‏‎‏‎‏‏‏‎‎Tap to restart this app for a better view‎‏‎‎‏‎"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‏‎‎‎‏‏‎‎‏‎‏‎‏‎‏‎‎‏‎‏‎‎‏‏‏‏‏‏‎‏‎‏‏‏‏‎‏‎‏‏‎‎‎‏‎‎‎‎‎‏‎‎‏‏‏‎‎‎‎Change this app\'s aspect ratio in Settings‎‏‎‎‏‎"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‏‏‏‏‎‎‎‏‏‏‏‏‏‏‎‎‏‎‎‏‎‎‏‎‎‎‎‏‎‏‎‏‏‏‎‏‎‎‏‎‎‏‎‎‏‎‎‏‎‎‏‏‎‎‏‎‎‎Change aspect ratio‎‏‎‎‏‎"</string>
diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
index 406c1f37c455..8653e5932a04 100644
--- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Cuadro"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Administrar"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Se descartó el cuadro."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Presiona para reiniciar esta app y tener una mejor vista"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Cambiar la relación de aspecto de esta app en Configuración"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Cambiar relación de aspecto"</string>
diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml
index 0583d79da127..8f59c9c91d20 100644
--- a/libs/WindowManager/Shell/res/values-es/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Burbuja"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionar"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Burbuja cerrada."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Toca para reiniciar esta aplicación y obtener una mejor vista"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Cambiar la relación de aspecto de esta aplicación en Ajustes"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Cambiar relación de aspecto"</string>
diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml
index 70547f566ea6..3d86eb4a91d9 100644
--- a/libs/WindowManager/Shell/res/values-et/strings.xml
+++ b/libs/WindowManager/Shell/res/values-et/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Mull"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Halda"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Mullist loobuti."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Puudutage, et see rakendus parema vaate jaoks taaskäivitada"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Muutke selle rakenduse kuvasuhet jaotises Seaded"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Muutke kuvasuhet"</string>
diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml
index 4be35eac6c1f..4e7bdd246d10 100644
--- a/libs/WindowManager/Shell/res/values-eu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-eu/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Burbuila"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Kudeatu"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Baztertu da globoa."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Hobeto ikusteko, sakatu hau, eta aplikazioa berrabiarazi egingo da"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Aldatu aplikazioaren aspektu-erlazioa ezarpenetan"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Aldatu aspektu-erlazioa"</string>
diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml
index 32d5f5f34fb8..39100425a9ac 100644
--- a/libs/WindowManager/Shell/res/values-fa/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fa/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"حباب"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"مدیریت"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"حبابک رد شد."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"برای داشتن نمایی بهتر، ضربه بزنید تا این برنامه بازراه‌اندازی شود"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"نسبت ابعادی این برنامه را در «تنظیمات» تغییر دهید"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"تغییر نسبت ابعادی"</string>
diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml
index 6f03545e5542..577d625ba8f8 100644
--- a/libs/WindowManager/Shell/res/values-fi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fi/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Kupla"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Ylläpidä"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Kupla ohitettu."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Napauta, niin sovellus käynnistyy uudelleen paremmin näytölle sopivana"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Muuta tämän sovelluksen kuvasuhdetta Asetuksissa"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Vaihda kuvasuhdetta"</string>
diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
index 3492f136c4f9..74d822ac02c0 100644
--- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bulle"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Gérer"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulle ignorée."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Touchez pour redémarrer cette application afin d\'obtenir un meilleur affichage"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Changer les proportions de cette application dans les paramètres"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Modifier les proportions"</string>
diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml
index 4002e4d04d51..4d14d0b85f3e 100644
--- a/libs/WindowManager/Shell/res/values-fr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bulle"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Gérer"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulle fermée."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Appuyez pour redémarrer cette appli et obtenir une meilleure vue."</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Modifiez le format de cette appli dans les Paramètres."</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Modifier le format"</string>
diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml
index c371f7f62feb..e5b67c2aaad1 100644
--- a/libs/WindowManager/Shell/res/values-gl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-gl/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Burbulla"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Xestionar"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ignorouse a burbulla."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Toca o botón para reiniciar esta aplicación e gozar dunha mellor visualización"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Cambia a proporción desta aplicación en Configuración"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Cambiar a proporción"</string>
diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml
index 7e3d7a373be4..e2a52dccd8ea 100644
--- a/libs/WindowManager/Shell/res/values-gu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-gu/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"બબલ"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"મેનેજ કરો"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"બબલ છોડી દેવાયો."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"વધુ સારા વ્યૂ માટે, આ ઍપને ફરી શરૂ કરવા ટૅપ કરો"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"સેટિંગમાં આ ઍપનો સાપેક્ષ ગુણોત્તર બદલો"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"સાપેક્ષ ગુણોત્તર બદલો"</string>
diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml
index cd0f4e3618f7..f75e0e0528e1 100644
--- a/libs/WindowManager/Shell/res/values-hi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hi/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"मैनेज करें"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल खारिज किया गया."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"बेहतर व्यू पाने के लिए, टैप करके ऐप्लिकेशन को रीस्टार्ट करें"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"सेटिंग में जाकर इस ऐप्लिकेशन का आसपेक्ट रेशियो (लंबाई-चौड़ाई का अनुपात) बदलें"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"आसपेक्ट रेशियो (लंबाई-चौड़ाई का अनुपात) बदलें"</string>
diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml
index 27d4cfcf22d5..ed80c505d756 100644
--- a/libs/WindowManager/Shell/res/values-hr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hr/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Oblačić"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljanje"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblačić odbačen."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Dodirnite da biste ponovo pokrenuli tu aplikaciju kako biste bolje vidjeli"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Promijeni omjer slike ove aplikacije u postavkama"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Promijeni omjer slike"</string>
diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml
index a8cc5c120efc..32a31063bd90 100644
--- a/libs/WindowManager/Shell/res/values-hu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hu/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Buborék"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Kezelés"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Buborék elvetve."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"A jobb nézet érdekében koppintson az alkalmazás újraindításához."</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Az app méretarányát a Beállításokban módosíthatja"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Méretarány módosítása"</string>
diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml
index 7f372774241a..65ca704ded09 100644
--- a/libs/WindowManager/Shell/res/values-hy/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hy/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Պղպջակ"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Կառավարել"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ամպիկը փակվեց։"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Հպեք՝ հավելվածը վերագործարկելու և ավելի հարմար տեսք ընտրելու համար"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Փոխել հավելվածի կողմերի հարաբերակցությունը Կարգավորումներում"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Փոխել չափերի հարաբերակցությունը"</string>
diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml
index 3cf55fa0ede2..975dd72f67d7 100644
--- a/libs/WindowManager/Shell/res/values-in/strings.xml
+++ b/libs/WindowManager/Shell/res/values-in/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Balon"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Kelola"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balon ditutup."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Ketuk untuk memulai ulang aplikasi ini agar mendapatkan tampilan yang lebih baik"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Ubah rasio aspek aplikasi ini di Setelan"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Ubah rasio aspek"</string>
diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml
index 6aa56f9858ad..11c47189dce0 100644
--- a/libs/WindowManager/Shell/res/values-is/strings.xml
+++ b/libs/WindowManager/Shell/res/values-is/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Blaðra"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Stjórna"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Blöðru lokað."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Ýttu til að endurræsa forritið og fá betri sýn"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Breyta myndhlutfalli þessa forrits í stillingunum"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Breyta myndhlutfalli"</string>
diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml
index 3c1d5e4dac02..168c8cc5936f 100644
--- a/libs/WindowManager/Shell/res/values-it/strings.xml
+++ b/libs/WindowManager/Shell/res/values-it/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Fumetto"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Gestisci"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Fumetto ignorato."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Tocca per riavviare l\'app e migliorare la visualizzazione"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Cambia le proporzioni dell\'app nelle Impostazioni"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Cambia proporzioni"</string>
diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml
index a0c3b3a95ca8..fd4cd1adaa2a 100644
--- a/libs/WindowManager/Shell/res/values-iw/strings.xml
+++ b/libs/WindowManager/Shell/res/values-iw/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"בועה"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"ניהול"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"הבועה נסגרה."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"כדי לראות טוב יותר יש להקיש ולהפעיל את האפליקציה הזו מחדש"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"אפשר לשנות את יחס הגובה-רוחב של האפליקציה הזו ב\'הגדרות\'"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"שינוי יחס גובה-רוחב"</string>
diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml
index fb726c180997..64ddec9450ae 100644
--- a/libs/WindowManager/Shell/res/values-ja/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ja/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"バブル"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ふきだしが非表示になっています。"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"タップしてこのアプリを再起動すると、表示が適切になります"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"このアプリのアスペクト比を [設定] で変更します"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"アスペクト比を変更"</string>
diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml
index e9f620a17203..cab8807b86d1 100644
--- a/libs/WindowManager/Shell/res/values-ka/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ka/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"ბუშტი"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"მართვა"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ბუშტი დაიხურა."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"შეხებით გადატვირთეთ ეს აპი უკეთესი ხედის მისაღებად"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"შეცვალეთ ამ აპის თანაფარდობა პარამეტრებიდან"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"თანაფარდობის შეცვლა"</string>
diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml
index 34e41038f285..4ff5b85b36fd 100644
--- a/libs/WindowManager/Shell/res/values-kk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-kk/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Көпіршік"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Басқару"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Қалқыма хабар жабылды."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Көріністі жақсарту үшін осы қолданбаны түртіп, қайта ашыңыз."</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Осы қолданбаның арақатынасын параметрлерден өзгертуге болады."</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Арақатынасты өзгерту"</string>
diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml
index 362bbad4ec12..ba7a32495659 100644
--- a/libs/WindowManager/Shell/res/values-km/strings.xml
+++ b/libs/WindowManager/Shell/res/values-km/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"ពពុះ"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"គ្រប់គ្រង"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"បានច្រានចោល​សារលេចឡើង។"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"ចុចដើម្បី​ចាប់ផ្ដើម​កម្មវិធី​នេះឡើងវិញសម្រាប់ទិដ្ឋភាពកាន់តែប្រសើរ"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ផ្លាស់ប្ដូរសមាមាត្រ​របស់កម្មវិធីនេះនៅក្នុងការកំណត់"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"ប្ដូរ​​សមាមាត្រ"</string>
diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml
index 77cc4a44f81a..423e8d53a654 100644
--- a/libs/WindowManager/Shell/res/values-kn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-kn/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"ಬಬಲ್"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"ನಿರ್ವಹಿಸಿ"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ಬಬಲ್ ವಜಾಗೊಳಿಸಲಾಗಿದೆ."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"ಉತ್ತಮ ವೀಕ್ಷಣೆಗಾಗಿ ಈ ಆ್ಯಪ್ ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ಸೆಟ್ಟಿಂಗ್‌ಗಳಲ್ಲಿ ಈ ಆ್ಯಪ್‌ನ ದೃಶ್ಯಾನುಪಾತವನ್ನು ಬದಲಾಯಿಸಿ"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"ದೃಶ್ಯಾನುಪಾತವನ್ನು ಬದಲಾಯಿಸಿ"</string>
diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml
index e8b5522838b7..0d1c6216776b 100644
--- a/libs/WindowManager/Shell/res/values-ko/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ko/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"버블"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"관리"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"대화창을 닫았습니다."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"탭하면 앱을 다시 시작하여 보기를 개선합니다."</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"설정에서 앱의 가로세로 비율을 변경합니다."</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"가로세로 비율 변경"</string>
@@ -95,7 +99,7 @@
<string name="letterbox_education_reposition_text" msgid="4589957299813220661">"앱 위치를 조정하려면 앱 외부를 두 번 탭합니다."</string>
<string name="letterbox_education_got_it" msgid="4057634570866051177">"확인"</string>
<string name="letterbox_education_expand_button_description" msgid="1729796567101129834">"추가 정보는 펼쳐서 확인하세요."</string>
- <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"화면에 맞게 보도록 다시 시작할까요?"</string>
+ <string name="letterbox_restart_dialog_title" msgid="8543049527871033505">"화면에 맞게 보이도록 다시 시작할까요?"</string>
<string name="letterbox_restart_dialog_description" msgid="6096946078246557848">"앱을 다시 시작하면 화면에 더 잘 맞게 볼 수는 있지만 진행 상황 또는 저장되지 않은 변경사항을 잃을 수도 있습니다."</string>
<string name="letterbox_restart_cancel" msgid="1342209132692537805">"취소"</string>
<string name="letterbox_restart_restart" msgid="8529976234412442973">"다시 시작"</string>
diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml
index 302c0071a73a..f17e9ca891c0 100644
--- a/libs/WindowManager/Shell/res/values-ky/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ky/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Көбүк"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Башкаруу"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Калкып чыкма билдирме жабылды."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Жакшыраак көрүү үчүн бул колдонмону өчүрүп күйгүзүңүз. Ал үчүн таптап коюңуз"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Бул колдонмонун тараптарынын катнашын параметрлерден өзгөртүү"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Тараптардын катнашын өзгөртүү"</string>
diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml
index a3519636b71f..195e4d56a1c1 100644
--- a/libs/WindowManager/Shell/res/values-lo/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lo/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"ຟອງ"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"ຈັດການ"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ປິດ Bubble ໄສ້ແລ້ວ."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"ແຕະເພື່ອຣີສະຕາດແອັບນີ້ເພື່ອມຸມມອງທີ່ດີຂຶ້ນ"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ປ່ຽນອັດຕາສ່ວນຂອງແອັບນີ້ໃນການຕັ້ງຄ່າ"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"ປ່ຽນອັດຕາສ່ວນ"</string>
diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml
index e4dd7398f679..63ad580a81cc 100644
--- a/libs/WindowManager/Shell/res/values-lt/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lt/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Debesėlis"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Tvarkyti"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Debesėlio atsisakyta."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Palieskite, kad iš naujo paleistumėte šią programą ir matytumėte aiškiau"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Pakeiskite šios programos kraštinių santykį"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Keisti kraštinių santykį"</string>
diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml
index 99aebf626322..268d89324f54 100644
--- a/libs/WindowManager/Shell/res/values-lv/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lv/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Burbulis"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Pārvaldīt"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Burbulis ir noraidīts."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Pieskarieties, lai restartētu šo lietotni un uzlabotu attēlojumu."</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Iestatījumos mainiet šīs lietotnes malu attiecību."</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Mainīt malu attiecību"</string>
diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml
index c152c60fa631..0a0027fa1bae 100644
--- a/libs/WindowManager/Shell/res/values-mk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mk/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Балонче"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Управувајте"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Балончето е отфрлено."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Допрете за да ја рестартирате апликацијава за подобар приказ"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Промени го соодносот на апликацијава во „Поставки“"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Променување на соодносот"</string>
diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml
index 90275cdb517a..07809e1a0014 100644
--- a/libs/WindowManager/Shell/res/values-ml/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ml/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"ബബിൾ"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"മാനേജ് ചെയ്യുക"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ബബിൾ ഡിസ്മിസ് ചെയ്തു."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"മികച്ച കാഴ്‌ചയ്‌ക്കായി ഈ ആപ്പ് റീസ്‌റ്റാർട്ട് ചെയ്യാൻ ടാപ്പ് ചെയ്യുക"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ഈ ആപ്പിന്റെ വീക്ഷണ അനുപാതം, ക്രമീകരണത്തിൽ മാറ്റുക"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"വീക്ഷണ അനുപാതം മാറ്റുക"</string>
diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml
index 5e43506ab621..99bd2dffca53 100644
--- a/libs/WindowManager/Shell/res/values-mn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mn/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Бөмбөлөг"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Удирдах"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Бөмбөлгийг үл хэрэгссэн."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Харагдах байдлыг сайжруулахын тулд энэ аппыг товшиж, дахин эхлүүлнэ үү"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Энэ аппын харьцааг Тохиргоонд өөрчилнө үү"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Харьцааг өөрчлөх"</string>
diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml
index 5874bffc9199..ac57e0a549b4 100644
--- a/libs/WindowManager/Shell/res/values-mr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mr/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"व्यवस्थापित करा"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल डिसमिस केला."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"अधिक चांगल्या दृश्यासाठी हे अ‍ॅप रीस्टार्ट करण्याकरिता टॅप करा"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"सेटिंग्ज मध्ये या ॲपचा आस्पेक्ट रेशो बदला"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"आस्पेक्ट रेशो बदला"</string>
diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml
index 4de8a7b03547..6bc2fbb27c51 100644
--- a/libs/WindowManager/Shell/res/values-ms/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ms/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Gelembung"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Urus"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Gelembung diketepikan."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Ketik untuk memulakan semula apl ini untuk mendapatkan paparan yang lebih baik"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Tukar nisbah bidang apl ini dalam Tetapan"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Tukar nisbah bidang"</string>
diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml
index 5b9e9cb7353e..12c19edcaeb1 100644
--- a/libs/WindowManager/Shell/res/values-my/strings.xml
+++ b/libs/WindowManager/Shell/res/values-my/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"ပူဖောင်းဖောက်သံ"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"စီမံရန်"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ပူဖောင်းကွက် ဖယ်လိုက်သည်။"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"ပိုကောင်းသောမြင်ကွင်းအတွက် ဤအက်ပ်ပြန်စရန် တို့ပါ"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ဆက်တင်များတွင် ဤအက်ပ်၏အချိုးအစားကို ပြောင်းရန်"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"အချိုးစား ပြောင်းရန်"</string>
diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml
index 9f03d8b5b178..1161eb608405 100644
--- a/libs/WindowManager/Shell/res/values-nb/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nb/strings.xml
@@ -84,8 +84,12 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Boble"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Administrer"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Boblen er avvist."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Trykk for å starte denne appen på nytt og få en bedre visning"</string>
- <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Endre høyde/bredde-forholdet for denne appen i innstillingene"</string>
+ <string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Endre høyde/bredde-forholdet for denne appen i Innstillinger"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Endre høyde/bredde-forholdet"</string>
<string name="camera_compat_treatment_suggested_button_description" msgid="8103916969024076767">"Har du kameraproblemer?\nTrykk for å tilpasse"</string>
<string name="camera_compat_treatment_applied_button_description" msgid="2944157113330703897">"Ble ikke problemet løst?\nTrykk for å gå tilbake"</string>
diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml
index a5bd2ab5c10b..25d033738c86 100644
--- a/libs/WindowManager/Shell/res/values-ne/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ne/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"बबल"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"व्यवस्थापन गर्नुहोस्"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"बबल हटाइयो।"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"अझ राम्रो भ्यू प्राप्त गर्नका लागि यो एप रिस्टार्ट गर्न ट्याप गर्नुहोस्"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"सेटिङमा गई यो एपको एस्पेक्ट रेसियो परिवर्तन गर्नुहोस्"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"एस्पेक्ट रेसियो परिवर्तन गर्नुहोस्"</string>
diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml
index 0cd27c5c1457..4ad343cb1a4e 100644
--- a/libs/WindowManager/Shell/res/values-nl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nl/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bubbel"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Beheren"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubbel gesloten."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Tik om deze app opnieuw op te starten voor een betere weergave"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Wijzig de beeldverhouding van deze app in Instellingen"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Beeldverhouding wijzigen"</string>
diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml
index bf751852a255..966d40440ac7 100644
--- a/libs/WindowManager/Shell/res/values-or/strings.xml
+++ b/libs/WindowManager/Shell/res/values-or/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"ବବଲ୍"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"ପରିଚାଳନା କରନ୍ତୁ"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ବବଲ୍ ଖାରଜ କରାଯାଇଛି।"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"ଏକ ଆହୁରି ଭଲ ଭ୍ୟୁ ପାଇଁ ଏହି ଆପ ରିଷ୍ଟାର୍ଟ କରିବାକୁ ଟାପ କରନ୍ତୁ"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ସେଟିଂସରେ ଏହି ଆପର ଚଉଡ଼ା ଓ ଉଚ୍ଚତାର ଅନୁପାତ ପରିବର୍ତ୍ତନ କରନ୍ତୁ"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"ଚଉଡ଼ା ଓ ଉଚ୍ଚତାର ଅନୁପାତ ପରିବର୍ତ୍ତନ କରନ୍ତୁ"</string>
diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml
index 325c1e80c433..9feaf41cda7f 100644
--- a/libs/WindowManager/Shell/res/values-pa/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pa/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"ਬੁਲਬੁਲਾ"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"ਪ੍ਰਬੰਧਨ ਕਰੋ"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ਬਬਲ ਨੂੰ ਖਾਰਜ ਕੀਤਾ ਗਿਆ।"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"ਬਿਹਤਰ ਦ੍ਰਿਸ਼ ਵਾਸਤੇ ਇਸ ਐਪ ਨੂੰ ਮੁੜ-ਸ਼ੁਰੂ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ਸੈਟਿੰਗਾਂ ਵਿੱਚ ਜਾ ਕੇ ਇਸ ਐਪ ਦੇ ਆਕਾਰ ਅਨੁਪਾਤ ਨੂੰ ਬਦਲੋ"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"ਆਕਾਰ ਅਨੁਪਾਤ ਬਦਲੋ"</string>
diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml
index a7648c8e323b..1c7fbf8e80d3 100644
--- a/libs/WindowManager/Shell/res/values-pl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pl/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Dymek"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Zarządzaj"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Zamknięto dymek"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Kliknij w celu zrestartowania aplikacji, aby lepiej się wyświetlała."</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Zmień proporcje obrazu aplikacji w Ustawieniach"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Zmień proporcje obrazu"</string>
diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
index e47d151337b2..5c2de2ad0d31 100644
--- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bolha"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Gerenciar"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão dispensado."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Toque para reiniciar o app e atualizar a visualização"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Mude o tamanho da janela deste app nas Configurações"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Mudar a proporção"</string>
diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
index 1210fe8fda05..6f76525473eb 100644
--- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Balão"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Gerir"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão ignorado."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Toque para reiniciar esta app e ficar com uma melhor visão"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Altere o formato desta app nas Definições"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Altere o formato"</string>
diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml
index e47d151337b2..5c2de2ad0d31 100644
--- a/libs/WindowManager/Shell/res/values-pt/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bolha"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Gerenciar"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balão dispensado."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Toque para reiniciar o app e atualizar a visualização"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Mude o tamanho da janela deste app nas Configurações"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Mudar a proporção"</string>
diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml
index ae871f3dd42b..6e85e7849d95 100644
--- a/libs/WindowManager/Shell/res/values-ro/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ro/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Balon"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Gestionează"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balonul a fost respins."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Atinge ca să repornești aplicația pentru o vizualizare mai bună"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Schimbă raportul de dimensiuni al aplicației din Setări"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Schimbă raportul de dimensiuni"</string>
diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml
index 971e146ba77e..1b41983cd1a3 100644
--- a/libs/WindowManager/Shell/res/values-ru/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ru/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Всплывающая подсказка"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Настроить"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Всплывающий чат закрыт."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Нажмите, чтобы перезапустить приложение и оптимизировать размер"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Изменить соотношение сторон приложения в настройках"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Изменить соотношение сторон"</string>
diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml
index ef1381cbe635..6fd37e91c8b0 100644
--- a/libs/WindowManager/Shell/res/values-si/strings.xml
+++ b/libs/WindowManager/Shell/res/values-si/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"බුබුළු"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"කළමනා කරන්න"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"බුබුල ඉවත දමා ඇත."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"වඩා හොඳ දසුනක් සඳහා මෙම යෙදුම යළි ඇරඹීමට තට්ටු කරන්න"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"සැකසීම් තුළ මෙම යෙදුමේ දර්ශන අනුපාතය වෙනස් කරන්න"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"දර්ශන අනුපාතය වෙනස් කරන්න"</string>
diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml
index 55a03122483b..dabbf397d38f 100644
--- a/libs/WindowManager/Shell/res/values-sk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sk/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bublina"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Spravovať"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bublina bola zavretá."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Ak chcete zlepšiť zobrazenie, klepnutím túto aplikáciu reštartujte"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Zmeniť pomer strán tejto aplikácie v Nastaveniach"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Zmeniť pomer strán"</string>
diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml
index bb123dcdbfb6..3ade33810cc8 100644
--- a/libs/WindowManager/Shell/res/values-sl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sl/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Mehurček"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Upravljanje"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Oblaček je bil opuščen."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Če želite boljši prikaz, se dotaknite za vnovični zagon te aplikacije."</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Razmerje stranic te aplikacije spremenite v nastavitvah."</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Sprememba razmerja stranic"</string>
diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml
index c74a8cd23338..ee1aa00c5cbe 100644
--- a/libs/WindowManager/Shell/res/values-sq/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sq/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Flluskë"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Menaxho"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Flluska u hoq."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Trokit për ta rinisur këtë aplikacion për një pamje më të mirë"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Ndrysho raportin e pamjes së këtij aplikacioni te \"Cilësimet\""</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Ndrysho raportin e pamjes"</string>
diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml
index 0694a973dc1e..b2868ca84dac 100644
--- a/libs/WindowManager/Shell/res/values-sr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sr/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Облачић"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Управљајте"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Облачић је одбачен."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Додирните да бисте рестартовали ову апликацију ради бољег приказа"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Промените размеру ове апликације у Подешавањима"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Промени размеру"</string>
diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml
index 8e0bcfe91679..66118efd5da7 100644
--- a/libs/WindowManager/Shell/res/values-sv/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sv/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bubbla"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Hantera"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bubblan ignorerades."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Tryck för att starta om appen och få en bättre vy"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Ändra appens bildformat i inställningarna"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Ändra bildformat"</string>
diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml
index 41180abcf712..863b49b1f010 100644
--- a/libs/WindowManager/Shell/res/values-sw/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sw/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Kiputo"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Dhibiti"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Umeondoa kiputo."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Gusa ili uzime kisha uwashe programu hii, ili upate mwonekano bora"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Badilisha uwiano wa programu hii katika Mipangilio"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Badilisha uwiano wa kipengele"</string>
diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml
index 01ac78d984f3..74e0207bf62e 100644
--- a/libs/WindowManager/Shell/res/values-ta/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ta/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"பபிள்"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"நிர்வகி"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"குமிழ் நிராகரிக்கப்பட்டது."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"இங்கு தட்டுவதன் மூலம் இந்த ஆப்ஸை மீண்டும் தொடங்கி, ஆப்ஸ் காட்டப்படும் விதத்தை இன்னும் சிறப்பாக்கலாம்"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"அமைப்புகளில் இந்த ஆப்ஸின் தோற்ற விகிதத்தை மாற்றும்"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"தோற்ற விகிதத்தை மாற்றும்"</string>
diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml
index 6224e72c19fe..35711567e760 100644
--- a/libs/WindowManager/Shell/res/values-te/strings.xml
+++ b/libs/WindowManager/Shell/res/values-te/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"బబుల్"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"మేనేజ్ చేయండి"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"బబుల్ విస్మరించబడింది."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"మెరుగైన వీక్షణ కోసం ఈ యాప్‌ను రీస్టార్ట్ చేయడానికి ట్యాప్ చేయండి"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"సెట్టింగ్‌లలో ఈ యాప్ ఆకార నిష్పత్తిని మార్చండి"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"ఆకార నిష్పత్తిని మార్చండి"</string>
diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml
index fe0b74c469f4..47694164270e 100644
--- a/libs/WindowManager/Shell/res/values-th/strings.xml
+++ b/libs/WindowManager/Shell/res/values-th/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"บับเบิล"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"จัดการ"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"ปิดบับเบิลแล้ว"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"แตะเพื่อรีสตาร์ทแอปนี้และรับมุมมองที่ดียิ่งขึ้น"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"เปลี่ยนสัดส่วนภาพของแอปนี้ในการตั้งค่า"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"เปลี่ยนอัตราส่วนกว้างยาว"</string>
diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml
index 786e99cfe8c8..be18d88194c1 100644
--- a/libs/WindowManager/Shell/res/values-tl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-tl/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bubble"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Pamahalaan"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Na-dismiss na ang bubble."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"I-tap para i-restart ang app na ito para sa mas magandang view"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Baguhin ang aspect ratio ng app na ito sa Mga Setting"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Baguhin ang aspect ratio"</string>
diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml
index e953f5808aff..4c8c53610711 100644
--- a/libs/WindowManager/Shell/res/values-tr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-tr/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Baloncuk"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Yönet"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Balon kapatıldı."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Bu uygulamayı yeniden başlatarak daha iyi bir görünüm elde etmek için dokunun"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Bu uygulamanın en boy oranını Ayarlar\'dan değiştirin"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"En boy oranını değiştir"</string>
diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml
index fbdf42e582d1..7cc1a0406f97 100644
--- a/libs/WindowManager/Shell/res/values-uk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-uk/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Спливаюче сповіщення"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Налаштувати"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Спливаюче сповіщення закрито."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Натисніть, щоб перезапустити цей додаток для зручнішого перегляду"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Змінити формат для цього додатка в налаштуваннях"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Змінити формат"</string>
diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml
index 5562fa70bf09..8b9f29969d80 100644
--- a/libs/WindowManager/Shell/res/values-ur/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ur/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"بلبلہ"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"نظم کریں"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"بلبلہ برخاست کر دیا گیا۔"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"بہتر منظر کے لیے اس ایپ کو ری اسٹارٹ کرنے کی خاطر تھپتھپائیں"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"ترتیبات میں اس ایپ کی تناسبی شرح کو تبدیل کریں"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"تناسبی شرح کو تبدیل کریں"</string>
diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml
index 50e42329a1a0..55c6b32c909b 100644
--- a/libs/WindowManager/Shell/res/values-uz/strings.xml
+++ b/libs/WindowManager/Shell/res/values-uz/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Pufaklar"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Boshqarish"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Bulutcha yopildi."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Yaxshiroq koʻrish maqsadida bu ilovani qayta ishga tushirish uchun bosing"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Sozlamalar orqali bu ilovaning tomonlar nisbatini oʻzgartiring"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Tomonlar nisbatini oʻzgartirish"</string>
diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml
index 6da85881210d..07a6b6f6c2b4 100644
--- a/libs/WindowManager/Shell/res/values-vi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-vi/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Bong bóng"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Quản lý"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Đã đóng bong bóng."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Nhấn nút khởi động lại ứng dụng này để xem dễ hơn"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Thay đổi tỷ lệ khung hình của ứng dụng này thông qua phần Cài đặt"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Thay đổi tỷ lệ khung hình"</string>
diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
index 4318caf26199..908095a163a7 100644
--- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"气泡"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"已关闭消息气泡。"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"点按即可重启此应用,获得更好的视觉体验"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"在“设置”中更改此应用的宽高比"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"更改高宽比"</string>
diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
index 72cd39d8e00a..c8550b4e0611 100644
--- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"氣泡"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"對話氣泡已關閉。"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"輕按並重新啟動此應用程式,以取得更佳的觀看體驗"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"前往「設定」變更此應用程式的長寬比"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"變更長寬比"</string>
diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
index c06d7b105694..67048335de64 100644
--- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"泡泡"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"管理"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"已關閉泡泡。"</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"輕觸此按鈕重新啟動這個應用程式,即可獲得更良好的觀看體驗"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"前往「設定」變更這個應用程式的顯示比例"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"變更顯示比例"</string>
diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml
index 755414e52762..96b4faec06b8 100644
--- a/libs/WindowManager/Shell/res/values-zu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zu/strings.xml
@@ -84,6 +84,10 @@
<string name="notification_bubble_title" msgid="6082910224488253378">"Ibhamuza"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Phatha"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Ibhamuza licashisiwe."</string>
+ <!-- no translation found for bubble_shortcut_label (666269077944378311) -->
+ <skip />
+ <!-- no translation found for bubble_shortcut_long_label (6088437544312894043) -->
+ <skip />
<string name="restart_button_description" msgid="4564728020654658478">"Thepha ukuze uqale kabusha le app ukuze ibonakale kangcono"</string>
<string name="user_aspect_ratio_settings_button_hint" msgid="734835849600713016">"Shintsha ukubukeka kwesilinganiselo kwe-app kuMasethingi"</string>
<string name="user_aspect_ratio_settings_button_description" msgid="4315566801697411684">"Shintsha ukubukeka kwesilinganiselo"</string>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index f27f46c07de6..595d34664cfa 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -459,6 +459,12 @@
start of this area. -->
<dimen name="desktop_mode_customizable_caption_margin_end">152dp</dimen>
+ <!-- The default minimum allowed window width when resizing a window in desktop mode. -->
+ <dimen name="desktop_mode_minimum_window_width">386dp</dimen>
+
+ <!-- The default minimum allowed window height when resizing a window in desktop mode. -->
+ <dimen name="desktop_mode_minimum_window_height">352dp</dimen>
+
<!-- The width of the maximize menu in desktop mode. -->
<dimen name="desktop_mode_maximize_menu_width">228dp</dimen>
@@ -476,6 +482,12 @@
<!-- The radius of the layout outline around the maximize menu buttons. -->
<dimen name="desktop_mode_maximize_menu_buttons_outline_radius">6dp</dimen>
+ <!-- The stroke width of the outline around the maximize menu buttons. -->
+ <dimen name="desktop_mode_maximize_menu_buttons_outline_stroke">1dp</dimen>
+ <!-- The radius of the inner fill of the maximize menu buttons. -->
+ <dimen name="desktop_mode_maximize_menu_buttons_fill_radius">4dp</dimen>
+ <!-- The padding between the outline and fill of the maximize menu buttons. -->
+ <dimen name="desktop_mode_maximize_menu_buttons_fill_padding">4dp</dimen>
<!-- The corner radius of the maximize menu. -->
<dimen name="desktop_mode_maximize_menu_corner_radius">8dp</dimen>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index bf654d979856..47846746b205 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -182,6 +182,12 @@
<!-- Content description to tell the user a bubble has been dismissed. -->
<string name="accessibility_bubble_dismissed">Bubble dismissed.</string>
+ <!-- Label used to for bubbles shortcut [CHAR_LIMIT=10] -->
+ <string name="bubble_shortcut_label">Bubbles</string>
+
+ <!-- Longer label used to for bubbles shortcut, shown if there is enough space [CHAR_LIMIT=25] -->
+ <string name="bubble_shortcut_long_label">Show Bubbles</string>
+
<!-- Description of the restart button in the hint of size compatibility mode. [CHAR LIMIT=NONE] -->
<string name="restart_button_description">Tap to restart this app for a better view</string>
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java
index 8d8655addc65..4876f327a650 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java
@@ -67,6 +67,10 @@ public class DesktopModeStatus {
private static final boolean ENFORCE_DEVICE_RESTRICTIONS = SystemProperties.getBoolean(
"persist.wm.debug.desktop_mode_enforce_device_restrictions", true);
+ /** Whether the desktop density override is enabled. */
+ public static final boolean DESKTOP_DENSITY_OVERRIDE_ENABLED =
+ SystemProperties.getBoolean("persist.wm.debug.desktop_mode_density_enabled", false);
+
/** Override density for tasks when they're inside the desktop. */
public static final int DESKTOP_DENSITY_OVERRIDE =
SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 284);
@@ -157,9 +161,23 @@ public class DesktopModeStatus {
}
/**
- * Return {@code true} if the override desktop density is set.
+ * Return {@code true} if the override desktop density is enabled and valid.
+ */
+ public static boolean useDesktopOverrideDensity() {
+ return isDesktopDensityOverrideEnabled() && isValidDesktopDensityOverrideSet();
+ }
+
+ /**
+ * Return {@code true} if the override desktop density is enabled.
+ */
+ private static boolean isDesktopDensityOverrideEnabled() {
+ return DESKTOP_DENSITY_OVERRIDE_ENABLED;
+ }
+
+ /**
+ * Return {@code true} if the override desktop density is set and within a valid range.
*/
- public static boolean isDesktopDensityOverrideSet() {
+ private static boolean isValidDesktopDensityOverrideSet() {
return DESKTOP_DENSITY_OVERRIDE >= DESKTOP_DENSITY_MIN
&& DESKTOP_DENSITY_OVERRIDE <= DESKTOP_DENSITY_MAX;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
index a426b206b0cd..5a42817e839b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java
@@ -31,6 +31,7 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
import android.util.ArraySet;
@@ -45,6 +46,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
import com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationAdapter.SnapshotAdapter;
import com.android.wm.shell.common.ScreenshotUtils;
import com.android.wm.shell.shared.TransitionUtil;
@@ -398,7 +400,15 @@ class ActivityEmbeddingAnimationRunner {
// This is because the TaskFragment surface/change won't contain the Activity's before its
// reparent.
Animation changeAnimation = null;
- Rect parentBounds = new Rect();
+ final Rect parentBounds = new Rect();
+ // We use a single boolean value to record the backdrop override because the override used
+ // for overlay and we restrict to single overlay animation. We should fix the assumption
+ // if we allow multiple overlay transitions.
+ // The backdrop logic is mainly for animations of split animations. The backdrop should be
+ // disabled if there is any open/close target in the same transition as the change target.
+ // However, the overlay change animation usually contains one change target, and shows
+ // backdrop unexpectedly.
+ Boolean overrideShowBackdrop = null;
for (TransitionInfo.Change change : info.getChanges()) {
if (change.getMode() != TRANSIT_CHANGE
|| change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
@@ -421,17 +431,17 @@ class ActivityEmbeddingAnimationRunner {
}
}
- // The TaskFragment may be enter/exit split, so we take the union of both as the parent
- // size.
- parentBounds.union(boundsAnimationChange.getStartAbsBounds());
- parentBounds.union(boundsAnimationChange.getEndAbsBounds());
- if (boundsAnimationChange != change) {
- // Union the change starting bounds in case the activity is resized and reparented
- // to a TaskFragment. In that case, the TaskFragment may not cover the activity's
- // starting bounds.
- parentBounds.union(change.getStartAbsBounds());
+ final TransitionInfo.AnimationOptions options = boundsAnimationChange
+ .getAnimationOptions();
+ if (options != null) {
+ final Animation overrideAnimation = mAnimationSpec.loadCustomAnimationFromOptions(
+ options, TRANSIT_CHANGE);
+ if (overrideAnimation != null) {
+ overrideShowBackdrop = overrideAnimation.getShowBackdrop();
+ }
}
+ calculateParentBounds(change, boundsAnimationChange, parentBounds);
// There are two animations in the array. The first one is for the start leash
// (snapshot), and the second one is for the end leash (TaskFragment).
final Animation[] animations = mAnimationSpec.createChangeBoundsChangeAnimations(change,
@@ -466,7 +476,7 @@ class ActivityEmbeddingAnimationRunner {
// If there is no corresponding open/close window with the change, we should show background
// color to cover the empty part of the screen.
- boolean shouldShouldBackgroundColor = true;
+ boolean shouldShowBackgroundColor = true;
// Handle the other windows that don't have bounds change in the same transition.
for (TransitionInfo.Change change : info.getChanges()) {
if (handledChanges.contains(change)) {
@@ -483,16 +493,18 @@ class ActivityEmbeddingAnimationRunner {
animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change);
} else if (TransitionUtil.isClosingType(change.getMode())) {
animation = mAnimationSpec.createChangeBoundsCloseAnimation(change, parentBounds);
- shouldShouldBackgroundColor = false;
+ shouldShowBackgroundColor = false;
} else {
animation = mAnimationSpec.createChangeBoundsOpenAnimation(change, parentBounds);
- shouldShouldBackgroundColor = false;
+ shouldShowBackgroundColor = false;
}
adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change,
TransitionUtil.getRootFor(change, info)));
}
- if (shouldShouldBackgroundColor && changeAnimation != null) {
+ shouldShowBackgroundColor = overrideShowBackdrop != null
+ ? overrideShowBackdrop : shouldShowBackgroundColor;
+ if (shouldShowBackgroundColor && changeAnimation != null) {
// Change animation may leave part of the screen empty. Show background color to cover
// that.
changeAnimation.setShowBackdrop(true);
@@ -502,6 +514,39 @@ class ActivityEmbeddingAnimationRunner {
}
/**
+ * Calculates parent bounds of the animation target by {@code change}.
+ */
+ @VisibleForTesting
+ static void calculateParentBounds(@NonNull TransitionInfo.Change change,
+ @NonNull TransitionInfo.Change boundsAnimationChange, @NonNull Rect outParentBounds) {
+ if (Flags.activityEmbeddingOverlayPresentationFlag()) {
+ final Point endParentSize = change.getEndParentSize();
+ if (endParentSize.equals(0, 0)) {
+ return;
+ }
+ final Point endRelPosition = change.getEndRelOffset();
+ final Point endAbsPosition = new Point(change.getEndAbsBounds().left,
+ change.getEndAbsBounds().top);
+ final Point parentEndAbsPosition = new Point(endAbsPosition.x - endRelPosition.x,
+ endAbsPosition.y - endRelPosition.y);
+ outParentBounds.set(parentEndAbsPosition.x, parentEndAbsPosition.y,
+ parentEndAbsPosition.x + endParentSize.x,
+ parentEndAbsPosition.y + endParentSize.y);
+ } else {
+ // The TaskFragment may be enter/exit split, so we take the union of both as
+ // the parent size.
+ outParentBounds.union(boundsAnimationChange.getStartAbsBounds());
+ outParentBounds.union(boundsAnimationChange.getEndAbsBounds());
+ if (boundsAnimationChange != change) {
+ // Union the change starting bounds in case the activity is resized and
+ // reparented to a TaskFragment. In that case, the TaskFragment may not cover
+ // the activity's starting bounds.
+ outParentBounds.union(change.getStartAbsBounds());
+ }
+ }
+ }
+
+ /**
* Takes a screenshot of the given {@code screenshotChange} surface if WM Core hasn't taken one.
* The screenshot leash should be attached to the {@code animationChange} surface which we will
* animate later.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
index 0272f1cda6ef..8d49614b021b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java
@@ -18,6 +18,8 @@ package com.android.wm.shell.activityembedding;
import static android.app.ActivityOptions.ANIM_CUSTOM;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.window.TransitionInfo.AnimationOptions.DEFAULT_ANIMATION_RESOURCES_ID;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_NONE;
import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;
@@ -27,6 +29,8 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.Rect;
+import android.util.Log;
+import android.view.WindowManager;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
@@ -38,6 +42,7 @@ import android.view.animation.TranslateAnimation;
import android.window.TransitionInfo;
import com.android.internal.policy.TransitionAnimation;
+import com.android.window.flags.Flags;
import com.android.wm.shell.shared.TransitionUtil;
/** Animation spec for ActivityEmbedding transition. */
@@ -202,7 +207,7 @@ class ActivityEmbeddingAnimationSpec {
Animation loadOpenAnimation(@NonNull TransitionInfo info,
@NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
final boolean isEnter = TransitionUtil.isOpeningType(change.getMode());
- final Animation customAnimation = loadCustomAnimation(info, isEnter);
+ final Animation customAnimation = loadCustomAnimation(info, change);
final Animation animation;
if (customAnimation != null) {
animation = customAnimation;
@@ -229,7 +234,7 @@ class ActivityEmbeddingAnimationSpec {
Animation loadCloseAnimation(@NonNull TransitionInfo info,
@NonNull TransitionInfo.Change change, @NonNull Rect wholeAnimationBounds) {
final boolean isEnter = TransitionUtil.isOpeningType(change.getMode());
- final Animation customAnimation = loadCustomAnimation(info, isEnter);
+ final Animation customAnimation = loadCustomAnimation(info, change);
final Animation animation;
if (customAnimation != null) {
animation = customAnimation;
@@ -261,13 +266,41 @@ class ActivityEmbeddingAnimationSpec {
}
@Nullable
- private Animation loadCustomAnimation(@NonNull TransitionInfo info, boolean isEnter) {
- final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
+ private Animation loadCustomAnimation(@NonNull TransitionInfo info,
+ @NonNull TransitionInfo.Change change) {
+ final TransitionInfo.AnimationOptions options;
+ if (Flags.moveAnimationOptionsToChange()) {
+ options = change.getAnimationOptions();
+ } else {
+ options = info.getAnimationOptions();
+ }
+ return loadCustomAnimationFromOptions(options, change.getMode());
+ }
+
+ @Nullable
+ Animation loadCustomAnimationFromOptions(@Nullable TransitionInfo.AnimationOptions options,
+ @WindowManager.TransitionType int mode) {
if (options == null || options.getType() != ANIM_CUSTOM) {
return null;
}
+ final int resId;
+ if (TransitionUtil.isOpeningType(mode)) {
+ resId = options.getEnterResId();
+ } else if (TransitionUtil.isClosingType(mode)) {
+ resId = options.getExitResId();
+ } else if (mode == TRANSIT_CHANGE) {
+ resId = options.getChangeResId();
+ } else {
+ Log.w(TAG, "Unknown transit type:" + mode);
+ resId = DEFAULT_ANIMATION_RESOURCES_ID;
+ }
+ // Use the default animation if the resources ID is not specified.
+ if (resId == DEFAULT_ANIMATION_RESOURCES_ID) {
+ return null;
+ }
+
final Animation anim = mTransitionAnimation.loadAnimationRes(options.getPackageName(),
- isEnter ? options.getEnterResId() : options.getExitResId());
+ resId);
if (anim != null) {
return anim;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index d6b9d34c5ab3..b4ef9f0fc2ac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -32,6 +32,7 @@ import android.os.IBinder;
import android.util.ArrayMap;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
+import android.window.TransitionInfo.AnimationOptions;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
@@ -39,6 +40,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
@@ -117,24 +119,39 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle
return false;
}
- final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
- if (options != null) {
- // Scene-transition should be handled by app side.
- if (options.getType() == ANIM_SCENE_TRANSITION) {
+ return shouldAnimateAnimationOptions(info);
+ }
+
+ private boolean shouldAnimateAnimationOptions(@NonNull TransitionInfo info) {
+ if (!Flags.moveAnimationOptionsToChange()) {
+ return shouldAnimateAnimationOptions(info.getAnimationOptions());
+ }
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (!shouldAnimateAnimationOptions(change.getAnimationOptions())) {
+ // If any of override animation is not supported, don't animate the transition.
return false;
}
- // The case of ActivityOptions#makeCustomAnimation, Activity#overridePendingTransition,
- // and Activity#overrideActivityTransition are supported.
- if (options.getType() == ANIM_CUSTOM) {
- return true;
- }
- // Use default transition handler to animate other override animation.
- return !isSupportedOverrideAnimation(options);
}
-
return true;
}
+ private boolean shouldAnimateAnimationOptions(@Nullable AnimationOptions options) {
+ if (options == null) {
+ return true;
+ }
+ // Scene-transition should be handled by app side.
+ if (options.getType() == ANIM_SCENE_TRANSITION) {
+ return false;
+ }
+ // The case of ActivityOptions#makeCustomAnimation, Activity#overridePendingTransition,
+ // and Activity#overrideActivityTransition are supported.
+ if (options.getType() == ANIM_CUSTOM) {
+ return true;
+ }
+ // Use default transition handler to animate other override animation.
+ return !isSupportedOverrideAnimation(options);
+ }
+
@Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 5600664a8f47..7041ea307b0f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -18,7 +18,6 @@ package com.android.wm.shell.back;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME;
import static com.android.window.flags.Flags.predictiveBackSystemAnims;
-import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;
@@ -31,6 +30,7 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Configuration;
import android.database.ContentObserver;
+import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.net.Uri;
import android.os.Bundle;
@@ -41,7 +41,6 @@ import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings.Global;
-import android.util.DisplayMetrics;
import android.util.Log;
import android.view.IRemoteAnimationRunner;
import android.view.InputDevice;
@@ -49,6 +48,7 @@ import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;
+import android.view.WindowManager;
import android.window.BackAnimationAdapter;
import android.window.BackEvent;
import android.window.BackMotionEvent;
@@ -63,7 +63,6 @@ import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.LatencyTracker;
import com.android.internal.view.AppearanceRegion;
import com.android.wm.shell.R;
-import com.android.wm.shell.animation.FlingAnimationUtils;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
@@ -88,15 +87,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
public static final boolean IS_ENABLED =
SystemProperties.getInt("persist.wm.debug.predictive_back",
SETTING_VALUE_ON) == SETTING_VALUE_ON;
- public static final float FLING_MAX_LENGTH_SECONDS = 0.1f; // 100ms
- public static final float FLING_SPEED_UP_FACTOR = 0.6f;
-
- /**
- * The maximum additional progress in case of fling gesture.
- * The end animation starts after the user lifts the finger from the screen, we continue to
- * fire {@link BackEvent}s until the velocity reaches 0.
- */
- private static final float MAX_FLING_PROGRESS = 0.3f; /* 30% of the screen */
/** Predictive back animation developer option */
private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
@@ -119,8 +109,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
private boolean mPointersPilfered = false;
private final boolean mRequirePointerPilfer;
- private final FlingAnimationUtils mFlingAnimationUtils;
-
/** Registry for the back animations */
private final ShellBackAnimationRegistry mShellBackAnimationRegistry;
@@ -133,6 +121,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
private final ShellCommandHandler mShellCommandHandler;
private final ShellExecutor mShellExecutor;
private final Handler mBgHandler;
+ private final WindowManager mWindowManager;
+ @VisibleForTesting
+ final Rect mTouchableArea = new Rect();
/**
* Tracks the current user back gesture.
@@ -233,14 +224,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mBgHandler = bgHandler;
shellInit.addInitCallback(this::onInit, this);
mAnimationBackground = backAnimationBackground;
- DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
- mFlingAnimationUtils = new FlingAnimationUtils.Builder(displayMetrics)
- .setMaxLengthSeconds(FLING_MAX_LENGTH_SECONDS)
- .setSpeedUpFactor(FLING_SPEED_UP_FACTOR)
- .build();
mShellBackAnimationRegistry = shellBackAnimationRegistry;
mLatencyTracker = LatencyTracker.getInstance(mContext);
mShellCommandHandler = shellCommandHandler;
+ mWindowManager = context.getSystemService(WindowManager.class);
+ updateTouchableArea();
}
private void onInit() {
@@ -302,6 +290,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
@Override
public void onConfigurationChanged(Configuration newConfig) {
mShellBackAnimationRegistry.onConfigurationChanged(newConfig);
+ updateTouchableArea();
+ }
+
+ private void updateTouchableArea() {
+ mTouchableArea.set(mWindowManager.getCurrentWindowMetrics().getBounds());
}
@Override
@@ -435,11 +428,19 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
if (!shouldDispatchToAnimator && mActiveCallback != null) {
mCurrentTracker.updateStartLocation();
tryDispatchOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null));
+ if (mBackNavigationInfo != null && !isAppProgressGenerationAllowed()) {
+ tryPilferPointers();
+ }
} else if (shouldDispatchToAnimator) {
tryPilferPointers();
}
}
+ private boolean isAppProgressGenerationAllowed() {
+ return mBackNavigationInfo.isAppProgressGenerationAllowed()
+ && mBackNavigationInfo.getTouchableRegion().equals(mTouchableArea);
+ }
+
/**
* Called when a new motion event needs to be transferred to this
* {@link BackAnimationController}
@@ -555,6 +556,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
// App is handling back animation. Cancel system animation latency tracking.
cancelLatencyTracking();
tryDispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null));
+ if (!isAppProgressGenerationAllowed()) {
+ tryPilferPointers();
+ }
}
}
@@ -661,7 +665,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
private void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
BackMotionEvent backEvent) {
- if (callback == null || !shouldDispatchToAnimator()) {
+ if (callback == null || (!shouldDispatchToAnimator() && mBackNavigationInfo != null
+ && isAppProgressGenerationAllowed())) {
return;
}
try {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
index 9114c7adb6d8..c9d3dbdcae05 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
@@ -324,6 +324,7 @@ abstract class CrossActivityBackAnimation(
enteringHasSameLetterbox = false
lastPostCommitFlingScale = SPRING_SCALE
gestureProgress = 0f
+ triggerBack = false
}
protected fun applyTransform(
@@ -354,7 +355,7 @@ abstract class CrossActivityBackAnimation(
matrix.postScale(scale, scale, scalePivotX, 0f)
matrix.postTranslate(tempRectF.left, tempRectF.top)
transaction
- .setAlpha(leash, keepMinimumAlpha(alpha))
+ .setAlpha(leash, alpha)
.setMatrix(leash, matrix, tmpFloat9)
.setCrop(leash, cropRect)
.setCornerRadius(leash, cornerRadius)
@@ -499,10 +500,12 @@ abstract class CrossActivityBackAnimation(
}
override fun onBackCancelled() {
+ triggerBack = false
progressAnimator.onBackCancelled { finishAnimation() }
}
override fun onBackInvoked() {
+ triggerBack = true
progressAnimator.reset()
onGestureCommitted(progressAnimator.velocity)
}
@@ -562,9 +565,6 @@ abstract class CrossActivityBackAnimation(
}
}
-// The target will loose focus when alpha == 0, so keep a minimum value for it.
-private fun keepMinimumAlpha(transAlpha: Float) = max(transAlpha, 0.005f)
-
private fun isDarkMode(context: Context): Boolean {
return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index da530d740d48..2aefc64a3ebb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -305,6 +305,7 @@ public class Bubble implements BubbleViewProvider {
getUser().getIdentifier(),
getPackageName(),
getTitle(),
+ getAppName(),
isImportantConversation());
}
@@ -893,11 +894,22 @@ public class Bubble implements BubbleViewProvider {
}
@Nullable
- Intent getAppBubbleIntent() {
+ @VisibleForTesting
+ public Intent getAppBubbleIntent() {
return mAppIntent;
}
/**
+ * Sets the intent for a bubble that is an app bubble (one for which {@link #mIsAppBubble} is
+ * true).
+ *
+ * @param appIntent The intent to set for the app bubble.
+ */
+ void setAppBubbleIntent(Intent appIntent) {
+ mAppIntent = appIntent;
+ }
+
+ /**
* Returns whether this bubble is from an app versus a notification.
*/
public boolean isAppBubble() {
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 317e00a44bce..c853301519e9 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
@@ -86,6 +86,7 @@ import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.util.CollectionUtils;
import com.android.launcher3.icons.BubbleIconFactory;
import com.android.wm.shell.Flags;
import com.android.wm.shell.R;
@@ -93,6 +94,7 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
import com.android.wm.shell.bubbles.properties.BubbleProperties;
+import com.android.wm.shell.bubbles.shortcut.BubbleShortcutHelper;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.FloatingContentCoordinator;
@@ -511,6 +513,10 @@ public class BubbleController implements ConfigurationChangeListener,
}
mCurrentProfiles = userProfiles;
+ if (Flags.enableRetrievableBubbles()) {
+ registerShortcutBroadcastReceiver();
+ }
+
mShellController.addConfigurationChangeListener(this);
mShellController.addExternalInterface(KEY_EXTRA_SHELL_BUBBLES,
this::createExternalInterface, this);
@@ -518,7 +524,7 @@ public class BubbleController implements ConfigurationChangeListener,
}
private ExternalInterfaceBinder createExternalInterface() {
- return new BubbleController.IBubblesImpl(this);
+ return new IBubblesImpl(this);
}
@VisibleForTesting
@@ -592,11 +598,12 @@ public class BubbleController implements ConfigurationChangeListener,
* Hides the current input method, wherever it may be focused, via InputMethodManagerInternal.
*/
void hideCurrentInputMethod() {
+ mBubblePositioner.setImeVisible(false /* visible */, 0 /* height */);
int displayId = mWindowManager.getDefaultDisplay().getDisplayId();
try {
mBarService.hideCurrentInputMethodForBubbles(displayId);
} catch (RemoteException e) {
- e.printStackTrace();
+ Log.e(TAG, "Failed to hide IME", e);
}
}
@@ -986,6 +993,25 @@ public class BubbleController implements ConfigurationChangeListener,
}
};
+ private void registerShortcutBroadcastReceiver() {
+ IntentFilter shortcutFilter = new IntentFilter();
+ shortcutFilter.addAction(BubbleShortcutHelper.ACTION_SHOW_BUBBLES);
+ ProtoLog.d(WM_SHELL_BUBBLES, "register broadcast receive for bubbles shortcut");
+ mContext.registerReceiver(mShortcutBroadcastReceiver, shortcutFilter,
+ Context.RECEIVER_NOT_EXPORTED);
+ }
+
+ private final BroadcastReceiver mShortcutBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ ProtoLog.v(WM_SHELL_BUBBLES, "receive broadcast to show bubbles %s",
+ intent.getAction());
+ if (BubbleShortcutHelper.ACTION_SHOW_BUBBLES.equals(intent.getAction())) {
+ mMainExecutor.execute(() -> showBubblesFromShortcut());
+ }
+ }
+ };
+
/**
* Called by the BubbleStackView and whenever all bubbles have animated out, and none have been
* added in the meantime.
@@ -1424,6 +1450,8 @@ public class BubbleController implements ConfigurationChangeListener,
if (b != null) {
// It's in the overflow, so remove it & reinflate
mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL);
+ // Update the bubble entry in the overflow with the latest intent.
+ b.setAppBubbleIntent(intent);
} else {
// App bubble does not exist, lets add and expand it
b = Bubble.createAppBubble(intent, user, icon, mMainExecutor);
@@ -1457,8 +1485,9 @@ public class BubbleController implements ConfigurationChangeListener,
SynchronousScreenCaptureListener screenCaptureListener) {
try {
ScreenCapture.CaptureArgs args = null;
- if (mStackView != null) {
- ViewRootImpl viewRoot = mStackView.getViewRootImpl();
+ View viewToUse = mStackView != null ? mStackView : mLayerView;
+ if (viewToUse != null) {
+ ViewRootImpl viewRoot = viewToUse.getViewRootImpl();
if (viewRoot != null) {
SurfaceControl bubbleLayer = viewRoot.getSurfaceControl();
if (bubbleLayer != null) {
@@ -1550,6 +1579,12 @@ public class BubbleController implements ConfigurationChangeListener,
Log.w(TAG, "Tried to add a bubble to the stack but the stack is null");
}
};
+ } else if (mBubbleData.isExpanded() && mBubbleData.getSelectedBubble() != null) {
+ callback = b -> {
+ if (b.getKey().equals(mBubbleData.getSelectedBubbleKey())) {
+ mLayerView.showExpandedView(b);
+ }
+ };
}
for (int i = mBubbleData.getBubbles().size() - 1; i >= 0; i--) {
Bubble bubble = mBubbleData.getBubbles().get(i);
@@ -2221,6 +2256,34 @@ public class BubbleController implements ConfigurationChangeListener,
}
/**
+ * Show bubbles UI when triggered via shortcut.
+ *
+ * <p>When there are bubbles visible, expands the top-most bubble. When there are no bubbles
+ * visible, opens the bubbles overflow UI.
+ */
+ public void showBubblesFromShortcut() {
+ if (isStackExpanded()) {
+ ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: stack visible, skip");
+ return;
+ }
+ if (mBubbleData.getSelectedBubble() != null) {
+ ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: open selected bubble");
+ expandStackWithSelectedBubble();
+ return;
+ }
+ BubbleViewProvider bubbleToSelect = CollectionUtils.firstOrNull(mBubbleData.getBubbles());
+ if (bubbleToSelect == null) {
+ ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: no bubbles");
+ // make sure overflow bubbles are loaded
+ loadOverflowBubblesFromDisk();
+ bubbleToSelect = mBubbleData.getOverflow();
+ }
+ ProtoLog.v(WM_SHELL_BUBBLES, "showBubblesFromShortcut: select and open %s",
+ bubbleToSelect.getKey());
+ mBubbleData.setSelectedBubbleAndExpandStack(bubbleToSelect);
+ }
+
+ /**
* Description of current bubble state.
*/
private void dump(PrintWriter pw, String prefix) {
@@ -2354,6 +2417,8 @@ public class BubbleController implements ConfigurationChangeListener,
@Override
public void invalidate() {
mController = null;
+ // Unregister the listeners to ensure any binder death recipients are unlinked
+ mListener.unregister();
}
@Override
@@ -2531,17 +2596,6 @@ public class BubbleController implements ConfigurationChangeListener,
private CachedState mCachedState = new CachedState();
- private IBubblesImpl mIBubbles;
-
- @Override
- public IBubbles createExternalInterface() {
- if (mIBubbles != null) {
- mIBubbles.invalidate();
- }
- mIBubbles = new IBubblesImpl(BubbleController.this);
- return mIBubbles;
- }
-
@Override
public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) {
return mCachedState.isBubbleNotificationSuppressedFromShade(key, groupKey);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index 874102c20925..761e02598460 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -256,11 +256,15 @@ public class BubbleData {
}
/**
- * Returns a bubble bar update populated with the current list of active bubbles.
+ * Returns a bubble bar update populated with the current list of active bubbles, expanded,
+ * and selected state.
*/
public BubbleBarUpdate getInitialStateForBubbleBar() {
BubbleBarUpdate initialState = mStateChange.getInitialState();
initialState.bubbleBarLocation = mPositioner.getBubbleBarLocation();
+ initialState.expanded = mExpanded;
+ initialState.expandedChanged = mExpanded; // only matters if we're expanded
+ initialState.selectedBubbleKey = getSelectedBubbleKey();
return initialState;
}
@@ -598,7 +602,7 @@ public class BubbleData {
List<Bubble> removedBubbles = filterAllBubbles(bubble ->
userId == bubble.getUser().getIdentifier());
for (Bubble b : removedBubbles) {
- doRemove(b.getKey(), Bubbles.DISMISS_USER_REMOVED);
+ doRemove(b.getKey(), Bubbles.DISMISS_USER_ACCOUNT_REMOVED);
}
if (!removedBubbles.isEmpty()) {
dispatchPendingChanges();
@@ -674,7 +678,7 @@ public class BubbleData {
|| reason == Bubbles.DISMISS_SHORTCUT_REMOVED
|| reason == Bubbles.DISMISS_PACKAGE_REMOVED
|| reason == Bubbles.DISMISS_USER_CHANGED
- || reason == Bubbles.DISMISS_USER_REMOVED;
+ || reason == Bubbles.DISMISS_USER_ACCOUNT_REMOVED;
int indexToRemove = indexForKey(key);
if (indexToRemove == -1) {
@@ -912,6 +916,9 @@ public class BubbleData {
((Bubble) bubble).markAsAccessedAt(mTimeSource.currentTimeMillis());
}
mSelectedBubble = bubble;
+ if (isOverflow) {
+ mShowingOverflow = true;
+ }
mStateChange.selectedBubble = bubble;
mStateChange.selectionChanged = true;
}
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 4e8afccee40f..c7ccd50af550 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
@@ -482,31 +482,38 @@ public class BubbleExpandedView extends LinearLayout {
mPointerWidth, mPointerHeight, true /* pointLeft */));
mRightPointer = new ShapeDrawable(TriangleShape.createHorizontal(
mPointerWidth, mPointerHeight, false /* pointLeft */));
- if (mPointerView != null) {
- updatePointerView();
- }
+ updatePointerViewIfExists();
+ updateManageButtonIfExists();
+ }
- if (mManageButton != null) {
- int visibility = mManageButton.getVisibility();
- removeView(mManageButton);
- ContextThemeWrapper ctw = new ContextThemeWrapper(getContext(),
- com.android.internal.R.style.Theme_DeviceDefault_DayNight);
- mManageButton = (AlphaOptimizedButton) LayoutInflater.from(ctw).inflate(
- R.layout.bubble_manage_button, this /* parent */, false /* attach */);
- addView(mManageButton);
- mManageButton.setVisibility(visibility);
- post(() -> {
- int touchAreaHeight =
- getResources().getDimensionPixelSize(
- R.dimen.bubble_manage_button_touch_area_height);
- Rect r = new Rect();
- mManageButton.getHitRect(r);
- int extraTouchArea = (touchAreaHeight - r.height()) / 2;
- r.top -= extraTouchArea;
- r.bottom += extraTouchArea;
- setTouchDelegate(new TouchDelegate(r, mManageButton));
- });
+
+ /**
+ * Reinflate manage button if {@link #mManageButton} is initialized.
+ * Does nothing otherwise.
+ */
+ private void updateManageButtonIfExists() {
+ if (mManageButton == null) {
+ return;
}
+ int visibility = mManageButton.getVisibility();
+ removeView(mManageButton);
+ ContextThemeWrapper ctw = new ContextThemeWrapper(getContext(),
+ com.android.internal.R.style.Theme_DeviceDefault_DayNight);
+ mManageButton = (AlphaOptimizedButton) LayoutInflater.from(ctw).inflate(
+ R.layout.bubble_manage_button, this /* parent */, false /* attach */);
+ addView(mManageButton);
+ mManageButton.setVisibility(visibility);
+ post(() -> {
+ int touchAreaHeight =
+ getResources().getDimensionPixelSize(
+ R.dimen.bubble_manage_button_touch_area_height);
+ Rect r = new Rect();
+ mManageButton.getHitRect(r);
+ int extraTouchArea = (touchAreaHeight - r.height()) / 2;
+ r.top -= extraTouchArea;
+ r.bottom += extraTouchArea;
+ setTouchDelegate(new TouchDelegate(r, mManageButton));
+ });
}
void updateFontSize() {
@@ -548,11 +555,18 @@ public class BubbleExpandedView extends LinearLayout {
if (mTaskView != null) {
mTaskView.setCornerRadius(mCornerRadius);
}
- updatePointerView();
+ updatePointerViewIfExists();
+ updateManageButtonIfExists();
}
- /** Updates the size and visuals of the pointer. **/
- private void updatePointerView() {
+ /**
+ * Updates the size and visuals of the pointer if {@link #mPointerView} is initialized.
+ * Does nothing otherwise.
+ */
+ private void updatePointerViewIfExists() {
+ if (mPointerView == null) {
+ return;
+ }
LayoutParams lp = (LayoutParams) mPointerView.getLayoutParams();
if (mCurrentPointer == mLeftPointer || mCurrentPointer == mRightPointer) {
lp.width = mPointerHeight;
@@ -1055,7 +1069,7 @@ public class BubbleExpandedView extends LinearLayout {
// Post because we need the width of the view
post(() -> {
mCurrentPointer = showVertically ? onLeft ? mLeftPointer : mRightPointer : mTopPointer;
- updatePointerView();
+ updatePointerViewIfExists();
if (showVertically) {
mPointerPos.y = bubbleCenter - (mPointerWidth / 2f);
if (!isRtl) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt
index b0d3cc4a5d5c..3d9bf032c1b0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt
@@ -29,6 +29,7 @@ interface BubbleExpandedViewManager {
fun setAppBubbleTaskId(key: String, taskId: Int)
fun isStackExpanded(): Boolean
fun isShowingAsBubbleBar(): Boolean
+ fun hideCurrentInputMethod()
companion object {
/**
@@ -73,6 +74,10 @@ interface BubbleExpandedViewManager {
override fun isStackExpanded(): Boolean = controller.isStackExpanded
override fun isShowingAsBubbleBar(): Boolean = controller.isShowingAsBubbleBar
+
+ override fun hideCurrentInputMethod() {
+ controller.hideCurrentInputMethod()
+ }
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index fac9bf6e2a4b..09bec8c37b9a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -134,6 +134,8 @@ public class BubbleStackView extends FrameLayout
private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f;
+ private static final float OPEN_OVERFLOW_ANIMATE_SCALE_AMOUNT = 0.5f;
+
private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150;
/** Minimum alpha value for scrim when alpha is being changed via drag */
@@ -1549,10 +1551,20 @@ public class BubbleStackView extends FrameLayout
}
private void updateOverflowVisibility() {
- mBubbleOverflow.setVisible(mShowingOverflow
- && (mIsExpanded || mBubbleData.isShowingOverflow())
- ? VISIBLE
- : GONE);
+ int visibility = GONE;
+ if (mShowingOverflow) {
+ if (mIsExpanded || mBubbleData.isShowingOverflow()) {
+ visibility = VISIBLE;
+ }
+ }
+ if (Flags.enableRetrievableBubbles()) {
+ if (BubbleOverflow.KEY.equals(mBubbleData.getSelectedBubbleKey())
+ && !mBubbleData.hasBubbles()) {
+ // Hide overflow bubble icon if it is the only bubble
+ visibility = GONE;
+ }
+ }
+ mBubbleOverflow.setVisible(visibility);
}
private void updateOverflowDotVisibility(boolean expanding) {
@@ -2147,6 +2159,13 @@ public class BubbleStackView extends FrameLayout
if (mIsExpanded) {
hideCurrentInputMethod();
+ if (Flags.enableRetrievableBubbles()) {
+ if (mBubbleData.getBubbles().size() == 1) {
+ // First bubble, check if overflow visibility needs to change
+ updateOverflowVisibility();
+ }
+ }
+
// Make the container of the expanded view transparent before removing the expanded view
// from it. Otherwise a punch hole created by {@link android.view.SurfaceView} in the
// expanded view becomes visible on the screen. See b/126856255
@@ -2215,6 +2234,16 @@ public class BubbleStackView extends FrameLayout
}
/**
+ * Check if we only have overflow expanded. Which is the case when we are launching bubbles from
+ * background.
+ */
+ private boolean isOnlyOverflowExpanded() {
+ boolean overflowExpanded = mExpandedBubble != null && BubbleOverflow.KEY.equals(
+ mExpandedBubble.getKey());
+ return overflowExpanded && !mBubbleData.hasBubbles();
+ }
+
+ /**
* Monitor for swipe up gesture that is used to collapse expanded view
*/
void startMonitoringSwipeUpGesture() {
@@ -2324,7 +2353,6 @@ public class BubbleStackView extends FrameLayout
* not.
*/
void hideCurrentInputMethod() {
- mPositioner.setImeVisible(false, 0);
mManager.hideCurrentInputMethod();
}
@@ -2434,7 +2462,7 @@ public class BubbleStackView extends FrameLayout
ProtoLog.d(WM_SHELL_BUBBLES, "animateExpansion, expandedBubble=%s",
mExpandedBubble != null ? mExpandedBubble.getKey() : "null");
cancelDelayedExpandCollapseSwitchAnimations();
- final boolean showVertically = mPositioner.showBubblesVertically();
+
mIsExpanded = true;
if (isStackEduVisible()) {
mStackEduView.hide(true /* fromExpansion */);
@@ -2444,8 +2472,17 @@ public class BubbleStackView extends FrameLayout
showScrim(true, null /* runnable */);
updateBubbleShadows(mIsExpanded);
mBubbleContainer.setActiveController(mExpandedAnimationController);
- updateBadges(false /* setBadgeForCollapsedStack */);
updateOverflowVisibility();
+
+ if (Flags.enableRetrievableBubbles() && isOnlyOverflowExpanded()) {
+ animateOverflowExpansion();
+ } else {
+ animateBubbleExpansion();
+ }
+ }
+
+ private void animateBubbleExpansion() {
+ updateBadges(false /* setBadgeForCollapsedStack */);
updatePointerPosition(false /* forIme */);
if (Flags.enableBubbleStashing()) {
mBubbleContainer.animate().translationX(0).start();
@@ -2469,6 +2506,7 @@ public class BubbleStackView extends FrameLayout
mExpandedViewContainer.setTranslationY(translationY);
mExpandedViewContainer.setAlpha(1f);
+ final boolean showVertically = mPositioner.showBubblesVertically();
// How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles
// that are animating farther, so that the expanded view doesn't move as much.
final float relevantStackPosition = showVertically
@@ -2561,6 +2599,47 @@ public class BubbleStackView extends FrameLayout
mMainExecutor.executeDelayed(mDelayedAnimation, startDelay);
}
+ /**
+ * Animate expansion of overflow view when it is shown from the bubble shortcut.
+ * <p>
+ * Animates the view with a scale originating from the center of the view.
+ */
+ private void animateOverflowExpansion() {
+ PointF bubbleXY = mPositioner.getExpandedBubbleXY(0, getState());
+ final float translationY = mPositioner.getExpandedViewY(mExpandedBubble,
+ mPositioner.showBubblesVertically() ? bubbleXY.y : bubbleXY.x);
+ mExpandedViewContainer.setTranslationX(0f);
+ mExpandedViewContainer.setTranslationY(translationY);
+ mExpandedViewContainer.setAlpha(1f);
+
+ boolean stackOnLeft = mPositioner.isStackOnLeft(getStackPosition());
+ float width = mPositioner.getTaskViewContentWidth(stackOnLeft);
+ float height = mPositioner.getExpandedViewHeight(mExpandedBubble);
+ float scale = 1f - OPEN_OVERFLOW_ANIMATE_SCALE_AMOUNT;
+ // Scale from the center of the view
+ mExpandedViewContainerMatrix.setScale(scale, scale, width / 2f, height / 2f);
+ mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
+ mExpandedViewAlphaAnimator.start();
+ PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
+ PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
+ .spring(AnimatableScaleMatrix.SCALE_X,
+ AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
+ mScaleInSpringConfig)
+ .spring(AnimatableScaleMatrix.SCALE_Y,
+ AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
+ mScaleInSpringConfig)
+ .addUpdateListener((target, values) -> {
+ mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
+ }).withEndActions(() -> {
+ mExpandedViewContainer.setAnimationMatrix(null);
+ afterExpandedViewAnimation();
+ BubbleExpandedView expandedView = getExpandedView();
+ if (expandedView != null) {
+ expandedView.setSurfaceZOrderedOnTop(false);
+ }
+ }).start();
+ }
+
private void animateCollapse() {
cancelDelayedExpandCollapseSwitchAnimations();
ProtoLog.d(WM_SHELL_BUBBLES, "animateCollapse");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 21b70b8e32da..0b66bcb6930e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -161,6 +161,11 @@ public class BubbleTaskViewHelper {
// The taskId is saved to use for removeTask, preventing appearance in recent tasks.
mTaskId = taskId;
+ if (mBubble != null && mBubble.isAppBubble()) {
+ // Let the controller know sooner what the taskId is.
+ mExpandedViewManager.setAppBubbleTaskId(mBubble.getKey(), mTaskId);
+ }
+
// With the task org, the taskAppeared callback will only happen once the task has
// already drawn
mListener.onTaskCreated();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 1d053f9aab35..82af88d03b19 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -61,7 +61,7 @@ public interface Bubbles {
DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE,
DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT,
DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED,
- DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK, DISMISS_USER_REMOVED,
+ DISMISS_NO_BUBBLE_UP, DISMISS_RELOAD_FROM_DISK, DISMISS_USER_ACCOUNT_REMOVED,
DISMISS_SWITCH_TO_STACK})
@Target({FIELD, LOCAL_VARIABLE, PARAMETER})
@interface DismissReason {
@@ -82,7 +82,7 @@ public interface Bubbles {
int DISMISS_PACKAGE_REMOVED = 13;
int DISMISS_NO_BUBBLE_UP = 14;
int DISMISS_RELOAD_FROM_DISK = 15;
- int DISMISS_USER_REMOVED = 16;
+ int DISMISS_USER_ACCOUNT_REMOVED = 16;
int DISMISS_SWITCH_TO_STACK = 17;
/** Returns a binder that can be passed to an external process to manipulate Bubbles. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 271fb9abce6a..972dce51e02b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -82,6 +82,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
private static final int INVALID_TASK_ID = -1;
private BubbleExpandedViewManager mManager;
+ private BubblePositioner mPositioner;
private boolean mIsOverflow;
private BubbleTaskViewHelper mBubbleTaskViewHelper;
private BubbleBarMenuViewController mMenuViewController;
@@ -160,6 +161,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
boolean isOverflow,
@Nullable BubbleTaskView bubbleTaskView) {
mManager = expandedViewManager;
+ mPositioner = positioner;
mIsOverflow = isOverflow;
if (mIsOverflow) {
@@ -207,7 +209,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
@Override
public void onDismissBubble(Bubble bubble) {
- mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_REMOVED);
+ mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_GESTURE);
}
});
mHandleView.setOnClickListener(view -> {
@@ -290,15 +292,27 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
}
/**
- * Hides the current modal menu view or collapses the bubble stack.
- * Called from {@link BubbleBarLayerView}
+ * Hides the current modal menu if it is visible
+ * @return {@code true} if menu was visible and is hidden
*/
- public void hideMenuOrCollapse() {
+ public boolean hideMenuIfVisible() {
if (mMenuViewController.isMenuVisible()) {
- mMenuViewController.hideMenu(/* animated = */ true);
- } else {
- mManager.collapseStack();
+ mMenuViewController.hideMenu(true /* animated */);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Hides the IME if it is visible
+ * @return {@code true} if IME was visible
+ */
+ public boolean hideImeIfVisible() {
+ if (mPositioner.isImeVisible()) {
+ mManager.hideCurrentInputMethod();
+ return true;
}
+ return false;
}
/** Updates the bubble shown in the expanded view. */
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 123cc7e9d488..badc40997902 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
@@ -132,7 +132,7 @@ public class BubbleBarLayerView extends FrameLayout
}
});
- setOnClickListener(view -> hideMenuOrCollapse());
+ setOnClickListener(view -> hideModalOrCollapse());
}
@Override
@@ -217,7 +217,7 @@ public class BubbleBarLayerView extends FrameLayout
@Override
public void onBackPressed() {
- hideMenuOrCollapse();
+ hideModalOrCollapse();
}
});
@@ -344,15 +344,23 @@ public class BubbleBarLayerView extends FrameLayout
addView(mDismissView);
}
- /** Hides the current modal education/menu view, expanded view or collapses the bubble stack */
- private void hideMenuOrCollapse() {
+ /** Hides the current modal education/menu view, IME or collapses the expanded view */
+ private void hideModalOrCollapse() {
if (mEducationViewController.isEducationVisible()) {
mEducationViewController.hideEducation(/* animated = */ true);
- } else if (isExpanded() && mExpandedView != null) {
- mExpandedView.hideMenuOrCollapse();
- } else {
- mBubbleController.collapseStack();
+ return;
+ }
+ if (isExpanded() && mExpandedView != null) {
+ boolean menuHidden = mExpandedView.hideMenuIfVisible();
+ if (menuHidden) {
+ return;
+ }
+ boolean imeHidden = mExpandedView.hideImeIfVisible();
+ if (imeHidden) {
+ return;
+ }
}
+ mBubbleController.collapseStack();
}
/** Updates the expanded view size and position. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/BubbleShortcutHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/BubbleShortcutHelper.kt
new file mode 100644
index 000000000000..efa12383f188
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/BubbleShortcutHelper.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.wm.shell.bubbles.shortcut
+
+import android.content.Context
+import android.content.pm.ShortcutInfo
+import android.graphics.drawable.Icon
+import com.android.wm.shell.R
+
+/** Helper class for creating a shortcut to open bubbles */
+object BubbleShortcutHelper {
+ const val SHORTCUT_ID = "bubbles_shortcut_id"
+ const val ACTION_SHOW_BUBBLES = "com.android.wm.shell.bubbles.action.SHOW_BUBBLES"
+
+ /** Create a shortcut that launches [ShowBubblesActivity] */
+ fun createShortcut(context: Context, icon: Icon): ShortcutInfo {
+ return ShortcutInfo.Builder(context, SHORTCUT_ID)
+ .setIntent(ShowBubblesActivity.createIntent(context))
+ .setActivity(ShowBubblesActivity.createComponent(context))
+ .setShortLabel(context.getString(R.string.bubble_shortcut_label))
+ .setLongLabel(context.getString(R.string.bubble_shortcut_long_label))
+ .setLongLived(true)
+ .setIcon(icon)
+ .build()
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt
new file mode 100644
index 000000000000..a124f95d7431
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/CreateBubbleShortcutActivity.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.wm.shell.bubbles.shortcut
+
+import android.app.Activity
+import android.content.pm.ShortcutManager
+import android.graphics.drawable.Icon
+import android.os.Bundle
+import com.android.wm.shell.Flags
+import com.android.wm.shell.R
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES
+import com.android.wm.shell.util.KtProtoLog
+
+/** Activity to create a shortcut to open bubbles */
+class CreateBubbleShortcutActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (Flags.enableRetrievableBubbles()) {
+ KtProtoLog.d(WM_SHELL_BUBBLES, "Creating a shortcut for bubbles")
+ createShortcut()
+ }
+ finish()
+ }
+
+ private fun createShortcut() {
+ val icon = Icon.createWithResource(this, R.drawable.ic_bubbles_shortcut_widget)
+ // TODO(b/340337839): shortcut shows the sysui icon
+ val shortcutInfo = BubbleShortcutHelper.createShortcut(this, icon)
+ val shortcutManager = getSystemService(ShortcutManager::class.java)
+ val shortcutIntent = shortcutManager?.createShortcutResultIntent(shortcutInfo)
+ if (shortcutIntent != null) {
+ setResult(RESULT_OK, shortcutIntent)
+ } else {
+ setResult(RESULT_CANCELED)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt
new file mode 100644
index 000000000000..ae7940ca1b65
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/shortcut/ShowBubblesActivity.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.wm.shell.bubbles.shortcut
+
+import android.app.Activity
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import com.android.wm.shell.Flags
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES
+import com.android.wm.shell.util.KtProtoLog
+
+/** Activity that sends a broadcast to open bubbles */
+class ShowBubblesActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (Flags.enableRetrievableBubbles()) {
+ val intent =
+ Intent().apply {
+ action = BubbleShortcutHelper.ACTION_SHOW_BUBBLES
+ // Set the package as the receiver is not exported
+ `package` = packageName
+ }
+ KtProtoLog.v(WM_SHELL_BUBBLES, "Sending broadcast to show bubbles")
+ sendBroadcast(intent)
+ }
+ finish()
+ }
+
+ companion object {
+ /** Create intent to launch this activity */
+ fun createIntent(context: Context): Intent {
+ return Intent(context, ShowBubblesActivity::class.java).apply {
+ action = BubbleShortcutHelper.ACTION_SHOW_BUBBLES
+ }
+ }
+
+ /** Create component for this activity */
+ fun createComponent(context: Context): ComponentName {
+ return ComponentName(context, ShowBubblesActivity::class.java)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index ad01d0fa311a..f4ac5f260fcd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -220,6 +220,8 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
final int mDisplayId;
final InsetsState mInsetsState = new InsetsState();
@InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
+ boolean mImeRequestedVisible =
+ (WindowInsets.Type.defaultVisible() & WindowInsets.Type.ime()) != 0;
InsetsSourceControl mImeSourceControl = null;
int mAnimationDirection = DIRECTION_NONE;
ValueAnimator mAnimation = null;
@@ -247,8 +249,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
return;
}
- updateImeVisibility(insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME,
- WindowInsets.Type.ime()));
+ if (!android.view.inputmethod.Flags.refactorInsetsController()) {
+ updateImeVisibility(insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME,
+ WindowInsets.Type.ime()));
+ }
final InsetsSource newSource = insetsState.peekSource(InsetsSource.ID_IME);
final Rect newFrame = newSource != null ? newSource.getFrame() : null;
@@ -287,32 +291,63 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
dispatchImeControlTargetChanged(mDisplayId, hasImeSourceControl);
}
- if (hasImeSourceControl) {
+ boolean pendingImeStartAnimation = false;
+ boolean canAnimate;
+ if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ canAnimate = hasImeSourceControl && imeSourceControl.getLeash() != null;
+ } else {
+ canAnimate = hasImeSourceControl;
+ }
+
+ boolean positionChanged = false;
+ if (canAnimate) {
if (mAnimation != null) {
final Point lastSurfacePosition = hadImeSourceControl
? mImeSourceControl.getSurfacePosition() : null;
- final boolean positionChanged =
- !imeSourceControl.getSurfacePosition().equals(lastSurfacePosition);
- if (positionChanged) {
- startAnimation(mImeShowing, true /* forceRestart */,
- SoftInputShowHideReason.DISPLAY_CONTROLS_CHANGED);
- }
+ positionChanged = !imeSourceControl.getSurfacePosition().equals(
+ lastSurfacePosition);
} else {
if (!haveSameLeash(mImeSourceControl, imeSourceControl)) {
applyVisibilityToLeash(imeSourceControl);
+
+ if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ pendingImeStartAnimation = true;
+ }
}
if (!mImeShowing) {
removeImeSurface();
}
}
- } else if (mAnimation != null) {
+ } else if (!android.view.inputmethod.Flags.refactorInsetsController()
+ && mAnimation != null) {
+ // we don"t want to cancel the hide animation, when the control is lost, but
+ // continue the bar to slide to the end (even without visible IME)
mAnimation.cancel();
}
+ if (positionChanged) {
+ if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ // For showing the IME, the leash has to be available first. Hiding
+ // the IME happens directly via {@link #hideInsets} (triggered by
+ // setImeInputTargetRequestedVisibility) while the leash is not gone
+ // yet.
+ pendingImeStartAnimation = true;
+ } else {
+ startAnimation(mImeShowing, true /* forceRestart */,
+ SoftInputShowHideReason.DISPLAY_CONTROLS_CHANGED);
+ }
+ }
if (hadImeSourceControl && mImeSourceControl != imeSourceControl) {
mImeSourceControl.release(SurfaceControl::release);
}
mImeSourceControl = imeSourceControl;
+
+ if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (pendingImeStartAnimation) {
+ startAnimation(true, true /* forceRestart */,
+ null /* statsToken */);
+ }
+ }
}
private void applyVisibilityToLeash(InsetsSourceControl imeSourceControl) {
@@ -354,6 +389,20 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
// Do nothing
}
+ @Override
+ // TODO(b/335404678): pass control target
+ public void setImeInputTargetRequestedVisibility(boolean visible) {
+ if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ mImeRequestedVisible = visible;
+ // In the case that the IME becomes visible, but we have the control with leash
+ // already (e.g., when focussing an editText in activity B, while and editText in
+ // activity A is focussed), we will not get a call of #insetsControlChanged, and
+ // therefore have to start the show animation from here
+ startAnimation(mImeRequestedVisible /* show */, false /* forceRestart */,
+ null /* TODO statsToken */);
+ }
+ }
+
/**
* Sends the local visibility state back to window manager. Needed for legacy adjustForIme.
*/
@@ -402,6 +451,12 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
private void startAnimation(final boolean show, final boolean forceRestart,
@NonNull final ImeTracker.Token statsToken) {
+ if (android.view.inputmethod.Flags.refactorInsetsController()) {
+ if (mImeSourceControl == null || mImeSourceControl.getLeash() == null) {
+ if (DEBUG) Slog.d(TAG, "No leash available, not starting the animation.");
+ return;
+ }
+ }
final InsetsSource imeSource = mInsetsState.peekSource(InsetsSource.ID_IME);
if (imeSource == null || mImeSourceControl == null) {
ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_WM_ANIMATION_CREATE);
@@ -463,10 +518,13 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
mAnimation.addUpdateListener(animation -> {
SurfaceControl.Transaction t = mTransactionPool.acquire();
float value = (float) animation.getAnimatedValue();
- t.setPosition(mImeSourceControl.getLeash(), x, value);
- final float alpha = (mAnimateAlpha || isFloating)
- ? (value - hiddenY) / (shownY - hiddenY) : 1.f;
- t.setAlpha(mImeSourceControl.getLeash(), alpha);
+ if (!android.view.inputmethod.Flags.refactorInsetsController() || (
+ mImeSourceControl != null && mImeSourceControl.getLeash() != null)) {
+ t.setPosition(mImeSourceControl.getLeash(), x, value);
+ final float alpha = (mAnimateAlpha || isFloating)
+ ? (value - hiddenY) / (shownY - hiddenY) : 1.f;
+ t.setAlpha(mImeSourceControl.getLeash(), alpha);
+ }
dispatchPositionChanged(mDisplayId, imeTop(value), t);
t.apply();
mTransactionPool.release(t);
@@ -480,8 +538,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
@Override
public void onAnimationStart(Animator animation) {
+ ValueAnimator valueAnimator = (ValueAnimator) animation;
+ float value = (float) valueAnimator.getAnimatedValue();
SurfaceControl.Transaction t = mTransactionPool.acquire();
- t.setPosition(mImeSourceControl.getLeash(), x, startY);
+ t.setPosition(mImeSourceControl.getLeash(), x, value);
if (DEBUG) {
Slog.d(TAG, "onAnimationStart d:" + mDisplayId + " top:"
+ imeTop(hiddenY) + "->" + imeTop(shownY)
@@ -491,7 +551,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
imeTop(shownY), mAnimationDirection == DIRECTION_SHOW, isFloating, t);
mAnimateAlpha = (flags & ImePositionProcessor.IME_ANIMATION_NO_ALPHA) == 0;
final float alpha = (mAnimateAlpha || isFloating)
- ? (startY - hiddenY) / (shownY - hiddenY)
+ ? (value - hiddenY) / (shownY - hiddenY)
: 1.f;
t.setAlpha(mImeSourceControl.getLeash(), alpha);
if (mAnimationDirection == DIRECTION_SHOW) {
@@ -502,7 +562,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
if (DEBUG_IME_VISIBILITY) {
EventLog.writeEvent(IMF_IME_REMOTE_ANIM_START,
mStatsToken != null ? mStatsToken.getTag() : ImeTracker.TOKEN_NONE,
- mDisplayId, mAnimationDirection, alpha, startY , endY,
+ mDisplayId, mAnimationDirection, alpha, value, endY,
Objects.toString(mImeSourceControl.getLeash()),
Objects.toString(mImeSourceControl.getInsetsHint()),
Objects.toString(mImeSourceControl.getSurfacePosition()),
@@ -525,17 +585,25 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
@Override
public void onAnimationEnd(Animator animation) {
+ boolean hasLeash =
+ mImeSourceControl != null && mImeSourceControl.getLeash() != null;
if (DEBUG) Slog.d(TAG, "onAnimationEnd " + mCancelled);
SurfaceControl.Transaction t = mTransactionPool.acquire();
if (!mCancelled) {
- t.setPosition(mImeSourceControl.getLeash(), x, endY);
- t.setAlpha(mImeSourceControl.getLeash(), 1.f);
+ if (!android.view.inputmethod.Flags.refactorInsetsController()
+ || hasLeash) {
+ t.setPosition(mImeSourceControl.getLeash(), x, endY);
+ t.setAlpha(mImeSourceControl.getLeash(), 1.f);
+ }
}
dispatchEndPositioning(mDisplayId, mCancelled, t);
if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) {
ImeTracker.forLogging().onProgress(mStatsToken,
ImeTracker.PHASE_WM_ANIMATION_RUNNING);
- t.hide(mImeSourceControl.getLeash());
+ if (!android.view.inputmethod.Flags.refactorInsetsController()
+ || hasLeash) {
+ t.hide(mImeSourceControl.getLeash());
+ }
removeImeSurface();
ImeTracker.forLogging().onHidden(mStatsToken);
} else if (mAnimationDirection == DIRECTION_SHOW && !mCancelled) {
@@ -548,9 +616,13 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
EventLog.writeEvent(IMF_IME_REMOTE_ANIM_END,
mStatsToken != null ? mStatsToken.getTag() : ImeTracker.TOKEN_NONE,
mDisplayId, mAnimationDirection, endY,
- Objects.toString(mImeSourceControl.getLeash()),
- Objects.toString(mImeSourceControl.getInsetsHint()),
- Objects.toString(mImeSourceControl.getSurfacePosition()),
+ Objects.toString(
+ mImeSourceControl != null ? mImeSourceControl.getLeash()
+ : "null"),
+ Objects.toString(mImeSourceControl != null
+ ? mImeSourceControl.getInsetsHint() : "null"),
+ Objects.toString(mImeSourceControl != null
+ ? mImeSourceControl.getSurfacePosition() : "null"),
Objects.toString(mImeFrame));
}
t.apply();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
index 55dc793cc3b6..1fb0e1745e3e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
@@ -199,6 +199,16 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan
}
}
+ private void setImeInputTargetRequestedVisibility(boolean visible) {
+ CopyOnWriteArrayList<OnInsetsChangedListener> listeners = mListeners.get(mDisplayId);
+ if (listeners == null) {
+ return;
+ }
+ for (OnInsetsChangedListener listener : listeners) {
+ listener.setImeInputTargetRequestedVisibility(visible);
+ }
+ }
+
@BinderThread
private class DisplayWindowInsetsControllerImpl
extends IDisplayWindowInsetsController.Stub {
@@ -240,6 +250,14 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan
PerDisplay.this.hideInsets(types, fromIme, statsToken);
});
}
+
+ @Override
+ public void setImeInputTargetRequestedVisibility(boolean visible)
+ throws RemoteException {
+ mMainExecutor.execute(() -> {
+ PerDisplay.this.setImeInputTargetRequestedVisibility(visible);
+ });
+ }
}
}
@@ -291,5 +309,12 @@ public class DisplayInsetsController implements DisplayController.OnDisplaysChan
*/
default void hideInsets(@InsetsType int types, boolean fromIme,
@Nullable ImeTracker.Token statsToken) {}
+
+ /**
+ * Called to set the requested visibility of the IME in DisplayImeController. Invoked by
+ * {@link com.android.server.wm.DisplayContent.RemoteInsetsControlTarget}.
+ * @param visible requested status of the IME
+ */
+ default void setImeInputTargetRequestedVisibility(boolean visible) {}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExecutorUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExecutorUtils.java
deleted file mode 100644
index b29058b1f204..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExecutorUtils.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.common;
-
-import android.Manifest;
-import android.util.Slog;
-
-import java.util.function.Consumer;
-
-/**
- * Helpers for working with executors
- */
-public class ExecutorUtils {
-
- /**
- * Checks that the caller has the MANAGE_ACTIVITY_TASKS permission and executes the given
- * callback.
- */
- public static <T> void executeRemoteCallWithTaskPermission(RemoteCallable<T> controllerInstance,
- String log, Consumer<T> callback) {
- executeRemoteCallWithTaskPermission(controllerInstance, log, callback,
- false /* blocking */);
- }
-
- /**
- * Checks that the caller has the MANAGE_ACTIVITY_TASKS permission and executes the given
- * callback.
- */
- public static <T> void executeRemoteCallWithTaskPermission(RemoteCallable<T> controllerInstance,
- String log, Consumer<T> callback, boolean blocking) {
- if (controllerInstance == null) return;
-
- final RemoteCallable<T> controller = controllerInstance;
- controllerInstance.getContext().enforceCallingPermission(
- Manifest.permission.MANAGE_ACTIVITY_TASKS, log);
- if (blocking) {
- try {
- controllerInstance.getRemoteCallExecutor().executeBlocking(() -> {
- callback.accept((T) controller);
- });
- } catch (InterruptedException e) {
- Slog.e("ExecutorUtils", "Remote call failed", e);
- }
- } else {
- controllerInstance.getRemoteCallExecutor().execute(() -> {
- callback.accept((T) controller);
- });
- }
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java
index aa5b0cb628e1..d6f4d81b44f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java
@@ -16,7 +16,11 @@
package com.android.wm.shell.common;
+import android.Manifest;
import android.os.IBinder;
+import android.util.Slog;
+
+import java.util.function.Consumer;
/**
* An interface for binders which can be registered to be sent to other processes.
@@ -31,4 +35,40 @@ public interface ExternalInterfaceBinder {
* Returns the IBinder to send.
*/
IBinder asBinder();
+
+ /**
+ * Checks that the caller has the MANAGE_ACTIVITY_TASKS permission and executes the given
+ * callback.
+ */
+ default <T> void executeRemoteCallWithTaskPermission(RemoteCallable<T> controllerInstance,
+ String log, Consumer<T> callback) {
+ executeRemoteCallWithTaskPermission(controllerInstance, log, callback,
+ false /* blocking */);
+ }
+
+ /**
+ * Checks that the caller has the MANAGE_ACTIVITY_TASKS permission and executes the given
+ * callback.
+ */
+ default <T> void executeRemoteCallWithTaskPermission(RemoteCallable<T> controllerInstance,
+ String log, Consumer<T> callback, boolean blocking) {
+ if (controllerInstance == null) return;
+
+ final RemoteCallable<T> controller = controllerInstance;
+ controllerInstance.getContext().enforceCallingPermission(
+ Manifest.permission.MANAGE_ACTIVITY_TASKS, log);
+ if (blocking) {
+ try {
+ controllerInstance.getRemoteCallExecutor().executeBlocking(() -> {
+ callback.accept((T) controller);
+ });
+ } catch (InterruptedException e) {
+ Slog.e("ExternalInterfaceBinder", "Remote call failed", e);
+ }
+ } else {
+ controllerInstance.getRemoteCallExecutor().execute(() -> {
+ callback.accept((T) controller);
+ });
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/RemoteCallable.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/RemoteCallable.java
index 30f535ba940c..0d90fb7e60fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/RemoteCallable.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/RemoteCallable.java
@@ -19,7 +19,7 @@ package com.android.wm.shell.common;
import android.content.Context;
/**
- * An interface for controllers that can receive remote calls.
+ * An interface for controllers (of type T) that can receive remote calls.
*/
public interface RemoteCallable<T> {
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java
index 24608d651d06..829af08e612a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java
@@ -45,10 +45,12 @@ public class BubbleInfo implements Parcelable {
private Icon mIcon;
@Nullable
private String mTitle;
+ @Nullable
+ private String mAppName;
private boolean mIsImportantConversation;
public BubbleInfo(String key, int flags, @Nullable String shortcutId, @Nullable Icon icon,
- int userId, String packageName, @Nullable String title,
+ int userId, String packageName, @Nullable String title, @Nullable String appName,
boolean isImportantConversation) {
mKey = key;
mFlags = flags;
@@ -57,6 +59,7 @@ public class BubbleInfo implements Parcelable {
mUserId = userId;
mPackageName = packageName;
mTitle = title;
+ mAppName = appName;
mIsImportantConversation = isImportantConversation;
}
@@ -68,6 +71,7 @@ public class BubbleInfo implements Parcelable {
mUserId = source.readInt();
mPackageName = source.readString();
mTitle = source.readString();
+ mAppName = source.readString();
mIsImportantConversation = source.readBoolean();
}
@@ -102,6 +106,11 @@ public class BubbleInfo implements Parcelable {
return mTitle;
}
+ @Nullable
+ public String getAppName() {
+ return mAppName;
+ }
+
public boolean isImportantConversation() {
return mIsImportantConversation;
}
@@ -161,6 +170,7 @@ public class BubbleInfo implements Parcelable {
parcel.writeInt(mUserId);
parcel.writeString(mPackageName);
parcel.writeString(mTitle);
+ parcel.writeString(mAppName);
parcel.writeBoolean(mIsImportantConversation);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.aidl
new file mode 100644
index 000000000000..c968e809bf61
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.aidl
@@ -0,0 +1,19 @@
+/*
+ * 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.wm.shell.common.desktopmode;
+
+parcelable DesktopModeTransitionSource; \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt
new file mode 100644
index 000000000000..dbbf178613b5
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/desktopmode/DesktopModeTransitionSource.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.wm.shell.common.desktopmode
+
+import android.os.Parcel
+import android.os.Parcelable
+
+/** Transition source types for Desktop Mode. */
+enum class DesktopModeTransitionSource : Parcelable {
+ /** Transitions that originated as a consequence of task dragging. */
+ TASK_DRAG,
+ /** Transitions that originated from an app from Overview. */
+ APP_FROM_OVERVIEW,
+ /** Transitions that originated from app handle menu button */
+ APP_HANDLE_MENU_BUTTON,
+ /** Transitions that originated as a result of keyboard shortcuts. */
+ KEYBOARD_SHORTCUT,
+ /** Transitions with source unknown. */
+ UNKNOWN;
+
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ dest.writeString(name)
+ }
+
+ companion object {
+ @JvmField
+ val CREATOR =
+ object : Parcelable.Creator<DesktopModeTransitionSource> {
+ override fun createFromParcel(parcel: Parcel): DesktopModeTransitionSource {
+ return parcel.readString()?.let { valueOf(it) } ?: UNKNOWN
+ }
+
+ override fun newArray(size: Int) = arrayOfNulls<DesktopModeTransitionSource>(size)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl
index b5f25433f9aa..e77987963b48 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/IPip.aidl
@@ -53,9 +53,11 @@ interface IPip {
* @param destinationBounds the destination bounds the PiP window lands into
* @param overlay an optional overlay to fade out after entering PiP
* @param appBounds the bounds used to set the buffer size of the optional content overlay
+ * @param sourceRectHint the bounds to show in the transition to PiP
*/
oneway void stopSwipePipToHome(int taskId, in ComponentName componentName,
- in Rect destinationBounds, in SurfaceControl overlay, in Rect appBounds) = 2;
+ in Rect destinationBounds, in SurfaceControl overlay, in Rect appBounds,
+ in Rect sourceRectHint) = 2;
/**
* Notifies the swiping Activity to PiP onto home transition is aborted
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java
index 6ffeb97f50fa..58007b50350b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsAlgorithm.java
@@ -27,7 +27,9 @@ import android.util.DisplayMetrics;
import android.util.Size;
import android.view.Gravity;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.io.PrintWriter;
@@ -39,6 +41,9 @@ public class PipBoundsAlgorithm {
private static final String TAG = PipBoundsAlgorithm.class.getSimpleName();
private static final float INVALID_SNAP_FRACTION = -1f;
+ // The same value (with the same name) is used in Launcher.
+ private static final float PIP_ASPECT_RATIO_MISMATCH_THRESHOLD = 0.01f;
+
@NonNull private final PipBoundsState mPipBoundsState;
@NonNull protected final PipDisplayLayoutState mPipDisplayLayoutState;
@NonNull protected final SizeSpecSource mSizeSpecSource;
@@ -206,9 +211,27 @@ public class PipBoundsAlgorithm {
*/
public static boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint,
Rect destinationBounds) {
- return sourceRectHint != null
- && sourceRectHint.width() > destinationBounds.width()
- && sourceRectHint.height() > destinationBounds.height();
+ if (sourceRectHint == null || sourceRectHint.isEmpty()) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "isSourceRectHintValidForEnterPip=false, empty hint");
+ return false;
+ }
+ if (sourceRectHint.width() <= destinationBounds.width()
+ || sourceRectHint.height() <= destinationBounds.height()) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "isSourceRectHintValidForEnterPip=false, hint(%s) is smaller"
+ + " than destination(%s)", sourceRectHint, destinationBounds);
+ return false;
+ }
+ final float reportedRatio = destinationBounds.width() / (float) destinationBounds.height();
+ final float inferredRatio = sourceRectHint.width() / (float) sourceRectHint.height();
+ if (Math.abs(reportedRatio - inferredRatio) > PIP_ASPECT_RATIO_MISMATCH_THRESHOLD) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "isSourceRectHintValidForEnterPip=false, hint(%s) does not match"
+ + " destination(%s) aspect ratio", sourceRectHint, destinationBounds);
+ return false;
+ }
+ return true;
}
public float getDefaultAspectRatio() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
index 579a7943829e..3e9366fd6459 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt
@@ -22,6 +22,7 @@ import android.app.WindowConfiguration
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
+import android.graphics.Rect
import android.os.RemoteException
import android.os.SystemProperties
import android.util.DisplayMetrics
@@ -33,6 +34,7 @@ import com.android.internal.protolog.common.ProtoLog
import com.android.wm.shell.Flags
import com.android.wm.shell.protolog.ShellProtoLogGroup
import kotlin.math.abs
+import kotlin.math.roundToInt
/** A class that includes convenience methods. */
object PipUtils {
@@ -138,6 +140,30 @@ object PipUtils {
}
}
+
+ /**
+ * Returns a fake source rect hint for animation purposes when app-provided one is invalid.
+ * Resulting adjusted source rect hint lets the app icon in the content overlay to stay visible.
+ */
+ @JvmStatic
+ fun getEnterPipWithOverlaySrcRectHint(appBounds: Rect, aspectRatio: Float): Rect {
+ val appBoundsAspRatio = appBounds.width().toFloat() / appBounds.height()
+ val width: Int
+ val height: Int
+ var left = appBounds.left
+ var top = appBounds.top
+ if (appBoundsAspRatio < aspectRatio) {
+ width = appBounds.width()
+ height = (width / aspectRatio).roundToInt()
+ top = appBounds.top + (appBounds.height() - height) / 2
+ } else {
+ height = appBounds.height()
+ width = (height * aspectRatio).roundToInt()
+ left = appBounds.left + (appBounds.width() - width) / 2
+ }
+ return Rect(left, top, left + width, top + height)
+ }
+
private var isPip2ExperimentEnabled: Boolean? = null
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
index 8fb9bda539a0..5d121c23c6e1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitWindowManager.java
@@ -143,6 +143,8 @@ public final class SplitWindowManager extends WindowlessWindowManager {
/**
* Releases the surface control of the current {@link DividerView} and tear down the view
* hierarchy.
+ * @param t If supplied, the surface removal will be bundled with this Transaction. If
+ * called with null, removes the surface immediately.
*/
void release(@Nullable SurfaceControl.Transaction t) {
if (mDividerView != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
index 835f1af85c51..07082a558744 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/ReachabilityEduWindowManager.java
@@ -53,7 +53,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
private final ShellExecutor mMainExecutor;
- private boolean mIsActivityLetterboxed;
+ private boolean mIsLetterboxDoubleTapEnabled;
private int mLetterboxVerticalPosition;
@@ -91,7 +91,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
Function<Integer, Integer> disappearTimeSupplier) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo;
- mIsActivityLetterboxed = appCompatTaskInfo.isLetterboxDoubleTapEnabled;
+ mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled;
mLetterboxVerticalPosition = appCompatTaskInfo.topActivityLetterboxVerticalPosition;
mLetterboxHorizontalPosition = appCompatTaskInfo.topActivityLetterboxHorizontalPosition;
mTopActivityLetterboxWidth = appCompatTaskInfo.topActivityLetterboxWidth;
@@ -119,7 +119,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
@Override
protected boolean eligibleToShowLayout() {
- return mIsActivityLetterboxed
+ return mIsLetterboxDoubleTapEnabled
&& (mLetterboxVerticalPosition != -1 || mLetterboxHorizontalPosition != -1);
}
@@ -142,13 +142,13 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
@Override
public boolean updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener,
boolean canShow) {
- final boolean prevIsActivityLetterboxed = mIsActivityLetterboxed;
+ final boolean prevIsLetterboxDoubleTapEnabled = mIsLetterboxDoubleTapEnabled;
final int prevLetterboxVerticalPosition = mLetterboxVerticalPosition;
final int prevLetterboxHorizontalPosition = mLetterboxHorizontalPosition;
final int prevTopActivityLetterboxWidth = mTopActivityLetterboxWidth;
final int prevTopActivityLetterboxHeight = mTopActivityLetterboxHeight;
final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo;
- mIsActivityLetterboxed = appCompatTaskInfo.isLetterboxDoubleTapEnabled;
+ mIsLetterboxDoubleTapEnabled = appCompatTaskInfo.isLetterboxDoubleTapEnabled;
mLetterboxVerticalPosition = appCompatTaskInfo.topActivityLetterboxVerticalPosition;
mLetterboxHorizontalPosition = appCompatTaskInfo.topActivityLetterboxHorizontalPosition;
mTopActivityLetterboxWidth = appCompatTaskInfo.topActivityLetterboxWidth;
@@ -162,7 +162,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
mHasLetterboxSizeChanged = prevTopActivityLetterboxWidth != mTopActivityLetterboxWidth
|| prevTopActivityLetterboxHeight != mTopActivityLetterboxHeight;
- if (mHasUserDoubleTapped || prevIsActivityLetterboxed != mIsActivityLetterboxed
+ if (mHasUserDoubleTapped || prevIsLetterboxDoubleTapEnabled != mIsLetterboxDoubleTapEnabled
|| prevLetterboxVerticalPosition != mLetterboxVerticalPosition
|| prevLetterboxHorizontalPosition != mLetterboxHorizontalPosition
|| prevTopActivityLetterboxWidth != mTopActivityLetterboxWidth
@@ -249,7 +249,7 @@ class ReachabilityEduWindowManager extends CompatUIWindowManagerAbstract {
&& (mLetterboxVerticalPosition == REACHABILITY_LEFT_OR_UP_POSITION
|| mLetterboxVerticalPosition == REACHABILITY_RIGHT_OR_BOTTOM_POSITION));
- if (mIsActivityLetterboxed && (eligibleForDisplayHorizontalEducation
+ if (mIsLetterboxDoubleTapEnabled && (eligibleForDisplayHorizontalEducation
|| eligibleForDisplayVerticalEducation)) {
int availableWidth = getTaskBounds().width() - mTopActivityLetterboxWidth;
int availableHeight = getTaskBounds().height() - mTopActivityLetterboxHeight;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 991fbafed296..609e5af5c5b0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -87,6 +87,7 @@ import com.android.wm.shell.performance.PerfHintController;
import com.android.wm.shell.recents.RecentTasks;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.recents.TaskStackTransitionObserver;
import com.android.wm.shell.shared.DesktopModeStatus;
import com.android.wm.shell.shared.ShellTransitions;
import com.android.wm.shell.shared.annotations.ShellAnimationThread;
@@ -619,12 +620,13 @@ public abstract class WMShellBaseModule {
TaskStackListenerImpl taskStackListener,
ActivityTaskManager activityTaskManager,
Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ TaskStackTransitionObserver taskStackTransitionObserver,
@ShellMainThread ShellExecutor mainExecutor
) {
return Optional.ofNullable(
RecentTasksController.create(context, shellInit, shellController,
shellCommandHandler, taskStackListener, activityTaskManager,
- desktopModeTaskRepository, mainExecutor));
+ desktopModeTaskRepository, taskStackTransitionObserver, mainExecutor));
}
@BindsOptionalOf
@@ -924,6 +926,19 @@ public abstract class WMShellBaseModule {
}
//
+ // Task Stack
+ //
+
+ @WMSingleton
+ @Provides
+ static TaskStackTransitionObserver provideTaskStackTransitionObserver(
+ Lazy<Transitions> transitions,
+ ShellInit shellInit
+ ) {
+ return new TaskStackTransitionObserver(transitions, shellInit);
+ }
+
+ //
// Misc
//
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 12bbd51b968d..1fcfa7fcf350 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
@@ -23,6 +23,7 @@ import android.os.Handler;
import android.os.UserManager;
import android.view.Choreographer;
import android.view.IWindowManager;
+import android.view.SurfaceControl;
import android.view.WindowManager;
import com.android.internal.jank.InteractionJankMonitor;
@@ -121,9 +122,9 @@ import java.util.Optional;
*/
@Module(
includes = {
- WMShellBaseModule.class,
- PipModule.class,
- ShellBackAnimationModule.class,
+ WMShellBaseModule.class,
+ PipModule.class,
+ ShellBackAnimationModule.class,
})
public abstract class WMShellModule {
@@ -400,7 +401,8 @@ public abstract class WMShellModule {
Optional<RecentTasksController> recentTasksController,
HomeTransitionObserver homeTransitionObserver) {
return new RecentsTransitionHandler(shellInit, transitions,
- recentTasksController.orElse(null), homeTransitionObserver);
+ recentTasksController.orElse(null), homeTransitionObserver,
+ SurfaceControl.Transaction::new);
}
//
@@ -664,7 +666,8 @@ public abstract class WMShellModule {
@Provides
static Object provideIndependentShellComponentsToCreate(
DragAndDropController dragAndDropController,
- Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional) {
+ Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional
+ ) {
return new Object();
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index df1b06225fda..31c8f1e45007 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode;
import android.graphics.Region;
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
import com.android.wm.shell.shared.annotations.ExternalThread;
import java.util.concurrent.Executor;
@@ -49,10 +50,10 @@ public interface DesktopMode {
/** Called when requested to go to desktop mode from the current focused app. */
- void moveFocusedTaskToDesktop(int displayId);
+ void moveFocusedTaskToDesktop(int displayId, DesktopModeTransitionSource transitionSource);
/** Called when requested to go to fullscreen from the current focused desktop app. */
- void moveFocusedTaskToFullscreen(int displayId);
+ void moveFocusedTaskToFullscreen(int displayId, DesktopModeTransitionSource transitionSource);
/** Called when requested to go to split screen from the current focused desktop app. */
void moveFocusedTaskToStageSplit(int displayId, boolean leftOrTop);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index 109868daae7d..fbc11c19a5a2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -159,13 +159,24 @@ class DesktopModeEventLogger {
}
companion object {
+ /**
+ * Describes a task position and dimensions.
+ *
+ * @property instanceId instance id of the task
+ * @property uid uid of the app associated with the task
+ * @property taskHeight height of the task in px
+ * @property taskWidth width of the task in px
+ * @property taskX x-coordinate of the top-left corner
+ * @property taskY y-coordinate of the top-left corner
+ *
+ */
data class TaskUpdate(
val instanceId: Int,
val uid: Int,
- val taskHeight: Int = Int.MIN_VALUE,
- val taskWidth: Int = Int.MIN_VALUE,
- val taskX: Int = Int.MIN_VALUE,
- val taskY: Int = Int.MIN_VALUE,
+ val taskHeight: Int,
+ val taskWidth: Int,
+ val taskX: Int,
+ val taskY: Int,
)
/**
@@ -187,7 +198,10 @@ class DesktopModeEventLogger {
KEYBOARD_SHORTCUT_ENTER(
FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__KEYBOARD_SHORTCUT_ENTER
),
- SCREEN_ON(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__SCREEN_ON)
+ SCREEN_ON(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__SCREEN_ON),
+ APP_FROM_OVERVIEW(
+ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__APP_FROM_OVERVIEW
+ ),
}
/**
@@ -204,7 +218,7 @@ class DesktopModeEventLogger {
FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__KEYBOARD_SHORTCUT_EXIT
),
RETURN_HOME_OR_OVERVIEW(
- FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME
+ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__RETURN_HOME_OR_OVERVIEW
),
TASK_FINISHED(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__TASK_FINISHED),
SCREEN_OFF(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__SCREEN_OFF)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index 5d8e34022841..e71056043d5c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -35,9 +35,16 @@ import androidx.core.util.plus
import androidx.core.util.putAll
import com.android.internal.logging.InstanceId
import com.android.internal.logging.InstanceIdSequence
+import com.android.internal.protolog.common.ProtoLog
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.DesktopModeStatus
import com.android.wm.shell.shared.TransitionUtil
@@ -74,6 +81,9 @@ class DesktopModeLoggerTransitionObserver(
// animation was cancelled, we restore these tasks to calculate the post-Transition state
private val tasksSavedForRecents: SparseArray<TaskInfo> = SparseArray()
+ // Caching whether the previous transition was exit to overview.
+ private var wasPreviousTransitionExitToOverview: Boolean = false
+
// The instanceId for the current logging session
private var loggerInstanceId: InstanceId? = null
@@ -95,7 +105,7 @@ class DesktopModeLoggerTransitionObserver(
finishTransaction: SurfaceControl.Transaction
) {
// this was a new recents animation
- if (info.isRecentsTransition() && tasksSavedForRecents.isEmpty()) {
+ if (info.isExitToRecentsTransition() && tasksSavedForRecents.isEmpty()) {
KtProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"DesktopModeLogger: Recents animation running, saving tasks for later"
@@ -137,6 +147,7 @@ class DesktopModeLoggerTransitionObserver(
preTransitionVisibleFreeformTasks = visibleFreeformTaskInfos,
postTransitionVisibleFreeformTasks = postTransitionVisibleFreeformTasks
)
+ wasPreviousTransitionExitToOverview = info.isExitToRecentsTransition()
}
override fun onTransitionStarting(transition: IBinder) {}
@@ -271,17 +282,23 @@ class DesktopModeLoggerTransitionObserver(
visibleFreeformTaskInfos.putAll(postTransitionVisibleFreeformTasks)
}
- // TODO(b/326231724) - Add logging around taskInfoChanges Updates
/** Compare the old and new state of taskInfos and identify and log the changes */
private fun identifyAndLogTaskUpdates(
sessionId: Int,
preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>
) {
- // find new tasks that were added
postTransitionVisibleFreeformTasks.forEach { taskId, taskInfo ->
- if (!preTransitionVisibleFreeformTasks.containsKey(taskId)) {
- desktopModeEventLogger.logTaskAdded(sessionId, buildTaskUpdateForTask(taskInfo))
+ val currentTaskUpdate = buildTaskUpdateForTask(taskInfo)
+ val previousTaskInfo = preTransitionVisibleFreeformTasks[taskId]
+ when {
+ // new tasks added
+ previousTaskInfo == null ->
+ desktopModeEventLogger.logTaskAdded(sessionId, currentTaskUpdate)
+ // old tasks that were resized or repositioned
+ // TODO(b/347935387): Log changes only once they are stable.
+ buildTaskUpdateForTask(previousTaskInfo) != currentTaskUpdate ->
+ desktopModeEventLogger.logTaskInfoChanged(sessionId, currentTaskUpdate)
}
}
@@ -293,38 +310,71 @@ class DesktopModeLoggerTransitionObserver(
}
}
- // TODO(b/326231724: figure out how to get taskWidth and taskHeight from TaskInfo
private fun buildTaskUpdateForTask(taskInfo: TaskInfo): TaskUpdate {
- val taskUpdate = TaskUpdate(taskInfo.taskId, taskInfo.userId)
- // add task x, y if available
- taskInfo.positionInParent?.let { taskUpdate.copy(taskX = it.x, taskY = it.y) }
-
- return taskUpdate
+ val screenBounds = taskInfo.configuration.windowConfiguration.bounds
+ val positionInParent = taskInfo.positionInParent
+ return TaskUpdate(
+ instanceId = taskInfo.taskId,
+ uid = taskInfo.userId,
+ taskHeight = screenBounds.height(),
+ taskWidth = screenBounds.width(),
+ taskX = positionInParent.x,
+ taskY = positionInParent.y,
+ )
}
/** Get [EnterReason] for this session enter */
- private fun getEnterReason(transitionInfo: TransitionInfo): EnterReason {
- // TODO(b/326231756) - Add support for missing enter reasons
- return when (transitionInfo.type) {
- WindowManager.TRANSIT_WAKE -> EnterReason.SCREEN_ON
- Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> EnterReason.APP_HANDLE_DRAG
- Transitions.TRANSIT_MOVE_TO_DESKTOP -> EnterReason.APP_HANDLE_MENU_BUTTON
- WindowManager.TRANSIT_OPEN -> EnterReason.APP_FREEFORM_INTENT
- else -> EnterReason.UNKNOWN_ENTER
+ private fun getEnterReason(transitionInfo: TransitionInfo): EnterReason =
+ when {
+ transitionInfo.type == WindowManager.TRANSIT_WAKE -> EnterReason.SCREEN_ON
+ transitionInfo.type == Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP ->
+ EnterReason.APP_HANDLE_DRAG
+ transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON ->
+ EnterReason.APP_HANDLE_MENU_BUTTON
+ transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW ->
+ EnterReason.APP_FROM_OVERVIEW
+ transitionInfo.type == TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT ->
+ EnterReason.KEYBOARD_SHORTCUT_ENTER
+ // NOTE: the below condition also applies for EnterReason quickswitch
+ transitionInfo.type == WindowManager.TRANSIT_TO_FRONT -> EnterReason.OVERVIEW
+ // Enter desktop mode from cancelled recents has no transition. Enter is detected on the
+ // next transition involving freeform windows.
+ // TODO(b/346564416): Modify logging for cancelled recents once it transition is
+ // changed. Also see how to account to time difference between actual enter time and
+ // time of this log. Also account for the missed session when exit happens just after
+ // a cancelled recents.
+ wasPreviousTransitionExitToOverview -> EnterReason.OVERVIEW
+ transitionInfo.type == WindowManager.TRANSIT_OPEN -> EnterReason.APP_FREEFORM_INTENT
+ else -> {
+ ProtoLog.w(
+ WM_SHELL_DESKTOP_MODE,
+ "Unknown enter reason for transition type ${transitionInfo.type}",
+ transitionInfo.type
+ )
+ EnterReason.UNKNOWN_ENTER
+ }
}
- }
/** Get [ExitReason] for this session exit */
- private fun getExitReason(transitionInfo: TransitionInfo): ExitReason {
- // TODO(b/326231756) - Add support for missing exit reasons
- return when {
+ private fun getExitReason(transitionInfo: TransitionInfo): ExitReason =
+ when {
transitionInfo.type == WindowManager.TRANSIT_SLEEP -> ExitReason.SCREEN_OFF
transitionInfo.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED
- transitionInfo.type == Transitions.TRANSIT_EXIT_DESKTOP_MODE -> ExitReason.DRAG_TO_EXIT
- transitionInfo.isRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW
- else -> ExitReason.UNKNOWN_EXIT
+ transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG -> ExitReason.DRAG_TO_EXIT
+ transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON ->
+ ExitReason.APP_HANDLE_MENU_BUTTON_EXIT
+ transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT ->
+ ExitReason.KEYBOARD_SHORTCUT_EXIT
+ transitionInfo.isExitToRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW
+ else -> {
+ ProtoLog.w(
+ WM_SHELL_DESKTOP_MODE,
+ "Unknown exit reason for transition type ${transitionInfo.type}",
+ transitionInfo.type
+ )
+ ExitReason.UNKNOWN_EXIT
+ }
}
- }
/** Adds tasks to the saved copy of freeform taskId, taskInfo. Only used for testing. */
@VisibleForTesting
@@ -347,7 +397,7 @@ class DesktopModeLoggerTransitionObserver(
return this.windowingMode == WINDOWING_MODE_FREEFORM
}
- private fun TransitionInfo.isRecentsTransition(): Boolean {
+ private fun TransitionInfo.isExitToRecentsTransition(): Boolean {
return this.type == WindowManager.TRANSIT_TO_FRONT &&
this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
index bc27f341b566..1a6ca0efa748 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt
@@ -16,7 +16,7 @@
package com.android.wm.shell.desktopmode
-import android.window.WindowContainerTransaction
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN
import com.android.wm.shell.sysui.ShellCommandHandler
import java.io.PrintWriter
@@ -64,7 +64,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
return false
}
- return controller.moveToDesktop(taskId, WindowContainerTransaction())
+ return controller.moveToDesktop(taskId, transitionSource = UNKNOWN)
}
private fun runMoveToNextDisplay(args: Array<String>, pw: PrintWriter): Boolean {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 7d01580ecb6e..81891ce91e04 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
@@ -46,6 +46,9 @@ class DesktopModeTaskRepository {
val activeTasks: ArraySet<Int> = ArraySet(),
val visibleTasks: ArraySet<Int> = ArraySet(),
val minimizedTasks: ArraySet<Int> = ArraySet(),
+ // Tasks that are closing, but are still visible
+ // TODO(b/332682201): Remove when the repository state is updated via TransitionObserver
+ val closingTasks: ArraySet<Int> = ArraySet(),
// Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
)
@@ -169,6 +172,42 @@ class DesktopModeTaskRepository {
return result
}
+ /**
+ * Mark a task with given [taskId] as closing on given [displayId]
+ *
+ * @return `true` if the task was not closing on given [displayId]
+ */
+ fun addClosingTask(displayId: Int, taskId: Int): Boolean {
+ val added = displayData.getOrCreate(displayId).closingTasks.add(taskId)
+ if (added) {
+ KtProtoLog.d(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTaskRepo: added closing task=%d displayId=%d",
+ taskId,
+ displayId
+ )
+ }
+ return added
+ }
+
+ /**
+ * Remove task with given [taskId] from closing tasks.
+ *
+ * @return `true` if the task was closing
+ */
+ fun removeClosingTask(taskId: Int): Boolean {
+ var removed = false
+ displayData.forEach { _, data ->
+ if (data.closingTasks.remove(taskId)) {
+ removed = true
+ }
+ }
+ if (removed) {
+ KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopTaskRepo: remove closing task=%d", taskId)
+ }
+ return removed
+ }
+
/** Check if a task with the given [taskId] was marked as an active task */
fun isActiveTask(taskId: Int): Boolean {
return displayData.valueIterator().asSequence().any { data ->
@@ -176,6 +215,10 @@ class DesktopModeTaskRepository {
}
}
+ /** Check if a task with the given [taskId] was marked as a closing task */
+ fun isClosingTask(taskId: Int): Boolean =
+ displayData.valueIterator().asSequence().any { data -> taskId in data.closingTasks }
+
/** Whether a task is visible. */
fun isVisibleTask(taskId: Int): Boolean {
return displayData.valueIterator().asSequence().any { data ->
@@ -190,12 +233,17 @@ class DesktopModeTaskRepository {
}
}
- /** Check if a task with the given [taskId] is the only active task on its display */
- fun isOnlyActiveTask(taskId: Int): Boolean {
- return displayData.valueIterator().asSequence().any { data ->
- data.activeTasks.singleOrNull() == taskId
+ /**
+ * Check if a task with the given [taskId] is the only visible, non-closing, not-minimized task
+ * on its display
+ */
+ fun isOnlyVisibleNonClosingTask(taskId: Int): Boolean =
+ displayData.valueIterator().asSequence().any { data ->
+ data.visibleTasks
+ .subtract(data.closingTasks)
+ .subtract(data.minimizedTasks)
+ .singleOrNull() == taskId
}
- }
/** Get a set of the active tasks for given [displayId] */
fun getActiveTasks(displayId: Int): ArraySet<Int> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt
new file mode 100644
index 000000000000..b24bd10eaa0d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypes.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.view.WindowManager.TransitionType
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
+import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TYPES
+
+/**
+ * Contains desktop mode [TransitionType]s (extended from [TRANSIT_DESKTOP_MODE_TYPES]) and helper
+ * methods.
+ */
+object DesktopModeTransitionTypes {
+
+ const val TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON = TRANSIT_DESKTOP_MODE_TYPES + 1
+ const val TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW = TRANSIT_DESKTOP_MODE_TYPES + 2
+ const val TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT = TRANSIT_DESKTOP_MODE_TYPES + 3
+ const val TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN = TRANSIT_DESKTOP_MODE_TYPES + 4
+ const val TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG = TRANSIT_DESKTOP_MODE_TYPES + 5
+ const val TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON = TRANSIT_DESKTOP_MODE_TYPES + 6
+ const val TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT = TRANSIT_DESKTOP_MODE_TYPES + 7
+ const val TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN = TRANSIT_DESKTOP_MODE_TYPES + 8
+
+ /** Return whether the [TransitionType] corresponds to a transition to enter desktop mode. */
+ @JvmStatic
+ fun @receiver:TransitionType Int.isEnterDesktopModeTransition(): Boolean {
+ return this in
+ listOf(
+ TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON,
+ TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW,
+ TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT,
+ TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN
+ )
+ }
+
+ /**
+ * Returns corresponding desktop mode enter [TransitionType] for a
+ * [DesktopModeTransitionSource].
+ */
+ @JvmStatic
+ @TransitionType
+ fun DesktopModeTransitionSource.getEnterTransitionType(): Int {
+ return when (this) {
+ DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON ->
+ TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON
+ DesktopModeTransitionSource.APP_FROM_OVERVIEW ->
+ TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW
+ DesktopModeTransitionSource.KEYBOARD_SHORTCUT ->
+ TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT
+ else -> TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN
+ }
+ }
+
+ /** Return whether the [TransitionType] corresponds to a transition to exit desktop mode. */
+ @JvmStatic
+ fun @receiver:TransitionType Int.isExitDesktopModeTransition(): Boolean {
+ return this in
+ listOf(
+ TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG,
+ TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON,
+ TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT,
+ TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN
+ )
+ }
+
+ /**
+ * Returns corresponding desktop mode exit [TransitionType] for a [DesktopModeTransitionSource].
+ */
+ @JvmStatic
+ @TransitionType
+ fun DesktopModeTransitionSource.getExitTransitionType(): Int {
+ return when (this) {
+ DesktopModeTransitionSource.TASK_DRAG -> TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
+ DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON ->
+ TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON
+ DesktopModeTransitionSource.KEYBOARD_SHORTCUT ->
+ TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT
+ else -> TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN
+ }
+ }
+}
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 ef384c74cb5e..196538248709 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
@@ -40,7 +40,6 @@ import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
import android.view.WindowManager.TRANSIT_NONE
import android.view.WindowManager.TRANSIT_OPEN
-import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.RemoteTransition
import android.window.TransitionInfo
@@ -54,7 +53,6 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
-import com.android.wm.shell.common.ExecutorUtils
import com.android.wm.shell.common.ExternalInterfaceBinder
import com.android.wm.shell.common.LaunchAdjacentController
import com.android.wm.shell.common.MultiInstanceHelper
@@ -63,6 +61,7 @@ import com.android.wm.shell.common.RemoteCallable
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SingleInstanceRemoteListener
import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
import com.android.wm.shell.compatui.isSingleTopActivityTranslucent
@@ -75,7 +74,8 @@ import com.android.wm.shell.recents.RecentsTransitionHandler
import com.android.wm.shell.recents.RecentsTransitionStateListener
import com.android.wm.shell.shared.DesktopModeStatus
import com.android.wm.shell.shared.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE
-import com.android.wm.shell.shared.DesktopModeStatus.isDesktopDensityOverrideSet
+import com.android.wm.shell.shared.DesktopModeStatus.useDesktopOverrideDensity
+import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.annotations.ExternalThread
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.splitscreen.SplitScreenController
@@ -155,6 +155,8 @@ class DesktopTasksController(
visualIndicator = null
}
}
+ private val sysUIPackageName = context.resources.getString(
+ com.android.internal.R.string.config_systemUi)
private val transitionAreaHeight
get() =
@@ -214,6 +216,11 @@ class DesktopTasksController(
return visualIndicator
}
+ // TODO(b/347289970): Consider replacing with API
+ private fun isSystemUIApplication(taskInfo: RunningTaskInfo): Boolean {
+ return taskInfo.baseActivity?.packageName == sysUIPackageName
+ }
+
fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) {
toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
@@ -253,7 +260,7 @@ class DesktopTasksController(
}
/** Enter desktop by using the focused task in given `displayId` */
- fun moveFocusedTaskToDesktop(displayId: Int) {
+ fun moveFocusedTaskToDesktop(displayId: Int, transitionSource: DesktopModeTransitionSource) {
val allFocusedTasks =
shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo ->
taskInfo.isFocused &&
@@ -272,11 +279,11 @@ class DesktopTasksController(
} else {
allFocusedTasks[0]
}
- moveToDesktop(splitFocusedTask)
+ moveToDesktop(splitFocusedTask, transitionSource = transitionSource)
}
1 -> {
// Fullscreen case where we move the current focused task.
- moveToDesktop(allFocusedTasks[0].taskId)
+ moveToDesktop(allFocusedTasks[0].taskId, transitionSource = transitionSource)
}
else -> {
KtProtoLog.w(
@@ -293,17 +300,20 @@ class DesktopTasksController(
/** Move a task with given `taskId` to desktop */
fun moveToDesktop(
taskId: Int,
- wct: WindowContainerTransaction = WindowContainerTransaction()
+ wct: WindowContainerTransaction = WindowContainerTransaction(),
+ transitionSource: DesktopModeTransitionSource,
): Boolean {
shellTaskOrganizer.getRunningTaskInfo(taskId)?.let {
- moveToDesktop(it, wct)
- } ?: moveToDesktopFromNonRunningTask(taskId, wct)
+ moveToDesktop(it, wct, transitionSource)
+ }
+ ?: moveToDesktopFromNonRunningTask(taskId, wct, transitionSource)
return true
}
private fun moveToDesktopFromNonRunningTask(
taskId: Int,
- wct: WindowContainerTransaction
+ wct: WindowContainerTransaction,
+ transitionSource: DesktopModeTransitionSource,
): Boolean {
recentTasksController?.findTaskInBackground(taskId)?.let {
KtProtoLog.v(
@@ -316,10 +326,11 @@ class DesktopTasksController(
bringDesktopAppsToFrontBeforeShowingNewTask(DEFAULT_DISPLAY, wct, taskId)
addMoveToDesktopChangesNonRunningTask(wct, taskId)
// TODO(343149901): Add DPI changes for task launch
- val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct)
+ val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
addPendingMinimizeTransition(transition, taskToMinimize)
return true
- } ?: return false
+ }
+ ?: return false
}
private fun addMoveToDesktopChangesNonRunningTask(
@@ -331,12 +342,11 @@ class DesktopTasksController(
wct.startTask(taskId, options.toBundle())
}
- /**
- * Move a task to desktop
- */
+ /** Move a task to desktop */
fun moveToDesktop(
task: RunningTaskInfo,
- wct: WindowContainerTransaction = WindowContainerTransaction()
+ wct: WindowContainerTransaction = WindowContainerTransaction(),
+ transitionSource: DesktopModeTransitionSource,
) {
if (Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task)) {
KtProtoLog.w(
@@ -346,6 +356,14 @@ class DesktopTasksController(
)
return
}
+ if (isSystemUIApplication(task)) {
+ KtProtoLog.w(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: Cannot enter desktop, " +
+ "systemUI top activity found."
+ )
+ return
+ }
KtProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"DesktopTasksController: moveToDesktop taskId=%d",
@@ -358,7 +376,7 @@ class DesktopTasksController(
addMoveToDesktopChanges(wct, task)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct)
+ val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
addPendingMinimizeTransition(transition, taskToMinimize)
} else {
shellTaskOrganizer.applyTransaction(wct)
@@ -424,25 +442,34 @@ class DesktopTasksController(
* active task.
*
* @param wct transaction to modify if the last active task is closed
+ * @param displayId display id of the window that's being closed
* @param taskId task id of the window that's being closed
*/
- fun onDesktopWindowClose(wct: WindowContainerTransaction, taskId: Int) {
- if (desktopModeTaskRepository.isOnlyActiveTask(taskId)) {
+ fun onDesktopWindowClose(wct: WindowContainerTransaction, displayId: Int, taskId: Int) {
+ if (desktopModeTaskRepository.isOnlyVisibleNonClosingTask(taskId)) {
removeWallpaperActivity(wct)
}
+ if (!desktopModeTaskRepository.addClosingTask(displayId, taskId)) {
+ // Could happen if the task hasn't been removed from closing list after it disappeared
+ KtProtoLog.w(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: the task with taskId=%d is already closing!",
+ taskId
+ )
+ }
}
/** Move a task with given `taskId` to fullscreen */
- fun moveToFullscreen(taskId: Int) {
+ fun moveToFullscreen(taskId: Int, transitionSource: DesktopModeTransitionSource) {
shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task ->
- moveToFullscreenWithAnimation(task, task.positionInParent)
+ moveToFullscreenWithAnimation(task, task.positionInParent, transitionSource)
}
}
/** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */
- fun enterFullscreen(displayId: Int) {
+ fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) {
getFocusedFreeformTask(displayId)?.let {
- moveToFullscreenWithAnimation(it, it.positionInParent)
+ moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource)
}
}
@@ -486,10 +513,16 @@ class DesktopTasksController(
"DesktopTasksController: cancelDragToDesktop taskId=%d",
task.taskId
)
- dragToDesktopTransitionHandler.cancelDragToDesktopTransition()
+ dragToDesktopTransitionHandler.cancelDragToDesktopTransition(
+ DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL
+ )
}
- private fun moveToFullscreenWithAnimation(task: RunningTaskInfo, position: Point) {
+ private fun moveToFullscreenWithAnimation(
+ task: RunningTaskInfo,
+ position: Point,
+ transitionSource: DesktopModeTransitionSource
+ ) {
KtProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"DesktopTasksController: moveToFullscreen with animation taskId=%d",
@@ -500,7 +533,7 @@ class DesktopTasksController(
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
exitDesktopTaskTransitionHandler.startTransition(
- Transitions.TRANSIT_EXIT_DESKTOP_MODE,
+ transitionSource,
wct,
position,
mOnAnimationFinishedCallback
@@ -846,8 +879,8 @@ class DesktopTasksController(
reason = "recents animation is running"
false
}
- // Handle back navigation for the last window if wallpaper available
- shouldRemoveWallpaper(request) -> true
+ // Handle task closing for the last window if wallpaper is available
+ shouldHandleTaskClosing(request) -> true
// Only handle open or to front transitions
request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> {
reason = "transition type not handled (${request.type})"
@@ -885,9 +918,12 @@ class DesktopTasksController(
val result =
triggerTask?.let { task ->
when {
- request.type == TRANSIT_TO_BACK -> handleBackNavigation(task)
+ // Check if the closing task needs to be handled
+ TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task)
// Check if the task has a top transparent activity
- shouldLaunchAsModal(task) -> handleTransparentTaskLaunch(task)
+ shouldLaunchAsModal(task) -> handleIncompatibleTaskLaunch(task)
+ // Check if the task has a top systemUI activity
+ isSystemUIApplication(task) -> handleIncompatibleTaskLaunch(task)
// Check if fullscreen task should be updated
task.isFullscreen -> handleFullscreenTaskLaunch(task, transition)
// Check if freeform task should be updated
@@ -921,15 +957,14 @@ class DesktopTasksController(
.forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) }
}
+ // TODO(b/347289970): Consider replacing with API
private fun shouldLaunchAsModal(task: TaskInfo) =
Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task)
- private fun shouldRemoveWallpaper(request: TransitionRequestInfo): Boolean {
+ private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean {
return Flags.enableDesktopWindowingWallpaperActivity() &&
- request.type == TRANSIT_TO_BACK &&
- request.triggerTask?.let { task ->
- desktopModeTaskRepository.isOnlyActiveTask(task.taskId)
- } ?: false
+ TransitionUtil.isClosingType(request.type) &&
+ request.triggerTask != null
}
private fun handleFreeformTaskLaunch(
@@ -940,7 +975,7 @@ class DesktopTasksController(
if (!desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) {
KtProtoLog.d(
WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: switch freeform task to fullscreen oon transition" +
+ "DesktopTasksController: bring desktop tasks to front on transition" +
" taskId=%d",
task.taskId
)
@@ -950,7 +985,7 @@ class DesktopTasksController(
}
}
val wct = WindowContainerTransaction()
- if (isDesktopDensityOverrideSet()) {
+ if (useDesktopOverrideDensity()) {
wct.setDensityDpi(task.token, DESKTOP_DENSITY_OVERRIDE)
}
// Desktop Mode is showing and we're launching a new Task - we might need to minimize
@@ -986,24 +1021,36 @@ class DesktopTasksController(
return null
}
- // Always launch transparent tasks in fullscreen.
- private fun handleTransparentTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
+ /**
+ * If a task is not compatible with desktop mode freeform, it should always be launched in
+ * fullscreen.
+ */
+ private fun handleIncompatibleTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
// Already fullscreen, no-op.
if (task.isFullscreen) return null
return WindowContainerTransaction().also { wct -> addMoveToFullscreenChanges(wct, task) }
}
- /** Handle back navigation by removing wallpaper activity if it's the last active task */
- private fun handleBackNavigation(task: RunningTaskInfo): WindowContainerTransaction? {
- if (
- desktopModeTaskRepository.isOnlyActiveTask(task.taskId) &&
+ /** Handle task closing by removing wallpaper activity if it's the last active task */
+ private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? {
+ val wct = if (
+ desktopModeTaskRepository.isOnlyVisibleNonClosingTask(task.taskId) &&
desktopModeTaskRepository.wallpaperActivityToken != null
) {
// Remove wallpaper activity when the last active task is removed
- return WindowContainerTransaction().also { wct -> removeWallpaperActivity(wct) }
+ WindowContainerTransaction().also { wct -> removeWallpaperActivity(wct) }
} else {
- return null
+ null
}
+ if (!desktopModeTaskRepository.addClosingTask(task.displayId, task.taskId)) {
+ // Could happen if the task hasn't been removed from closing list after it disappeared
+ KtProtoLog.w(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: the task with taskId=%d is already closing!",
+ task.taskId
+ )
+ }
+ return wct
}
private fun addMoveToDesktopChanges(
@@ -1025,7 +1072,7 @@ class DesktopTasksController(
}
wct.setWindowingMode(taskInfo.token, targetWindowingMode)
wct.reorder(taskInfo.token, true /* onTop */)
- if (isDesktopDensityOverrideSet()) {
+ if (useDesktopOverrideDensity()) {
wct.setDensityDpi(taskInfo.token, DESKTOP_DENSITY_OVERRIDE)
}
}
@@ -1045,7 +1092,7 @@ class DesktopTasksController(
}
wct.setWindowingMode(taskInfo.token, targetWindowingMode)
wct.setBounds(taskInfo.token, Rect())
- if (isDesktopDensityOverrideSet()) {
+ if (useDesktopOverrideDensity()) {
wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
}
}
@@ -1105,20 +1152,31 @@ class DesktopTasksController(
@JvmOverloads
fun requestSplit(
taskInfo: RunningTaskInfo,
- leftOrTop: Boolean = false,
+ leftOrTop: Boolean = false
) {
- val windowingMode = taskInfo.windowingMode
- if (
- windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_FREEFORM
- ) {
- val wct = WindowContainerTransaction()
- addMoveToSplitChanges(wct, taskInfo)
- splitScreenController.requestEnterSplitSelect(
- taskInfo,
- wct,
- if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT,
- taskInfo.configuration.windowConfiguration.bounds
- )
+ // If a drag to desktop is in progress, we want to enter split select
+ // even if the requesting task is already in split.
+ val isDragging = dragToDesktopTransitionHandler.inProgress
+ val shouldRequestSplit = taskInfo.isFullscreen || taskInfo.isFreeform || isDragging
+ if (shouldRequestSplit) {
+ if (isDragging) {
+ releaseVisualIndicator()
+ val cancelState = if (leftOrTop) {
+ DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT
+ } else {
+ DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT
+ }
+ dragToDesktopTransitionHandler.cancelDragToDesktopTransition(cancelState)
+ } else {
+ val wct = WindowContainerTransaction()
+ addMoveToSplitChanges(wct, taskInfo)
+ splitScreenController.requestEnterSplitSelect(
+ taskInfo,
+ wct,
+ if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT,
+ taskInfo.configuration.windowConfiguration.bounds
+ )
+ }
}
}
@@ -1206,7 +1264,11 @@ class DesktopTasksController(
)
when (indicatorType) {
DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> {
- moveToFullscreenWithAnimation(taskInfo, position)
+ moveToFullscreenWithAnimation(
+ taskInfo,
+ position,
+ DesktopModeTransitionSource.TASK_DRAG
+ )
}
DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
releaseVisualIndicator()
@@ -1247,7 +1309,10 @@ class DesktopTasksController(
* @param taskInfo the task being dragged.
* @param y height of drag, to be checked against status bar height.
*/
- fun onDragPositioningEndThroughStatusBar(inputCoordinates: PointF, taskInfo: RunningTaskInfo) {
+ fun onDragPositioningEndThroughStatusBar(
+ inputCoordinates: PointF,
+ taskInfo: RunningTaskInfo,
+ ) {
val indicator = getVisualIndicator() ?: return
val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
when (indicatorType) {
@@ -1264,10 +1329,10 @@ class DesktopTasksController(
cancelDragToDesktop(taskInfo)
}
DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
- finalizeDragToDesktop(taskInfo, getSnapBounds(taskInfo, SnapPosition.LEFT))
+ requestSplit(taskInfo, leftOrTop = true)
}
DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
- finalizeDragToDesktop(taskInfo, getSnapBounds(taskInfo, SnapPosition.RIGHT))
+ requestSplit(taskInfo, leftOrTop = false)
}
}
}
@@ -1375,12 +1440,22 @@ class DesktopTasksController(
}
}
- override fun moveFocusedTaskToDesktop(displayId: Int) {
- mainExecutor.execute { this@DesktopTasksController.moveFocusedTaskToDesktop(displayId) }
+ override fun moveFocusedTaskToDesktop(
+ displayId: Int,
+ transitionSource: DesktopModeTransitionSource
+ ) {
+ mainExecutor.execute {
+ this@DesktopTasksController.moveFocusedTaskToDesktop(displayId, transitionSource)
+ }
}
- override fun moveFocusedTaskToFullscreen(displayId: Int) {
- mainExecutor.execute { this@DesktopTasksController.enterFullscreen(displayId) }
+ override fun moveFocusedTaskToFullscreen(
+ displayId: Int,
+ transitionSource: DesktopModeTransitionSource
+ ) {
+ mainExecutor.execute {
+ this@DesktopTasksController.enterFullscreen(displayId, transitionSource)
+ }
}
override fun moveFocusedTaskToStageSplit(displayId: Int, leftOrTop: Boolean) {
@@ -1432,13 +1507,13 @@ class DesktopTasksController(
}
override fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition?) {
- ExecutorUtils.executeRemoteCallWithTaskPermission(controller, "showDesktopApps") { c ->
+ executeRemoteCallWithTaskPermission(controller, "showDesktopApps") { c ->
c.showDesktopApps(displayId, remoteTransition)
}
}
override fun showDesktopApp(taskId: Int) {
- ExecutorUtils.executeRemoteCallWithTaskPermission(controller, "showDesktopApp") { c ->
+ executeRemoteCallWithTaskPermission(controller, "showDesktopApp") { c ->
c.moveTaskToFront(taskId)
}
}
@@ -1456,7 +1531,7 @@ class DesktopTasksController(
override fun getVisibleTaskCount(displayId: Int): Int {
val result = IntArray(1)
- ExecutorUtils.executeRemoteCallWithTaskPermission(
+ executeRemoteCallWithTaskPermission(
controller,
"getVisibleTaskCount",
{ controller -> result[0] = controller.getVisibleTaskCount(displayId) },
@@ -1466,7 +1541,7 @@ class DesktopTasksController(
}
override fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) {
- ExecutorUtils.executeRemoteCallWithTaskPermission(
+ executeRemoteCallWithTaskPermission(
controller,
"onDesktopSplitSelectAnimComplete"
) { c ->
@@ -1480,14 +1555,14 @@ class DesktopTasksController(
"IDesktopModeImpl: set task listener=%s",
listener ?: "null"
)
- ExecutorUtils.executeRemoteCallWithTaskPermission(controller, "setTaskListener") { _ ->
+ executeRemoteCallWithTaskPermission(controller, "setTaskListener") { _ ->
listener?.let { remoteListener.register(it) } ?: remoteListener.unregister()
}
}
- override fun moveToDesktop(taskId: Int) {
- ExecutorUtils.executeRemoteCallWithTaskPermission(controller, "moveToDesktop") { c ->
- c.moveToDesktop(taskId)
+ override fun moveToDesktop(taskId: Int, transitionSource: DesktopModeTransitionSource) {
+ executeRemoteCallWithTaskPermission(controller, "moveToDesktop") { c ->
+ c.moveToDesktop(taskId, transitionSource = transitionSource)
}
}
}
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 98c79d7174a9..d99b724c936f 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
@@ -4,6 +4,7 @@ import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.RectEvaluator
import android.animation.ValueAnimator
+import android.app.ActivityManager.RunningTaskInfo
import android.app.ActivityOptions
import android.app.ActivityOptions.SourceInfo
import android.app.ActivityTaskManager.INVALID_TASK_ID
@@ -12,9 +13,11 @@ import android.app.PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
import android.app.PendingIntent.FLAG_MUTABLE
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
import android.content.Context
import android.content.Intent
import android.content.Intent.FILL_IN_COMPONENT
+import android.graphics.PointF
import android.graphics.Rect
import android.os.Bundle
import android.os.IBinder
@@ -30,6 +33,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED
+import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition
import com.android.wm.shell.protolog.ShellProtoLogGroup
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.splitscreen.SplitScreenController
@@ -186,7 +190,7 @@ class DragToDesktopTransitionHandler(
* outside the desktop drop zone and is instead dropped back into the status bar region that
* means the user wants to remain in their current windowing mode.
*/
- fun cancelDragToDesktopTransition() {
+ fun cancelDragToDesktopTransition(cancelState: CancelState) {
if (!inProgress) {
// Don't attempt to cancel a drag to desktop transition since there is no transition in
// progress which means that the drag to desktop transition was never successfully
@@ -200,13 +204,32 @@ class DragToDesktopTransitionHandler(
clearState()
return
}
- state.cancelled = true
- if (state.draggedTaskChange != null) {
+ state.cancelState = cancelState
+
+ if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) {
// Regular case, transient launch of Home happened as is waiting for the cancel
// 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()
+ } else if (
+ state.draggedTaskChange != null &&
+ (cancelState == CancelState.CANCEL_SPLIT_LEFT ||
+ cancelState == CancelState.CANCEL_SPLIT_RIGHT)
+ ) {
+ // We have a valid dragged task, but the animation will be handled by
+ // SplitScreenController; request the transition here.
+ @SplitPosition val splitPosition = if (cancelState == CancelState.CANCEL_SPLIT_LEFT) {
+ SPLIT_POSITION_TOP_OR_LEFT
+ } else {
+ SPLIT_POSITION_BOTTOM_OR_RIGHT
+ }
+ val wct = WindowContainerTransaction()
+ restoreWindowOrder(wct, state)
+ state.startTransitionFinishTransaction?.apply()
+ state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
+ requestSplitFromScaledTask(splitPosition, wct)
+ clearState()
} else {
// There's no dragged task, this can happen when the "cancel" happened too quickly
// before the "start" transition is even ready (like on a fling gesture). The
@@ -217,6 +240,54 @@ class DragToDesktopTransitionHandler(
}
}
+ /** Calculate the bounds of a scaled task, then use those bounds to request split select. */
+ private fun requestSplitFromScaledTask(
+ @SplitPosition splitPosition: Int,
+ wct: WindowContainerTransaction
+ ) {
+ val state = requireTransitionState()
+ val taskInfo = state.draggedTaskChange?.taskInfo
+ ?: error("Expected non-null taskInfo")
+ val taskBounds = Rect(taskInfo.configuration.windowConfiguration.bounds)
+ val taskScale = state.dragAnimator.scale
+ val scaledWidth = taskBounds.width() * taskScale
+ val scaledHeight = taskBounds.height() * taskScale
+ val dragPosition = PointF(state.dragAnimator.position)
+ state.dragAnimator.cancelAnimator()
+ val animatedTaskBounds = Rect(
+ dragPosition.x.toInt(),
+ dragPosition.y.toInt(),
+ (dragPosition.x + scaledWidth).toInt(),
+ (dragPosition.y + scaledHeight).toInt()
+ )
+ requestSplitSelect(wct, taskInfo, splitPosition, animatedTaskBounds)
+ }
+
+ private fun requestSplitSelect(
+ wct: WindowContainerTransaction,
+ taskInfo: RunningTaskInfo,
+ @SplitPosition splitPosition: Int,
+ taskBounds: Rect = Rect(taskInfo.configuration.windowConfiguration.bounds)
+ ) {
+ // Prepare to exit split in order to enter split select.
+ if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
+ splitScreenController.prepareExitSplitScreen(
+ wct,
+ splitScreenController.getStageOfTask(taskInfo.taskId),
+ SplitScreenController.EXIT_REASON_DESKTOP_MODE
+ )
+ splitScreenController.transitionHandler.onSplitToDesktop()
+ }
+ wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW)
+ wct.setDensityDpi(taskInfo.token, context.resources.displayMetrics.densityDpi)
+ splitScreenController.requestEnterSplitSelect(
+ taskInfo,
+ wct,
+ splitPosition,
+ taskBounds
+ )
+ }
+
override fun startAnimation(
transition: IBinder,
info: TransitionInfo,
@@ -261,7 +332,7 @@ class DragToDesktopTransitionHandler(
is TransitionState.FromSplit -> {
state.splitRootChange = change
val layer =
- if (!state.cancelled) {
+ if (state.cancelState == CancelState.NO_CANCEL) {
// Normal case, split root goes to the bottom behind everything
// else.
appLayers - i
@@ -311,8 +382,18 @@ class DragToDesktopTransitionHandler(
// Do not do this in the cancel-early case though, since in that case nothing should
// happen on screen so the layering will remain the same as if no transition
// occurred.
- if (change.taskInfo?.taskId == state.draggedTaskId && !state.cancelled) {
+ if (
+ change.taskInfo?.taskId == state.draggedTaskId &&
+ state.cancelState != CancelState.STANDARD_CANCEL
+ ) {
+ // We need access to the dragged task's change in both non-cancel and split
+ // cancel cases.
state.draggedTaskChange = change
+ }
+ if (
+ change.taskInfo?.taskId == state.draggedTaskId &&
+ state.cancelState == CancelState.NO_CANCEL
+ ) {
taskDisplayAreaOrganizer.reparentToDisplayArea(
change.endDisplayId,
change.leash,
@@ -331,11 +412,11 @@ class DragToDesktopTransitionHandler(
state.startTransitionFinishTransaction = finishTransaction
startTransaction.apply()
- if (!state.cancelled) {
+ if (state.cancelState == CancelState.NO_CANCEL) {
// Normal case, start animation to scale down the dragged task. It'll also be moved to
// follow the finger and when released we'll start the next phase/transition.
state.dragAnimator.startAnimation()
- } else {
+ } else if (state.cancelState == CancelState.STANDARD_CANCEL) {
// Cancel-early case, the state was flagged was cancelled already, which means the
// gesture ended in the cancel region. This can happen even before the start transition
// is ready/animate here when cancelling quickly like with a fling. There's no point
@@ -343,6 +424,26 @@ class DragToDesktopTransitionHandler(
// directly into starting the cancel transition to restore WM order. Surfaces should
// not move as if no transition happened.
startCancelDragToDesktopTransition()
+ } else if (
+ state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
+ state.cancelState == CancelState.CANCEL_SPLIT_RIGHT
+ ){
+ // Cancel-early case for split-cancel. The state was flagged already as a cancel for
+ // requesting split select. Similar to the above, this can happen due to quick fling
+ // gestures. We can simply request split here without needing to calculate animated
+ // task bounds as the task has not shrunk at all.
+ val splitPosition = if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT) {
+ SPLIT_POSITION_TOP_OR_LEFT
+ } else {
+ SPLIT_POSITION_BOTTOM_OR_RIGHT
+ }
+ val taskInfo = state.draggedTaskChange?.taskInfo
+ ?: error("Expected non-null task info.")
+ val wct = WindowContainerTransaction()
+ restoreWindowOrder(wct)
+ state.startTransitionFinishTransaction?.apply()
+ state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
+ requestSplitSelect(wct, taskInfo, splitPosition)
}
return true
}
@@ -355,6 +456,12 @@ class DragToDesktopTransitionHandler(
finishCallback: Transitions.TransitionFinishCallback
) {
val state = requireTransitionState()
+ // We don't want to merge the split select animation if that's what we requested.
+ if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
+ state.cancelState == CancelState.CANCEL_SPLIT_RIGHT) {
+ clearState()
+ return
+ }
val isCancelTransition =
info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP &&
transition == state.cancelTransitionToken &&
@@ -552,6 +659,17 @@ class DragToDesktopTransitionHandler(
private fun startCancelDragToDesktopTransition() {
val state = requireTransitionState()
val wct = WindowContainerTransaction()
+ restoreWindowOrder(wct, state)
+ state.cancelTransitionToken =
+ transitions.startTransition(
+ TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this
+ )
+ }
+
+ private fun restoreWindowOrder(
+ wct: WindowContainerTransaction,
+ state: TransitionState = requireTransitionState()
+ ) {
when (state) {
is TransitionState.FromFullscreen -> {
// There may have been tasks sent behind home that are not the dragged task (like
@@ -580,9 +698,6 @@ class DragToDesktopTransitionHandler(
}
val homeWc = state.homeToken ?: error("Home task should be non-null before cancelling")
wct.restoreTransientOrder(homeWc)
-
- state.cancelTransitionToken =
- transitions.startTransition(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this)
}
private fun clearState() {
@@ -624,7 +739,7 @@ class DragToDesktopTransitionHandler(
abstract var cancelTransitionToken: IBinder?
abstract var homeToken: WindowContainerToken?
abstract var draggedTaskChange: Change?
- abstract var cancelled: Boolean
+ abstract var cancelState: CancelState
abstract var startAborted: Boolean
data class FromFullscreen(
@@ -636,7 +751,7 @@ class DragToDesktopTransitionHandler(
override var cancelTransitionToken: IBinder? = null,
override var homeToken: WindowContainerToken? = null,
override var draggedTaskChange: Change? = null,
- override var cancelled: Boolean = false,
+ override var cancelState: CancelState = CancelState.NO_CANCEL,
override var startAborted: Boolean = false,
var otherRootChanges: MutableList<Change> = mutableListOf()
) : TransitionState()
@@ -650,13 +765,25 @@ class DragToDesktopTransitionHandler(
override var cancelTransitionToken: IBinder? = null,
override var homeToken: WindowContainerToken? = null,
override var draggedTaskChange: Change? = null,
- override var cancelled: Boolean = false,
+ override var cancelState: CancelState = CancelState.NO_CANCEL,
override var startAborted: Boolean = false,
var splitRootChange: Change? = null,
var otherSplitTask: Int
) : TransitionState()
}
+ /** Enum to provide context on cancelling a drag to desktop event. */
+ enum class CancelState {
+ /** No cancel case; this drag is not flagged for a cancel event. */
+ NO_CANCEL,
+ /** A standard cancel event; should restore task to previous windowing mode. */
+ STANDARD_CANCEL,
+ /** A cancel event where the task will request to enter split on the left side. */
+ CANCEL_SPLIT_LEFT,
+ /** A cancel event where the task will request to enter split on the right side. */
+ CANCEL_SPLIT_RIGHT
+ }
+
companion object {
/** The duration of the animation to commit or cancel the drag-to-desktop gesture. */
private const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
index 526cf4d0295b..e5b624f91c54 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
@@ -18,7 +18,8 @@ package com.android.wm.shell.desktopmode;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_TO_DESKTOP;
+import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getEnterTransitionType;
+import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isEnterDesktopModeTransition;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -30,6 +31,7 @@ import android.os.IBinder;
import android.util.Slog;
import android.view.SurfaceControl;
import android.view.WindowManager;
+import android.view.WindowManager.TransitionType;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
@@ -37,6 +39,7 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener;
@@ -82,8 +85,12 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition
* @param wct WindowContainerTransaction for transition
* @return the token representing the started transition
*/
- public IBinder moveToDesktop(@NonNull WindowContainerTransaction wct) {
- final IBinder token = mTransitions.startTransition(TRANSIT_MOVE_TO_DESKTOP, wct, this);
+ public IBinder moveToDesktop(
+ @NonNull WindowContainerTransaction wct,
+ DesktopModeTransitionSource transitionSource
+ ) {
+ final IBinder token = mTransitions.startTransition(getEnterTransitionType(transitionSource),
+ wct, this);
mPendingTransitionTokens.add(token);
return token;
}
@@ -117,7 +124,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition
private boolean startChangeTransition(
@NonNull IBinder transition,
- @WindowManager.TransitionType int type,
+ @TransitionType int type,
@NonNull TransitionInfo.Change change,
@NonNull SurfaceControl.Transaction startT,
@NonNull SurfaceControl.Transaction finishT,
@@ -127,7 +134,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition
}
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
- if (type == TRANSIT_MOVE_TO_DESKTOP
+ if (isEnterDesktopModeTransition(type)
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
return animateMoveToDesktop(change, startT, finishCallback);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
index 9f9e256fc2b7..891f75cfdbda 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java
@@ -18,6 +18,9 @@ package com.android.wm.shell.desktopmode;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getExitTransitionType;
+import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isExitDesktopModeTransition;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
@@ -30,6 +33,7 @@ import android.os.IBinder;
import android.util.DisplayMetrics;
import android.view.SurfaceControl;
import android.view.WindowManager;
+import android.view.WindowManager.TransitionType;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
@@ -38,6 +42,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
import com.android.wm.shell.transition.Transitions;
import java.util.ArrayList;
@@ -52,6 +57,7 @@ import java.util.function.Supplier;
*/
public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionHandler {
private static final int FULLSCREEN_ANIMATION_DURATION = 336;
+
private final Context mContext;
private final Transitions mTransitions;
private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
@@ -77,17 +83,18 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH
/**
* Starts Transition of a given type
*
- * @param type Transition type
+ * @param transitionSource DesktopModeTransitionSource for transition
* @param wct WindowContainerTransaction for transition
* @param position Position of the task when transition is started
* @param onAnimationEndCallback to be called after animation
*/
- public void startTransition(@WindowManager.TransitionType int type,
+ public void startTransition(@NonNull DesktopModeTransitionSource transitionSource,
@NonNull WindowContainerTransaction wct, Point position,
Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
mPosition = position;
mOnAnimationFinishedCallback = onAnimationEndCallback;
- final IBinder token = mTransitions.startTransition(type, wct, this);
+ final IBinder token = mTransitions.startTransition(getExitTransitionType(transitionSource),
+ wct, this);
mPendingTransitionTokens.add(token);
}
@@ -121,7 +128,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH
@VisibleForTesting
boolean startChangeTransition(
@NonNull IBinder transition,
- @WindowManager.TransitionType int type,
+ @TransitionType int type,
@NonNull TransitionInfo.Change change,
@NonNull SurfaceControl.Transaction startT,
@NonNull SurfaceControl.Transaction finishT,
@@ -130,7 +137,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH
return false;
}
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
- if (type == Transitions.TRANSIT_EXIT_DESKTOP_MODE
+ if (isExitDesktopModeTransition(type)
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
// This Transition animates a task to fullscreen after being dragged to status bar
final Resources resources = mContext.getResources();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
index c36f8deb6ecc..a7ec2037706d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl
@@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode;
import android.app.ActivityManager.RunningTaskInfo;
import android.window.RemoteTransition;
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
import com.android.wm.shell.desktopmode.IDesktopTaskListener;
/**
@@ -47,5 +48,5 @@ interface IDesktopMode {
oneway void setTaskListener(IDesktopTaskListener listener);
/** Move a task with given `taskId` to desktop */
- void moveToDesktop(int taskId);
+ void moveToDesktop(int taskId, in DesktopModeTransitionSource transitionSource);
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS
index 1385f42bc676..7ad68aac62c5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS
@@ -5,3 +5,4 @@ madym@google.com
nmusgrave@google.com
pbdr@google.com
tkachenkoi@google.com
+vaniadesmonda@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
index 88d0554669b7..5335c0b69a24 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt
@@ -27,6 +27,8 @@ import android.window.TransitionInfo
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.core.animation.addListener
+import com.android.internal.jank.Cuj
+import com.android.wm.shell.common.InteractionJankMonitorUtils
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
@@ -103,6 +105,8 @@ class ToggleResizeDesktopTaskTransitionHandler(
onTaskResizeAnimationListener.onAnimationEnd(taskId)
finishCallback.onTransitionFinished(null)
boundsAnimator = null
+ InteractionJankMonitorUtils.endTracing(
+ Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW)
}
)
addUpdateListener { anim ->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
index 9aa5f4ffcd78..0acc7df98d1c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
@@ -54,8 +54,8 @@ Specifically, to support calling into a controller from an external process (lik
extend `ExternalInterfaceBinder` and implement `invalidate()` to ensure it doesn't hold long
references to the outer controller
- Make the controller implement `RemoteCallable<T>`, and have all incoming calls use one of
- the `ExecutorUtils.executeRemoteCallWithTaskPermission()` calls to verify the caller's identity
- and ensure the call happens on the main shell thread and not the binder thread
+ the `executeRemoteCallWithTaskPermission()` calls to verify the caller's identity and ensure the
+ call happens on the main shell thread and not the binder thread
- Inject `ShellController` and add the instance of the implementation as external interface
- In Launcher, update `TouchInteractionService` to pass the interface to `SystemUIProxy`, and then
call the SystemUIProxy method as needed in that code
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
index 438aa768165e..b1cbe8d98397 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
@@ -73,7 +73,7 @@ stack traces when specific surface transaction calls are made, which is possible
following system properties for example:
```shell
# Enabling
-adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha # matches the name of the SurfaceControlTransaction method
+adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,setPosition # matches the name of the SurfaceControlTransaction methods
adb shell setprop persist.wm.debug.sc.tx.log_match_name com.android.systemui # matches the name of the surface
adb reboot
adb logcat -s "SurfaceControlRegistry"
@@ -87,6 +87,16 @@ adb reboot
It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite
noisy if unfiltered.
+It can sometimes be useful to trace specific logs and when they are applied (sometimes we build
+transactions that can be applied later). You can do this by adding the "merge" and "apply" calls to
+the set of requested calls:
+```shell
+# Enabling
+adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,merge,apply # apply will dump logs of each setAlpha or merge call on that tx
+adb reboot
+adb logcat -s "SurfaceControlRegistry"
+```
+
## Tracing activity starts in the app process
It's sometimes useful to know when to see a stack trace of when an activity starts in the app code
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index 7e70d6a3debe..c374eb8e8f03 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
@@ -32,7 +32,6 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMA
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
-import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP;
import android.app.ActivityManager;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index 7d2aa275a684..b48aee5ccd5e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -150,6 +150,10 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"Adding active freeform task: #%d", taskInfo.taskId);
}
+ } else if (repository.isClosingTask(taskInfo.taskId)
+ && repository.removeClosingTask(taskInfo.taskId)) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "Removing closing freeform task: #%d", taskInfo.taskId);
}
repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId,
taskInfo.isVisible);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
index 39b9000856f2..962309f7c534 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
@@ -19,7 +19,6 @@ package com.android.wm.shell.onehanded;
import static android.os.UserHandle.myUserId;
import static android.view.Display.DEFAULT_DISPLAY;
-import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.onehanded.OneHandedState.STATE_ACTIVE;
import static com.android.wm.shell.onehanded.OneHandedState.STATE_ENTERING;
import static com.android.wm.shell.onehanded.OneHandedState.STATE_EXITING;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index eb845db409e3..0a3c15b6057f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -40,6 +40,7 @@ import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.animation.Interpolators;
+import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.transition.Transitions;
@@ -583,7 +584,7 @@ public class PipAnimationController {
}
static PipTransitionAnimator<Rect> ofBounds(TaskInfo taskInfo, SurfaceControl leash,
- Rect baseValue, Rect startValue, Rect endValue, Rect sourceHintRect,
+ Rect baseValue, Rect startValue, Rect endValue, Rect sourceRectHint,
@PipAnimationController.TransitionDirection int direction, float startingAngle,
@Surface.Rotation int rotationDelta) {
final boolean isOutPipDirection = isOutPipDirection(direction);
@@ -613,14 +614,25 @@ public class PipAnimationController {
initialContainerRect = initialSourceValue;
}
- final Rect sourceHintRectInsets;
- if (sourceHintRect == null) {
- sourceHintRectInsets = null;
+ final Rect adjustedSourceRectHint = new Rect();
+ if (sourceRectHint == null || sourceRectHint.isEmpty()) {
+ // Crop a Rect matches the aspect ratio and pivots at the center point.
+ // This is done for entering case only.
+ if (isInPipDirection(direction)) {
+ final float aspectRatio = endValue.width() / (float) endValue.height();
+ adjustedSourceRectHint.set(PipUtils.getEnterPipWithOverlaySrcRectHint(
+ startValue, aspectRatio));
+ }
} else {
- sourceHintRectInsets = new Rect(sourceHintRect.left - initialContainerRect.left,
- sourceHintRect.top - initialContainerRect.top,
- initialContainerRect.right - sourceHintRect.right,
- initialContainerRect.bottom - sourceHintRect.bottom);
+ adjustedSourceRectHint.set(sourceRectHint);
+ }
+ final Rect sourceHintRectInsets = new Rect();
+ if (!adjustedSourceRectHint.isEmpty()) {
+ sourceHintRectInsets.set(
+ adjustedSourceRectHint.left - initialContainerRect.left,
+ adjustedSourceRectHint.top - initialContainerRect.top,
+ initialContainerRect.right - adjustedSourceRectHint.right,
+ initialContainerRect.bottom - adjustedSourceRectHint.bottom);
}
final Rect zeroInsets = new Rect(0, 0, 0, 0);
@@ -648,7 +660,7 @@ public class PipAnimationController {
}
float angle = (1.0f - fraction) * startingAngle;
setCurrentValue(bounds);
- if (inScaleTransition() || sourceHintRect == null) {
+ if (inScaleTransition() || adjustedSourceRectHint.isEmpty()) {
if (isOutPipDirection) {
getSurfaceTransactionHelper().crop(tx, leash, end)
.scale(tx, leash, end, bounds);
@@ -661,7 +673,7 @@ public class PipAnimationController {
} else {
final Rect insets = computeInsets(fraction);
getSurfaceTransactionHelper().scaleAndCrop(tx, leash,
- sourceHintRect, initialSourceValue, bounds, insets,
+ adjustedSourceRectHint, initialSourceValue, bounds, insets,
isInPipDirection, fraction);
if (shouldApplyCornerRadius()) {
final Rect sourceBounds = new Rect(initialContainerRect);
@@ -729,9 +741,6 @@ public class PipAnimationController {
}
private Rect computeInsets(float fraction) {
- if (sourceHintRectInsets == null) {
- return zeroInsets;
- }
final Rect startRect = isOutPipDirection ? sourceHintRectInsets : zeroInsets;
final Rect endRect = isOutPipDirection ? zeroInsets : sourceHintRectInsets;
return mInsetsEvaluator.evaluate(fraction, startRect, endRect);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
index e11e8596a7fe..ff2d46e11107 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipContentOverlay.java
@@ -226,11 +226,10 @@ public abstract class PipContentOverlay {
appBoundsCenterX - mOverlayHalfSize,
appBoundsCenterY - mOverlayHalfSize);
// Scale back the bitmap with the pivot point at center.
- mTmpTransform.postScale(
+ final float scale = Math.min(
(float) mAppBounds.width() / currentBounds.width(),
- (float) mAppBounds.height() / currentBounds.height(),
- appBoundsCenterX,
- appBoundsCenterY);
+ (float) mAppBounds.height() / currentBounds.height());
+ mTmpTransform.postScale(scale, scale, appBoundsCenterX, appBoundsCenterY);
atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9)
.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
index a58d94ecd19b..3d1994cac534 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -152,8 +152,14 @@ public class PipSurfaceTransactionHelper {
scale = Math.max((float) destinationBounds.width() / sourceBounds.width(),
(float) destinationBounds.height() / sourceBounds.height());
}
- final float left = destinationBounds.left - insets.left * scale;
- final float top = destinationBounds.top - insets.top * scale;
+ float left = destinationBounds.left - insets.left * scale;
+ float top = destinationBounds.top - insets.top * scale;
+ if (scale == 1) {
+ // Work around the 1 pixel off error by rounding the position down at very beginning.
+ // We noticed such error from flicker tests, not visually.
+ left = sourceBounds.left;
+ top = sourceBounds.top;
+ }
mTmpTransform.setScale(scale, scale);
tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
.setCrop(leash, mTmpDestinationRect)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index e1657f99639d..e2e1ecde8b56 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -63,7 +63,6 @@ import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.SystemProperties;
-import android.util.Rational;
import android.view.Choreographer;
import android.view.Display;
import android.view.Surface;
@@ -128,8 +127,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
SystemProperties.getInt(
"persist.wm.debug.extra_content_overlay_fade_out_delay_ms", 400);
- private static final float PIP_ASPECT_RATIO_MISMATCH_THRESHOLD = 0.005f;
-
private final Context mContext;
private final SyncTransactionQueue mSyncTransactionQueue;
private final PipBoundsState mPipBoundsState;
@@ -373,6 +370,10 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
@NonNull
final Rect mAppBounds = new Rect();
+ /** The source rect hint from stopSwipePipToHome(). */
+ @Nullable
+ private Rect mSwipeSourceRectHint;
+
public PipTaskOrganizer(Context context,
@NonNull SyncTransactionQueue syncTransactionQueue,
@NonNull PipTransitionState pipTransitionState,
@@ -504,7 +505,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
* Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards.
*/
public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds,
- SurfaceControl overlay, Rect appBounds) {
+ SurfaceControl overlay, Rect appBounds, Rect sourceRectHint) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"stopSwipePipToHome: %s, stat=%s", componentName, mPipTransitionState);
// do nothing if there is no startSwipePipToHome being called before
@@ -513,6 +514,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
mPipBoundsState.setBounds(destinationBounds);
setContentOverlay(overlay, appBounds);
+ mSwipeSourceRectHint = sourceRectHint;
if (ENABLE_SHELL_TRANSITIONS && overlay != null) {
// With Shell transition, the overlay was attached to the remote transition leash, which
// will be removed when the current transition is finished, so we need to reparent it
@@ -529,6 +531,20 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
}
+ /**
+ * Returns non-null Rect if the pip is entering from swipe-to-home with a specified source hint.
+ * This also consumes the rect hint.
+ */
+ @Nullable
+ Rect takeSwipeSourceRectHint() {
+ final Rect sourceRectHint = mSwipeSourceRectHint;
+ if (sourceRectHint == null || sourceRectHint.isEmpty()) {
+ return null;
+ }
+ mSwipeSourceRectHint = null;
+ return mPipTransitionState.getInSwipePipToHomeTransition() ? sourceRectHint : null;
+ }
+
private void mayRemoveContentOverlay(SurfaceControl overlay) {
final WeakReference<SurfaceControl> overlayRef = new WeakReference<>(overlay);
final long timeoutDuration = (mEnterAnimationDuration
@@ -589,6 +605,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
public void exitPip(int animationDurationMs, boolean requestEnterSplit) {
if (!mPipTransitionState.isInPip()
|| mPipTransitionState.getTransitionState() == PipTransitionState.EXITING_PIP
+ || mPipTransitionState.getInSwipePipToHomeTransition()
|| mToken == null) {
ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Not allowed to exitPip in current state"
@@ -603,6 +620,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
// the end of the enter animation and reschedule exitPip to run after enter-PiP
// has finished its transition and allowed the client to draw in PiP mode.
mPipTransitionController.end(() -> {
+ // TODO(341627042): force set to entered state to avoid potential stack overflow.
+ mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP);
exitPip(animationDurationMs, requestEnterSplit);
});
return;
@@ -800,37 +819,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mPictureInPictureParams.getTitle());
mPipParamsChangedForwarder.notifySubtitleChanged(
mPictureInPictureParams.getSubtitle());
-
- if (mPictureInPictureParams.hasSourceBoundsHint()
- && mPictureInPictureParams.hasSetAspectRatio()) {
- Rational sourceRectHintAspectRatio = new Rational(
- mPictureInPictureParams.getSourceRectHint().width(),
- mPictureInPictureParams.getSourceRectHint().height());
- if (sourceRectHintAspectRatio.compareTo(
- mPictureInPictureParams.getAspectRatio()) != 0) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "Aspect ratio of source rect hint (%d/%d) does not match the provided "
- + "aspect ratio value (%d/%d). Consider matching them for "
- + "improved animation. Future releases might override the "
- + "value to match.",
- mPictureInPictureParams.getSourceRectHint().width(),
- mPictureInPictureParams.getSourceRectHint().height(),
- mPictureInPictureParams.getAspectRatio().getNumerator(),
- mPictureInPictureParams.getAspectRatio().getDenominator());
- }
- if (Math.abs(sourceRectHintAspectRatio.floatValue()
- - mPictureInPictureParams.getAspectRatioFloat())
- > PIP_ASPECT_RATIO_MISMATCH_THRESHOLD) {
- ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "Aspect ratio of source rect hint (%f) does not match the provided "
- + "aspect ratio value (%f) and is above threshold of %f. "
- + "Consider matching them for improved animation. Future "
- + "releases might override the value to match.",
- sourceRectHintAspectRatio.floatValue(),
- mPictureInPictureParams.getAspectRatioFloat(),
- PIP_ASPECT_RATIO_MISMATCH_THRESHOLD);
- }
- }
}
mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo);
@@ -978,7 +966,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
private void onEndOfSwipePipToHomeTransition() {
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- mPipTransitionController.setEnterAnimationType(ANIM_TYPE_BOUNDS);
return;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index b52b0d8dee74..3cae72d89ecc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -200,9 +200,6 @@ public class PipTransition extends PipTransitionController {
animator.cancel();
}
mExitTransition = mTransitions.startTransition(type, out, this);
- if (mPipOrganizer.getOutPipWindowingMode() == WINDOWING_MODE_UNDEFINED) {
- mHomeTransitionObserver.notifyHomeVisibilityChanged(false /* isVisible */);
- }
}
@Override
@@ -659,6 +656,9 @@ public class PipTransition extends PipTransitionController {
startTransaction.remove(mPipOrganizer.mPipOverlay);
mPipOrganizer.clearContentOverlay();
}
+ if (mPipOrganizer.getOutPipWindowingMode() == WINDOWING_MODE_UNDEFINED) {
+ mHomeTransitionObserver.notifyHomeVisibilityChanged(false /* isVisible */);
+ }
if (pipChange == null) {
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: No window of exiting PIP is found. Can't play expand animation", TAG);
@@ -1004,8 +1004,11 @@ public class PipTransition extends PipTransitionController {
final Rect currentBounds = pipChange.getStartAbsBounds();
int rotationDelta = deltaRotation(startRotation, endRotation);
- Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect(
- taskInfo.pictureInPictureParams, currentBounds, destinationBounds);
+ Rect sourceHintRect = mPipOrganizer.takeSwipeSourceRectHint();
+ if (sourceHintRect == null) {
+ sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect(
+ taskInfo.pictureInPictureParams, currentBounds, destinationBounds);
+ }
if (rotationDelta != Surface.ROTATION_0
&& endRotation != mPipDisplayLayoutState.getRotation()) {
// Computes the destination bounds in new rotation.
@@ -1080,6 +1083,8 @@ public class PipTransition extends PipTransitionController {
mSurfaceTransactionHelper
.crop(finishTransaction, leash, destinationBounds)
.round(finishTransaction, leash, true /* applyCornerRadius */);
+ // Always reset to bounds animation type afterwards.
+ setEnterAnimationType(ANIM_TYPE_BOUNDS);
} else {
throw new RuntimeException("Unrecognized animation type: " + enterAnimationType);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index 1d1a4e2be3e4..6eefdcfc4d93 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -311,6 +311,14 @@ public abstract class PipTransitionController implements Transitions.TransitionH
}
/**
+ * Finish the current transition if possible.
+ *
+ * @param tx transaction to be applied with a potentially new draw after finishing.
+ */
+ public void finishTransition(@Nullable SurfaceControl.Transaction tx) {
+ }
+
+ /**
* End the currently-playing PiP animation.
*
* @param onTransitionEnd callback to run upon finishing the playing transition.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 85f9194ac804..8c4bf7620068 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -22,7 +22,6 @@ import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
import static android.view.WindowManager.INPUT_CONSUMER_PIP;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION;
-import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND;
import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
@@ -1002,9 +1001,9 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
private void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds,
- SurfaceControl overlay, Rect appBounds) {
+ SurfaceControl overlay, Rect appBounds, Rect sourceRectHint) {
mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay,
- appBounds);
+ appBounds, sourceRectHint);
}
private void abortSwipePipToHome(int taskId, ComponentName componentName) {
@@ -1292,13 +1291,15 @@ public class PipController implements PipTransitionController.PipTransitionCallb
@Override
public void stopSwipePipToHome(int taskId, ComponentName componentName,
- Rect destinationBounds, SurfaceControl overlay, Rect appBounds) {
+ Rect destinationBounds, SurfaceControl overlay, Rect appBounds,
+ Rect sourceRectHint) {
if (overlay != null) {
overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome");
}
executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome",
(controller) -> controller.stopSwipePipToHome(
- taskId, componentName, destinationBounds, overlay, appBounds));
+ taskId, componentName, destinationBounds, overlay, appBounds,
+ sourceRectHint));
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java
new file mode 100644
index 000000000000..895c2aeba9ef
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java
@@ -0,0 +1,117 @@
+/*
+ * 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.wm.shell.pip2.animation;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.content.Context;
+import android.view.SurfaceControl;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Animator that handles the alpha animation for entering PIP
+ */
+public class PipAlphaAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener,
+ ValueAnimator.AnimatorListener {
+ @IntDef(prefix = {"FADE_"}, value = {
+ FADE_IN,
+ FADE_OUT
+ })
+
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Fade {}
+
+ public static final int FADE_IN = 0;
+ public static final int FADE_OUT = 1;
+
+ private final int mEnterAnimationDuration;
+ private final SurfaceControl mLeash;
+ private final SurfaceControl.Transaction mStartTransaction;
+
+ // optional callbacks for tracking animation start and end
+ @Nullable private Runnable mAnimationStartCallback;
+ @Nullable private Runnable mAnimationEndCallback;
+
+ private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+ mSurfaceControlTransactionFactory;
+
+ public PipAlphaAnimator(Context context,
+ SurfaceControl leash,
+ SurfaceControl.Transaction tx,
+ @Fade int direction) {
+ mLeash = leash;
+ mStartTransaction = tx;
+ if (direction == FADE_IN) {
+ setFloatValues(0f, 1f);
+ } else { // direction == FADE_OUT
+ setFloatValues(1f, 0f);
+ }
+ mSurfaceControlTransactionFactory =
+ new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
+ mEnterAnimationDuration = context.getResources()
+ .getInteger(R.integer.config_pipEnterAnimationDuration);
+ setDuration(mEnterAnimationDuration);
+ addListener(this);
+ addUpdateListener(this);
+ }
+
+ public void setAnimationStartCallback(@NonNull Runnable runnable) {
+ mAnimationStartCallback = runnable;
+ }
+
+ public void setAnimationEndCallback(@NonNull Runnable runnable) {
+ mAnimationEndCallback = runnable;
+ }
+
+ @Override
+ public void onAnimationStart(@NonNull Animator animation) {
+ if (mAnimationStartCallback != null) {
+ mAnimationStartCallback.run();
+ }
+ if (mStartTransaction != null) {
+ mStartTransaction.apply();
+ }
+ }
+
+ @Override
+ public void onAnimationUpdate(@NonNull ValueAnimator animation) {
+ final float alpha = (Float) animation.getAnimatedValue();
+ mSurfaceControlTransactionFactory.getTransaction().setAlpha(mLeash, alpha).apply();
+ }
+
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation) {
+ if (mAnimationEndCallback != null) {
+ mAnimationEndCallback.run();
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(@NonNull Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(@NonNull Animator animation) {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java
new file mode 100644
index 000000000000..5c561fed89c7
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java
@@ -0,0 +1,154 @@
+/*
+ * 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.wm.shell.pip2.animation;
+
+import android.animation.Animator;
+import android.animation.RectEvaluator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
+
+/**
+ * Animator that handles any resize related animation for PIP.
+ */
+public class PipResizeAnimator extends ValueAnimator
+ implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener{
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final SurfaceControl mLeash;
+ @Nullable
+ private SurfaceControl.Transaction mStartTx;
+ @Nullable
+ private SurfaceControl.Transaction mFinishTx;
+ @Nullable
+ private Runnable mAnimationStartCallback;
+ @Nullable
+ private Runnable mAnimationEndCallback;
+ private RectEvaluator mRectEvaluator;
+ private final Rect mBaseBounds = new Rect();
+ private final Rect mStartBounds = new Rect();
+ private final Rect mEndBounds = new Rect();
+ private final Rect mAnimatedRect = new Rect();
+ private final float mDelta;
+
+ private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+ mSurfaceControlTransactionFactory;
+
+ public PipResizeAnimator(@NonNull Context context,
+ @NonNull SurfaceControl leash,
+ @Nullable SurfaceControl.Transaction startTransaction,
+ @Nullable SurfaceControl.Transaction finishTransaction,
+ @NonNull Rect baseBounds,
+ @NonNull Rect startBounds,
+ @NonNull Rect endBounds,
+ int duration,
+ float delta) {
+ mContext = context;
+ mLeash = leash;
+ mStartTx = startTransaction;
+ mFinishTx = finishTransaction;
+ mSurfaceControlTransactionFactory =
+ new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
+
+ mBaseBounds.set(baseBounds);
+ mStartBounds.set(startBounds);
+ mAnimatedRect.set(startBounds);
+ mEndBounds.set(endBounds);
+ mDelta = delta;
+
+ mRectEvaluator = new RectEvaluator(mAnimatedRect);
+
+ setObjectValues(startBounds, endBounds);
+ addListener(this);
+ addUpdateListener(this);
+ setEvaluator(mRectEvaluator);
+ // TODO: change this
+ setDuration(duration);
+ }
+
+ public void setAnimationStartCallback(@NonNull Runnable runnable) {
+ mAnimationStartCallback = runnable;
+ }
+
+ public void setAnimationEndCallback(@NonNull Runnable runnable) {
+ mAnimationEndCallback = runnable;
+ }
+
+ @Override
+ public void onAnimationStart(@NonNull Animator animation) {
+ if (mAnimationStartCallback != null) {
+ mAnimationStartCallback.run();
+ }
+ if (mStartTx != null) {
+ setBoundsAndRotation(mStartTx, mLeash, mBaseBounds, mStartBounds, mDelta);
+ mStartTx.apply();
+ }
+ }
+
+ @Override
+ public void onAnimationUpdate(@NonNull ValueAnimator animation) {
+ final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
+ final float fraction = getAnimatedFraction();
+ final float degrees = (1.0f - fraction) * mDelta;
+ setBoundsAndRotation(tx, mLeash, mBaseBounds, mAnimatedRect, degrees);
+ tx.apply();
+ }
+
+ /**
+ * Set a proper transform matrix for a leash to move it to given bounds with a certain rotation.
+ *
+ * @param baseBounds crop/buffer size relative to which we are scaling the leash.
+ * @param targetBounds bounds to which we are scaling the leash.
+ * @param degrees degrees of rotation - counter-clockwise is positive by convention.
+ */
+ public static void setBoundsAndRotation(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect baseBounds, Rect targetBounds, float degrees) {
+ Matrix transformTensor = new Matrix();
+ final float[] mMatrixTmp = new float[9];
+ final float scale = (float) targetBounds.width() / baseBounds.width();
+
+ transformTensor.setScale(scale, scale);
+ transformTensor.postTranslate(targetBounds.left, targetBounds.top);
+ transformTensor.postRotate(degrees, targetBounds.centerX(), targetBounds.centerY());
+
+ tx.setMatrix(leash, transformTensor, mMatrixTmp);
+ }
+
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation) {
+ if (mFinishTx != null) {
+ setBoundsAndRotation(mFinishTx, mLeash, mBaseBounds, mEndBounds, 0f);
+ }
+ if (mAnimationEndCallback != null) {
+ mAnimationEndCallback.run();
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(@NonNull Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(@NonNull Animator animation) {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index f5afeea3eaef..fc0d36d13b2e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -19,7 +19,6 @@ package com.android.wm.shell.pip2.phone;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
-import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP;
import android.app.ActivityManager;
@@ -286,7 +285,8 @@ public class PipController implements ConfigurationChangeListener,
}
private void onSwipePipToHomeAnimationStart(int taskId, ComponentName componentName,
- Rect destinationBounds, SurfaceControl overlay, Rect appBounds) {
+ Rect destinationBounds, SurfaceControl overlay, Rect appBounds,
+ Rect sourceRectHint) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"onSwipePipToHomeAnimationStart: %s", componentName);
Bundle extra = new Bundle();
@@ -391,6 +391,7 @@ public class PipController implements ConfigurationChangeListener,
@Override
public void invalidate() {
mController = null;
+ // Unregister the listener to ensure any registered binder death recipients are unlinked
mListener.unregister();
}
@@ -409,13 +410,15 @@ public class PipController implements ConfigurationChangeListener,
@Override
public void stopSwipePipToHome(int taskId, ComponentName componentName,
- Rect destinationBounds, SurfaceControl overlay, Rect appBounds) {
+ Rect destinationBounds, SurfaceControl overlay, Rect appBounds,
+ Rect sourceRectHint) {
if (overlay != null) {
overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome");
}
executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome",
(controller) -> controller.onSwipePipToHomeAnimationStart(
- taskId, componentName, destinationBounds, overlay, appBounds));
+ taskId, componentName, destinationBounds, overlay, appBounds,
+ sourceRectHint));
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java
index aed493f2bc8f..495cd0075494 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java
@@ -731,8 +731,8 @@ public class PipMotionHelper implements PipAppOpsListener.Callback,
settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */);
cleanUpHighPerfSessionMaybe();
- // Setting state to CHANGED_PIP_BOUNDS applies finishTx and notifies Core.
- mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS);
+ // Signal that the transition is done - should update transition state by default.
+ mPipScheduler.scheduleFinishResizePip(false /* configAtEnd */);
break;
case PipTransitionState.EXITING_PIP:
// We need to force finish any local animators if about to leave PiP, to avoid
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java
index 7dffe543ec9c..33e80bd80988 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java
@@ -38,6 +38,7 @@ import android.view.ViewConfiguration;
import androidx.annotation.VisibleForTesting;
+import com.android.internal.util.Preconditions;
import com.android.wm.shell.R;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
@@ -45,6 +46,7 @@ import com.android.wm.shell.common.pip.PipBoundsState;
import com.android.wm.shell.common.pip.PipPerfHintController;
import com.android.wm.shell.common.pip.PipPinchResizingAlgorithm;
import com.android.wm.shell.common.pip.PipUiEventLogger;
+import com.android.wm.shell.pip2.animation.PipResizeAnimator;
import java.io.PrintWriter;
import java.util.function.Consumer;
@@ -82,6 +84,7 @@ public class PipResizeGestureHandler implements
private final Rect mLastResizeBounds = new Rect();
private final Rect mUserResizeBounds = new Rect();
private final Rect mDownBounds = new Rect();
+ private final Rect mStartBoundsAfterRelease = new Rect();
private final Runnable mUpdateMovementBoundsRunnable;
private final Consumer<Rect> mUpdateResizeBoundsCallback;
@@ -418,7 +421,9 @@ public class PipResizeGestureHandler implements
if (!mOngoingPinchToResize) {
return;
}
- final Rect startBounds = new Rect(mLastResizeBounds);
+
+ // Cache initial bounds after release for animation before mLastResizeBounds are modified.
+ mStartBoundsAfterRelease.set(mLastResizeBounds);
// If user resize is pretty close to max size, just auto resize to max.
if (mLastResizeBounds.width() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.x
@@ -527,28 +532,39 @@ public class PipResizeGestureHandler implements
int offsetY = inTopHalf ? 1 : -1;
mLastResizeBounds.offset(0 /* dx */, offsetY);
}
-
mWaitingForBoundsChangeTransition = true;
- mPipScheduler.scheduleAnimateResizePip(mLastResizeBounds);
+
+ // Schedule PiP resize transition, but delay any config updates until very end.
+ mPipScheduler.scheduleAnimateResizePip(mLastResizeBounds, true /* configAtEnd */);
break;
case PipTransitionState.CHANGING_PIP_BOUNDS:
if (!mWaitingForBoundsChangeTransition) break;
-
- // If bounds change transition was scheduled from this class, handle leash updates.
+ // If resize transition was scheduled from this component, handle leash updates.
mWaitingForBoundsChangeTransition = false;
+ SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
+ Preconditions.checkState(pipLeash != null,
+ "No leash cached by mPipTransitionState=" + mPipTransitionState);
+
SurfaceControl.Transaction startTx = extra.getParcelable(
PipTransition.PIP_START_TX, SurfaceControl.Transaction.class);
- Rect destinationBounds = extra.getParcelable(
- PipTransition.PIP_DESTINATION_BOUNDS, Rect.class);
- startTx.apply();
-
- // All motion operations have actually finished, so make bounds cache updates.
- mUpdateResizeBoundsCallback.accept(destinationBounds);
- cleanUpHighPerfSessionMaybe();
-
- // Setting state to CHANGED_PIP_BOUNDS applies finishTx and notifies Core.
- mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS);
+ SurfaceControl.Transaction finishTx = extra.getParcelable(
+ PipTransition.PIP_FINISH_TX, SurfaceControl.Transaction.class);
+ startTx.setWindowCrop(pipLeash, mPipBoundsState.getBounds().width(),
+ mPipBoundsState.getBounds().height());
+
+ PipResizeAnimator animator = new PipResizeAnimator(mContext, pipLeash,
+ startTx, finishTx, mPipBoundsState.getBounds(), mStartBoundsAfterRelease,
+ mLastResizeBounds, PINCH_RESIZE_SNAP_DURATION, mAngle);
+ animator.setAnimationEndCallback(() -> {
+ // All motion operations have actually finished, so make bounds cache updates.
+ mUpdateResizeBoundsCallback.accept(mLastResizeBounds);
+ cleanUpHighPerfSessionMaybe();
+
+ // Signal that we are done with resize transition
+ mPipScheduler.scheduleFinishResizePip(true /* configAtEnd */);
+ });
+ animator.start();
break;
}
}
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 49475077211f..9c1e321a1273 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
@@ -153,15 +153,46 @@ public class PipScheduler {
* Animates resizing of the pinned stack given the duration.
*/
public void scheduleAnimateResizePip(Rect toBounds) {
+ scheduleAnimateResizePip(toBounds, false /* configAtEnd */);
+ }
+
+ /**
+ * Animates resizing of the pinned stack given the duration.
+ *
+ * @param configAtEnd true if we are delaying config updates until the transition ends.
+ */
+ public void scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd) {
if (mPipTransitionState.mPipTaskToken == null || !mPipTransitionState.isInPip()) {
return;
}
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setBounds(mPipTransitionState.mPipTaskToken, toBounds);
+ if (configAtEnd) {
+ wct.deferConfigToTransitionEnd(mPipTransitionState.mPipTaskToken);
+ }
mPipTransitionController.startResizeTransition(wct);
}
/**
+ * Signals to Core to finish the PiP resize transition.
+ * Note that we do not allow any actual WM Core changes at this point.
+ *
+ * @param configAtEnd true if we are waiting for config updates at the end of the transition.
+ */
+ public void scheduleFinishResizePip(boolean configAtEnd) {
+ SurfaceControl.Transaction tx = null;
+ if (configAtEnd) {
+ tx = new SurfaceControl.Transaction();
+ tx.addTransactionCommittedListener(mMainExecutor, () -> {
+ mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS);
+ });
+ } else {
+ mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS);
+ }
+ mPipTransitionController.finishTransition(tx);
+ }
+
+ /**
* Directly perform a scaled matrix transformation on the leash. This will not perform any
* {@link WindowContainerTransaction}.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
index 7dddd2748f83..57dc5f92b2b6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java
@@ -52,6 +52,7 @@ import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.pip.PipContentOverlay;
import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
@@ -292,37 +293,32 @@ public class PipTransition extends PipTransitionController implements
return false;
}
+ SurfaceControl overlayLeash = mPipTransitionState.getSwipePipToHomeOverlay();
PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams;
- Rect srcRectHint = params.getSourceRectHint();
- Rect startBounds = pipChange.getStartAbsBounds();
+
+ Rect appBounds = mPipTransitionState.getSwipePipToHomeAppBounds();
Rect destinationBounds = pipChange.getEndAbsBounds();
+ float aspectRatio = pipChange.getTaskInfo().pictureInPictureParams.getAspectRatioFloat();
+
+ // We fake the source rect hint when the one prvided by the app is invalid for
+ // the animation with an app icon overlay.
+ Rect animationSrcRectHint = overlayLeash == null ? params.getSourceRectHint()
+ : PipUtils.getEnterPipWithOverlaySrcRectHint(appBounds, aspectRatio);
+
WindowContainerTransaction finishWct = new WindowContainerTransaction();
SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
- if (PipBoundsAlgorithm.isSourceRectHintValidForEnterPip(srcRectHint, destinationBounds)) {
- final float scale = (float) destinationBounds.width() / srcRectHint.width();
- startTransaction.setWindowCrop(pipLeash, srcRectHint);
- startTransaction.setPosition(pipLeash,
- destinationBounds.left - srcRectHint.left * scale,
- destinationBounds.top - srcRectHint.top * scale);
-
- // Reset the scale in case we are in the multi-activity case.
- // TO_FRONT transition already scales down the task in single-activity case, but
- // in multi-activity case, reparenting yields new reset scales coming from pinned task.
- startTransaction.setScale(pipLeash, scale, scale);
- } else {
- final float scaleX = (float) destinationBounds.width() / startBounds.width();
- final float scaleY = (float) destinationBounds.height() / startBounds.height();
+ final float scale = (float) destinationBounds.width() / animationSrcRectHint.width();
+ startTransaction.setWindowCrop(pipLeash, animationSrcRectHint);
+ startTransaction.setPosition(pipLeash,
+ destinationBounds.left - animationSrcRectHint.left * scale,
+ destinationBounds.top - animationSrcRectHint.top * scale);
+ startTransaction.setScale(pipLeash, scale, scale);
+
+ if (overlayLeash != null) {
final int overlaySize = PipContentOverlay.PipAppIconOverlay.getOverlaySize(
mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds);
- SurfaceControl overlayLeash = mPipTransitionState.getSwipePipToHomeOverlay();
-
- startTransaction.setPosition(pipLeash, destinationBounds.left, destinationBounds.top)
- .setScale(pipLeash, scaleX, scaleY)
- .setWindowCrop(pipLeash, startBounds)
- .reparent(overlayLeash, pipLeash)
- .setLayer(overlayLeash, Integer.MAX_VALUE);
// Overlay needs to be adjusted once a new draw comes in resetting surface transform.
tx.setScale(overlayLeash, 1f, 1f);
@@ -389,11 +385,25 @@ public class PipTransition extends PipTransitionController implements
if (pipChange == null) {
return false;
}
- // cache the PiP task token and leash
- WindowContainerToken pipTaskToken = pipChange.getContainer();
- startTransaction.apply();
- finishCallback.onTransitionFinished(null);
+ Rect destinationBounds = pipChange.getEndAbsBounds();
+ SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash;
+ Preconditions.checkNotNull(pipLeash, "Leash is null for alpha transition.");
+
+ // Start transition with 0 alpha at the entry bounds.
+ startTransaction.setPosition(pipLeash, destinationBounds.left, destinationBounds.top)
+ .setWindowCrop(pipLeash, destinationBounds.width(), destinationBounds.height())
+ .setAlpha(pipLeash, 0f);
+
+ PipAlphaAnimator animator = new PipAlphaAnimator(mContext, pipLeash, startTransaction,
+ PipAlphaAnimator.FADE_IN);
+ animator.setAnimationEndCallback(() -> {
+ finishCallback.onTransitionFinished(null);
+ // This should update the pip transition state accordingly after we stop playing.
+ onClientDrawAtTransitionEnd();
+ });
+
+ animator.start();
return true;
}
@@ -473,10 +483,10 @@ public class PipTransition extends PipTransitionController implements
private boolean isLegacyEnter(@NonNull TransitionInfo info) {
TransitionInfo.Change pipChange = getPipChange(info);
- // If the only change in the changes list is a TO_FRONT mode PiP task,
+ // If the only change in the changes list is a opening type PiP task,
// then this is legacy-enter PiP.
- return pipChange != null && pipChange.getMode() == TRANSIT_TO_FRONT
- && info.getChanges().size() == 1;
+ return pipChange != null && info.getChanges().size() == 1
+ && (pipChange.getMode() == TRANSIT_TO_FRONT || pipChange.getMode() == TRANSIT_OPEN);
}
private boolean isRemovePipTransition(@NonNull TransitionInfo info) {
@@ -514,6 +524,20 @@ public class PipTransition extends PipTransitionController implements
}
@Override
+ public void finishTransition(@Nullable SurfaceControl.Transaction tx) {
+ WindowContainerTransaction wct = null;
+ if (tx != null && mPipTransitionState.mPipTaskToken != null) {
+ // Outside callers can only provide a transaction to be applied with the final draw.
+ // So no actual WM changes can be applied for this transition after this point.
+ wct = new WindowContainerTransaction();
+ wct.setBoundsChangeTransaction(mPipTransitionState.mPipTaskToken, tx);
+ }
+ if (mFinishCallback != null) {
+ mFinishCallback.onTransitionFinished(wct);
+ }
+ }
+
+ @Override
public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState,
@PipTransitionState.TransitionState int newState, @Nullable Bundle extra) {
switch (newState) {
@@ -535,15 +559,6 @@ public class PipTransition extends PipTransitionController implements
mPipTransitionState.mPipTaskToken = null;
mPipTransitionState.mPinnedTaskLeash = null;
break;
- case PipTransitionState.CHANGED_PIP_BOUNDS:
- // Note: this might not be the end of the animation, rather animator just finished
- // adjusting startTx and finishTx and is ready to finishTransition(). The animator
- // can still continue playing the leash into the destination bounds after.
- if (mFinishCallback != null) {
- mFinishCallback.onTransitionFinished(null);
- mFinishCallback = null;
- }
- break;
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/OWNERS
new file mode 100644
index 000000000000..3f3308cfc75a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/OWNERS
@@ -0,0 +1 @@
+include platform/development:/tools/winscope/OWNERS
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index 19af3d544b36..497c3f704c82 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -18,6 +18,8 @@ package com.android.wm.shell.protolog;
import com.android.internal.protolog.common.IProtoLogGroup;
+import java.util.UUID;
+
/**
* Defines logging groups for ProtoLog.
*
@@ -116,6 +118,11 @@ public enum ShellProtoLogGroup implements IProtoLogGroup {
this.mLogToLogcat = logToLogcat;
}
+ @Override
+ public int getId() {
+ return Consts.START_ID + this.ordinal();
+ }
+
private static class Consts {
private static final String TAG_WM_SHELL = "WindowManagerShell";
private static final String TAG_WM_STARTING_WINDOW = "ShellStartingWindow";
@@ -124,5 +131,9 @@ public enum ShellProtoLogGroup implements IProtoLogGroup {
private static final boolean ENABLE_DEBUG = true;
private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true;
+
+ private static final int START_ID = (int) (
+ UUID.nameUUIDFromBytes(ShellProtoLogGroup.class.getName().getBytes())
+ .getMostSignificantBits() % Integer.MAX_VALUE);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
index 62d195efb381..245829ecafb3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
@@ -42,4 +42,7 @@ oneway interface IRecentTasksListener {
* Called when a running task changes.
*/
void onRunningTaskChanged(in RunningTaskInfo taskInfo);
-}
+
+ /** A task has moved to front. */
+ oneway void onTaskMovedToFront(in RunningTaskInfo taskInfo);
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS
new file mode 100644
index 000000000000..452644b05a2a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS
@@ -0,0 +1,6 @@
+# WM shell sub-module task stack owners
+uysalorhan@google.com
+samcackett@google.com
+alexchau@google.com
+silvajordan@google.com
+uwaisashraf@google.com \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 863202d5e1c3..9f3c519b441b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -20,7 +20,7 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.content.pm.PackageManager.FEATURE_PC;
import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
-import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.window.flags.Flags.enableTaskStackObserverInShell;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
import android.app.ActivityManager;
@@ -58,6 +58,7 @@ import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.util.GroupedRecentTaskInfo;
import com.android.wm.shell.util.SplitBounds;
@@ -74,7 +75,8 @@ import java.util.function.Consumer;
* Manages the recent task list from the system, caching it as necessary.
*/
public class RecentTasksController implements TaskStackListenerCallback,
- RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener {
+ RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener,
+ TaskStackTransitionObserver.TaskStackTransitionObserverListener {
private static final String TAG = RecentTasksController.class.getSimpleName();
private final Context mContext;
@@ -85,6 +87,7 @@ public class RecentTasksController implements TaskStackListenerCallback,
private final TaskStackListenerImpl mTaskStackListener;
private final RecentTasksImpl mImpl = new RecentTasksImpl();
private final ActivityTaskManager mActivityTaskManager;
+ private final TaskStackTransitionObserver mTaskStackTransitionObserver;
private RecentsTransitionHandler mTransitionHandler = null;
private IRecentTasksListener mListener;
private final boolean mPcFeatureEnabled;
@@ -113,13 +116,15 @@ public class RecentTasksController implements TaskStackListenerCallback,
TaskStackListenerImpl taskStackListener,
ActivityTaskManager activityTaskManager,
Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ TaskStackTransitionObserver taskStackTransitionObserver,
@ShellMainThread ShellExecutor mainExecutor
) {
if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) {
return null;
}
return new RecentTasksController(context, shellInit, shellController, shellCommandHandler,
- taskStackListener, activityTaskManager, desktopModeTaskRepository, mainExecutor);
+ taskStackListener, activityTaskManager, desktopModeTaskRepository,
+ taskStackTransitionObserver, mainExecutor);
}
RecentTasksController(Context context,
@@ -129,6 +134,7 @@ public class RecentTasksController implements TaskStackListenerCallback,
TaskStackListenerImpl taskStackListener,
ActivityTaskManager activityTaskManager,
Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ TaskStackTransitionObserver taskStackTransitionObserver,
ShellExecutor mainExecutor) {
mContext = context;
mShellController = shellController;
@@ -137,6 +143,7 @@ public class RecentTasksController implements TaskStackListenerCallback,
mPcFeatureEnabled = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
mTaskStackListener = taskStackListener;
mDesktopModeTaskRepository = desktopModeTaskRepository;
+ mTaskStackTransitionObserver = taskStackTransitionObserver;
mMainExecutor = mainExecutor;
shellInit.addInitCallback(this::onInit, this);
}
@@ -155,6 +162,10 @@ public class RecentTasksController implements TaskStackListenerCallback,
mShellCommandHandler.addDumpCallback(this::dump, this);
mTaskStackListener.addListener(this);
mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this));
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this,
+ mMainExecutor);
+ }
}
void setTransitionHandler(RecentsTransitionHandler handler) {
@@ -268,6 +279,12 @@ public class RecentTasksController implements TaskStackListenerCallback,
notifyRecentTasksChanged();
}
+ @Override
+ public void onTaskMovedToFrontThroughTransition(
+ ActivityManager.RunningTaskInfo runningTaskInfo) {
+ notifyTaskMovedToFront(runningTaskInfo);
+ }
+
@VisibleForTesting
void notifyRecentTasksChanged() {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Notify recent tasks changed");
@@ -329,6 +346,19 @@ public class RecentTasksController implements TaskStackListenerCallback,
}
}
+ private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
+ if (mListener == null
+ || !enableTaskStackObserverInShell()
+ || taskInfo.realActivity == null) {
+ return;
+ }
+ try {
+ mListener.onTaskMovedToFront(taskInfo);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed call onTaskMovedToFront", e);
+ }
+ }
+
private boolean shouldEnableRunningTasksForDesktopMode() {
return mPcFeatureEnabled
|| (DesktopModeStatus.canEnterDesktopMode(mContext)
@@ -379,10 +409,6 @@ public class RecentTasksController implements TaskStackListenerCallback,
if (DesktopModeStatus.canEnterDesktopMode(mContext)
&& mDesktopModeTaskRepository.isPresent()
&& mDesktopModeTaskRepository.get().isActiveTask(taskInfo.taskId)) {
- if (mDesktopModeTaskRepository.get().isMinimizedTask(taskInfo.taskId)) {
- // Minimized freeform tasks should not be shown at all.
- continue;
- }
// Freeform tasks will be added as a separate entry
if (mostRecentFreeformTaskIndex == Integer.MAX_VALUE) {
mostRecentFreeformTaskIndex = recentTasks.size();
@@ -465,6 +491,7 @@ public class RecentTasksController implements TaskStackListenerCallback,
}
return null;
}
+
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
@@ -548,6 +575,11 @@ public class RecentTasksController implements TaskStackListenerCallback,
public void onRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
mListener.call(l -> l.onRunningTaskChanged(taskInfo));
}
+
+ @Override
+ public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
+ mListener.call(l -> l.onTaskMovedToFront(taskInfo));
+ }
};
public IRecentTasksImpl(RecentTasksController controller) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index 3a266d9bb3ef..c67cf1d85918 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -74,6 +74,7 @@ import com.android.wm.shell.transition.Transitions;
import java.util.ArrayList;
import java.util.function.Consumer;
+import java.util.function.Supplier;
/**
* Handles the Recents (overview) animation. Only one of these can run at a time. A recents
@@ -84,6 +85,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
private final Transitions mTransitions;
private final ShellExecutor mExecutor;
+ private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
@Nullable
private final RecentTasksController mRecentTasksController;
private IApplicationThread mAnimApp = null;
@@ -101,11 +103,13 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions,
@Nullable RecentTasksController recentTasksController,
- HomeTransitionObserver homeTransitionObserver) {
+ HomeTransitionObserver homeTransitionObserver,
+ Supplier<SurfaceControl.Transaction> transactionSupplier) {
mTransitions = transitions;
mExecutor = transitions.getMainExecutor();
mRecentTasksController = recentTasksController;
mHomeTransitionObserver = homeTransitionObserver;
+ mTransactionSupplier = transactionSupplier;
if (!Transitions.ENABLE_SHELL_TRANSITIONS) return;
if (recentTasksController == null) return;
shellInit.addInitCallback(() -> {
@@ -1056,7 +1060,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
final Transitions.TransitionFinishCallback finishCB = mFinishCB;
mFinishCB = null;
- final SurfaceControl.Transaction t = mFinishTransaction;
+ SurfaceControl.Transaction t = mFinishTransaction;
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (mKeyguardLocked && mRecentsTask != null) {
@@ -1106,6 +1110,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
}
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " normal finish");
+ if (toHome && !mOpeningTasks.isEmpty()) {
+ // Attempting to start a task after swipe to home, don't show it,
+ // move recents to top
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " attempting to start a task after swipe to home");
+ t = mTransactionSupplier.get();
+ wct.reorder(mRecentsTask, true /*onTop*/);
+ mClosingTasks.addAll(mOpeningTasks);
+ mOpeningTasks.clear();
+ }
// The general case: committing to recents, going home, or switching tasks.
for (int i = 0; i < mOpeningTasks.size(); ++i) {
t.show(mOpeningTasks.get(i).mTaskSurface);
@@ -1174,6 +1188,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
mPipTransaction = null;
}
}
+ if (t != mFinishTransaction) {
+ // apply after merges because these changes are accounting for finishWCT changes.
+ mTransitions.setAfterMergeFinishTransaction(mTransition, t);
+ }
cleanUp();
finishCB.onTransitionFinished(wct.isEmpty() ? null : wct);
if (runnerFinishCb != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
new file mode 100644
index 000000000000..7c5f10a5bcca
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.wm.shell.recents
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.os.IBinder
+import android.util.ArrayMap
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import com.android.window.flags.Flags.enableTaskStackObserverInShell
+import com.android.wm.shell.shared.TransitionUtil
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import dagger.Lazy
+import java.util.concurrent.Executor
+
+/**
+ * A [Transitions.TransitionObserver] that observes shell transitions and sends updates to listeners
+ * about task stack changes.
+ *
+ * TODO(346588978) Move split/pip signals here as well so that launcher don't need to handle it
+ */
+class TaskStackTransitionObserver(
+ private val transitions: Lazy<Transitions>,
+ shellInit: ShellInit
+) : Transitions.TransitionObserver {
+
+ private val transitionToTransitionChanges: MutableMap<IBinder, TransitionChanges> =
+ mutableMapOf()
+ private val taskStackTransitionObserverListeners =
+ ArrayMap<TaskStackTransitionObserverListener, Executor>()
+
+ init {
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ shellInit.addInitCallback(::onInit, this)
+ }
+ }
+
+ fun onInit() {
+ transitions.get().registerObserver(this)
+ }
+
+ override fun onTransitionReady(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction
+ ) {
+ if (enableTaskStackObserverInShell()) {
+ val taskInfoList = mutableListOf<RunningTaskInfo>()
+ val transitionTypeList = mutableListOf<Int>()
+
+ for (change in info.changes) {
+ if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) {
+ continue
+ }
+
+ val taskInfo = change.taskInfo
+ if (taskInfo == null || taskInfo.taskId == -1) {
+ continue
+ }
+
+ if (change.mode == WindowManager.TRANSIT_OPEN) {
+ change.taskInfo?.let { taskInfoList.add(it) }
+ transitionTypeList.add(change.mode)
+ }
+ }
+ transitionToTransitionChanges.put(
+ transition,
+ TransitionChanges(taskInfoList, transitionTypeList)
+ )
+ }
+ }
+
+ override fun onTransitionStarting(transition: IBinder) {}
+
+ override fun onTransitionMerged(merged: IBinder, playing: IBinder) {}
+
+ override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
+ val taskInfoList =
+ transitionToTransitionChanges.getOrDefault(transition, TransitionChanges()).taskInfoList
+ val typeList =
+ transitionToTransitionChanges
+ .getOrDefault(transition, TransitionChanges())
+ .transitionTypeList
+ transitionToTransitionChanges.remove(transition)
+
+ for ((index, taskInfo) in taskInfoList.withIndex()) {
+ if (
+ TransitionUtil.isOpeningType(typeList[index]) &&
+ taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
+ ) {
+ notifyTaskStackTransitionObserverListeners(taskInfo)
+ }
+ }
+ }
+
+ fun addTaskStackTransitionObserverListener(
+ taskStackTransitionObserverListener: TaskStackTransitionObserverListener,
+ executor: Executor
+ ) {
+ taskStackTransitionObserverListeners[taskStackTransitionObserverListener] = executor
+ }
+
+ fun removeTaskStackTransitionObserverListener(
+ taskStackTransitionObserverListener: TaskStackTransitionObserverListener
+ ) {
+ taskStackTransitionObserverListeners.remove(taskStackTransitionObserverListener)
+ }
+
+ private fun notifyTaskStackTransitionObserverListeners(taskInfo: RunningTaskInfo) {
+ taskStackTransitionObserverListeners.forEach { (listener, executor) ->
+ executor.execute { listener.onTaskMovedToFrontThroughTransition(taskInfo) }
+ }
+ }
+
+ /** Listener to use to get updates regarding task stack from this observer */
+ interface TaskStackTransitionObserverListener {
+ /** Called when a task is moved to front. */
+ fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {}
+ }
+
+ private data class TransitionChanges(
+ val taskInfoList: MutableList<RunningTaskInfo> = ArrayList(),
+ val transitionTypeList: MutableList<Int> = ArrayList()
+ )
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index b9d70e1a599d..dd219d32bbaa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -24,7 +24,6 @@ import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
-import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.common.MultiInstanceHelper.getComponent;
import static com.android.wm.shell.common.MultiInstanceHelper.getShortcutComponent;
import static com.android.wm.shell.common.MultiInstanceHelper.samePackage;
@@ -1241,8 +1240,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
@Override
public void invalidate() {
mController = null;
- // Unregister the listener to ensure any registered binder death recipients are unlinked
+ // Unregister the listeners to ensure any binder death recipients are unlinked
mListener.unregister();
+ mSelectListener.unregister();
}
@Override
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 82ef422f829a..45eff4a24898 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
@@ -1524,6 +1524,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
prepareExitSplitScreen(mTopStageAfterFoldDismiss, wct);
mSplitTransitions.startDismissTransition(wct, this,
mTopStageAfterFoldDismiss, EXIT_REASON_DEVICE_FOLDED);
+ setSplitsVisible(false);
} else {
exitSplitScreen(
mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage,
@@ -1846,7 +1847,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
void finishEnterSplitScreen(SurfaceControl.Transaction finishT) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "finishEnterSplitScreen");
- mSplitLayout.update(finishT, true /* resetImePosition */);
+ mSplitLayout.update(null, true /* resetImePosition */);
mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash);
mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash);
setDividerVisibility(true, finishT);
@@ -1893,6 +1894,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// will be canceled.
options.setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
+
+ // TODO (b/336477473): Disallow enter PiP when launching a task in split by default;
+ // this might have to be changed as more split-to-pip cujs are defined.
+ options.setDisallowEnterPictureInPictureWhileLaunching(true);
opts.putAll(options.toBundle());
}
@@ -2644,7 +2649,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
@Nullable TransitionRequestInfo request) {
final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
if (triggerTask == null) {
- if (isSplitActive()) {
+ if (isSplitScreenVisible()) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d display rotation",
request.getDebugId());
// Check if the display is rotating.
@@ -2757,6 +2762,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// cases above and it is not already visible
return null;
} else {
+ if (triggerTask.parentTaskId == mMainStage.mRootTaskInfo.taskId
+ || triggerTask.parentTaskId == mSideStage.mRootTaskInfo.taskId) {
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d "
+ + "restoring to split", request.getDebugId());
+ out = new WindowContainerTransaction();
+ mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(),
+ TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, false /* resizeAnim */);
+ }
if (isOpening && getStageOfTask(triggerTask) != null) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d enter split",
request.getDebugId());
@@ -3103,7 +3116,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// Includes TRANSIT_CHANGE to cover reparenting top-most task to split.
mainChild = change;
} else if (sideChild == null && stageType == STAGE_TYPE_SIDE
- && isOpeningType(change.getMode())) {
+ && (isOpeningType(change.getMode()) || change.getMode() == TRANSIT_CHANGE)) {
sideChild = change;
} else if (stageType != STAGE_TYPE_UNDEFINED && change.getMode() == TRANSIT_TO_BACK) {
// Collect all to back task's and evict them when transition finished.
@@ -3114,7 +3127,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
SplitScreenTransitions.EnterSession pendingEnter = mSplitTransitions.mPendingEnter;
if (pendingEnter.mExtraTransitType
== TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) {
- // Open to side should only be used when split already active and foregorund.
+ // Open to side should only be used when split already active and foregorund or when
+ // app is restoring to split from fullscreen.
if (mainChild == null && sideChild == null) {
Log.w(TAG, splitFailureMessage("startPendingEnterAnimation",
"Launched a task in split, but didn't receive any task in transition."));
@@ -3201,6 +3215,22 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mPausingTasks.clear();
});
+ if (info.getType() == TRANSIT_CHANGE && !isSplitActive()
+ && pendingEnter.mExtraTransitType == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) {
+ if (finalMainChild != null && finalSideChild == null) {
+ requestEnterSplitSelect(finalMainChild.getTaskInfo(),
+ new WindowContainerTransaction(),
+ getMainStagePosition(), finalMainChild.getStartAbsBounds());
+ } else if (finalSideChild != null && finalMainChild == null) {
+ requestEnterSplitSelect(finalSideChild.getTaskInfo(),
+ new WindowContainerTransaction(),
+ getSideStagePosition(), finalSideChild.getStartAbsBounds());
+ } else {
+ throw new IllegalStateException(
+ "Attempting to restore to split but reparenting change not found");
+ }
+ }
+
finishEnterSplitScreen(finishT);
addDividerBarToTransition(info, true /* show */);
return true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
index bec4ba3bf0d1..fa084c585a59 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
@@ -23,7 +23,6 @@ import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SOLID_COLOR
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_WINDOWLESS;
-import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW;
import android.app.ActivityManager.RunningTaskInfo;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
index 66b3553bea09..8fc54edcbd4b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
@@ -21,8 +21,6 @@ import static android.graphics.Color.WHITE;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
-import static com.android.window.flags.Flags.windowSessionRelayoutInfo;
-
import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -30,7 +28,6 @@ import android.app.ActivityManager;
import android.app.ActivityManager.TaskDescription;
import android.graphics.Paint;
import android.graphics.Rect;
-import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.Trace;
@@ -139,16 +136,10 @@ public class TaskSnapshotWindow {
}
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout");
- if (windowSessionRelayoutInfo()) {
- final WindowRelayoutResult outRelayoutResult = new WindowRelayoutResult(tmpFrames,
- tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls);
- session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0,
- outRelayoutResult);
- } else {
- session.relayoutLegacy(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0,
- tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState,
- tmpControls, new Bundle());
- }
+ final WindowRelayoutResult outRelayoutResult = new WindowRelayoutResult(tmpFrames,
+ tmpMergedConfiguration, surfaceControl, tmpInsetsState, tmpControls);
+ session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0,
+ outRelayoutResult);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
} catch (RemoteException e) {
snapshotSurface.clearWindowSynced();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
index b03daaafd70c..35427b93acea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/CounterRotatorHelper.java
@@ -94,6 +94,11 @@ public class CounterRotatorHelper {
return rotatedBounds;
}
+ /** Returns true if the change is put on a surface in previous rotation. */
+ public boolean isRotated(@NonNull TransitionInfo.Change change) {
+ return mLastRotationDelta != 0 && mRotatorMap.containsKey(change.getParent());
+ }
+
/**
* Removes the counter rotation surface in the finish transaction. No need to reparent the
* children as the finish transaction should have already taken care of that.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 2d6ba6ee7217..9412b2b0b243 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -103,6 +103,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.window.flags.Flags;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
@@ -516,7 +517,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
animRelOffset.y = Math.max(animRelOffset.y, change.getEndRelOffset().y);
}
- if (change.getActivityComponent() != null && !isActivityLevel) {
+ if (change.getActivityComponent() != null && !isActivityLevel
+ && !mRotator.isRotated(change)) {
// At this point, this is an independent activity change in a non-activity
// transition. This means that an activity transition got erroneously combined
// with another ongoing transition. This then means that the animation root may
@@ -543,7 +545,13 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
mTransactionPool, mMainExecutor, animRelOffset, cornerRadius,
clipRect);
- if (info.getAnimationOptions() != null) {
+ final TransitionInfo.AnimationOptions options;
+ if (Flags.moveAnimationOptionsToChange()) {
+ options = info.getAnimationOptions();
+ } else {
+ options = change.getAnimationOptions();
+ }
+ if (options != null) {
attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions(),
cornerRadius);
}
@@ -725,7 +733,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
final boolean isOpeningType = TransitionUtil.isOpeningType(type);
final boolean enter = TransitionUtil.isOpeningType(changeMode);
final boolean isTask = change.getTaskInfo() != null;
- final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
+ final TransitionInfo.AnimationOptions options;
+ if (Flags.moveAnimationOptionsToChange()) {
+ options = change.getAnimationOptions();
+ } else {
+ options = info.getAnimationOptions();
+ }
final int overrideType = options != null ? options.getType() : ANIM_NONE;
final Rect endBounds = TransitionUtil.isClosingType(changeMode)
? mRotator.getEndBoundsInStartRotation(change)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
index b1a1e5999aa9..9b27e413b5e4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -133,5 +133,9 @@ public class HomeTransitionObserver implements TransitionObserver,
*/
public void invalidate(Transitions transitions) {
transitions.unregisterObserver(this);
+ if (mListener != null) {
+ // Unregister the listener to ensure any registered binder death recipients are unlinked
+ mListener.unregister();
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
index ad4f02d13cc6..2047b5a88604 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java
@@ -55,6 +55,7 @@ import android.window.TransitionInfo;
import com.android.internal.R;
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.window.flags.Flags;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.shared.TransitionUtil;
@@ -71,7 +72,12 @@ public class TransitionAnimationHelper {
final int changeFlags = change.getFlags();
final boolean enter = TransitionUtil.isOpeningType(changeMode);
final boolean isTask = change.getTaskInfo() != null;
- final TransitionInfo.AnimationOptions options = info.getAnimationOptions();
+ final TransitionInfo.AnimationOptions options;
+ if (Flags.moveAnimationOptionsToChange()) {
+ options = change.getAnimationOptions();
+ } else {
+ options = info.getAnimationOptions();
+ }
final int overrideType = options != null ? options.getType() : ANIM_NONE;
int animAttr = 0;
boolean translucent = false;
@@ -246,7 +252,7 @@ public class TransitionAnimationHelper {
if (!a.getShowBackdrop()) {
return defaultColor;
}
- if (info.getAnimationOptions() != null
+ if (!Flags.moveAnimationOptionsToChange() && info.getAnimationOptions() != null
&& info.getAnimationOptions().getBackgroundColor() != 0) {
// If available use the background color provided through AnimationOptions
return info.getAnimationOptions().getBackgroundColor();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 6ade81c0f3a1..d2760ff88ece 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -35,7 +35,6 @@ import static android.window.TransitionInfo.FLAG_NO_ANIMATION;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary;
-import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
@@ -54,6 +53,7 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.provider.Settings;
+import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import android.view.SurfaceControl;
@@ -166,9 +166,6 @@ public class Transitions implements RemoteCallable<Transitions>,
public static final int TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP =
WindowManager.TRANSIT_FIRST_CUSTOM + 11;
- /** Transition type to fullscreen from desktop mode. */
- public static final int TRANSIT_EXIT_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 12;
-
/** Transition type to cancel the drag to desktop mode. */
public static final int TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP =
WindowManager.TRANSIT_FIRST_CUSTOM + 13;
@@ -177,9 +174,6 @@ public class Transitions implements RemoteCallable<Transitions>,
public static final int TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE =
WindowManager.TRANSIT_FIRST_CUSTOM + 14;
- /** Transition to animate task to desktop. */
- public static final int TRANSIT_MOVE_TO_DESKTOP = WindowManager.TRANSIT_FIRST_CUSTOM + 15;
-
/** Transition to resize PiP task. */
public static final int TRANSIT_RESIZE_PIP = TRANSIT_FIRST_CUSTOM + 16;
@@ -190,6 +184,10 @@ public class Transitions implements RemoteCallable<Transitions>,
// TRANSIT_FIRST_CUSTOM + 17
TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_DRAG_RESIZE;
+ /** Transition type for desktop mode transitions. */
+ public static final int TRANSIT_DESKTOP_MODE_TYPES =
+ WindowManager.TRANSIT_FIRST_CUSTOM + 100;
+
private final ShellTaskOrganizer mOrganizer;
private final Context mContext;
private final ShellExecutor mMainExecutor;
@@ -229,7 +227,8 @@ public class Transitions implements RemoteCallable<Transitions>,
private boolean mDisableForceSync = false;
private static final class ActiveTransition {
- IBinder mToken;
+ final IBinder mToken;
+
TransitionHandler mHandler;
boolean mAborted;
TransitionInfo mInfo;
@@ -239,6 +238,17 @@ public class Transitions implements RemoteCallable<Transitions>,
/** Ordered list of transitions which have been merged into this one. */
private ArrayList<ActiveTransition> mMerged;
+ /**
+ * @deprecated DO NOT USE THIS unless absolutely necessary. It will be removed once
+ * everything migrates off finishWCT.
+ */
+ @java.lang.Deprecated
+ SurfaceControl.Transaction mAfterMergeFinishT;
+
+ ActiveTransition(IBinder token) {
+ mToken = token;
+ }
+
boolean isSync() {
return (mInfo.getFlags() & TransitionInfo.FLAG_SYNC) != 0;
}
@@ -268,6 +278,9 @@ public class Transitions implements RemoteCallable<Transitions>,
}
}
+ /** All transitions that we have created, but not yet finished. */
+ private final ArrayMap<IBinder, ActiveTransition> mKnownTransitions = new ArrayMap<>();
+
/** Keeps track of transitions which have been started, but aren't ready yet. */
private final ArrayList<ActiveTransition> mPendingTransitions = new ArrayList<>();
@@ -657,8 +670,10 @@ public class Transitions implements RemoteCallable<Transitions>,
}
if (change.hasFlags(FLAG_NO_ANIMATION)) {
hasNoAnimation = true;
- } else {
- // at-least one relevant participant *is* animated, so we need to animate.
+ } else if (!TransitionUtil.isOrderOnly(change) && !change.hasFlags(FLAG_IS_OCCLUDED)) {
+ // Ignore the order only or occluded changes since they shouldn't be visible during
+ // animation. For anything else, we need to animate if at-least one relevant
+ // participant *is* animated,
return false;
}
}
@@ -690,7 +705,7 @@ public class Transitions implements RemoteCallable<Transitions>,
info.getDebugId(), transitionToken, info);
int activeIdx = findByToken(mPendingTransitions, transitionToken);
if (activeIdx < 0) {
- final ActiveTransition existing = getKnownTransition(transitionToken);
+ final ActiveTransition existing = mKnownTransitions.get(transitionToken);
if (existing != null) {
Log.e(TAG, "Got duplicate transitionReady for " + transitionToken);
// The transition is already somewhere else in the pipeline, so just return here.
@@ -705,8 +720,8 @@ public class Transitions implements RemoteCallable<Transitions>,
+ transitionToken + ". expecting one of "
+ Arrays.toString(mPendingTransitions.stream().map(
activeTransition -> activeTransition.mToken).toArray()));
- final ActiveTransition fallback = new ActiveTransition();
- fallback.mToken = transitionToken;
+ final ActiveTransition fallback = new ActiveTransition(transitionToken);
+ mKnownTransitions.put(transitionToken, fallback);
mPendingTransitions.add(fallback);
activeIdx = mPendingTransitions.size() - 1;
}
@@ -746,7 +761,7 @@ public class Transitions implements RemoteCallable<Transitions>,
// Sleep starts a process of forcing all prior transitions to finish immediately
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
"Start finish-for-sync track %d", i);
- finishForSync(active, i, null /* forceFinish */);
+ finishForSync(active.mToken, i, null /* forceFinish */);
}
if (hadPreceding) {
return false;
@@ -864,6 +879,7 @@ public class Transitions implements RemoteCallable<Transitions>,
} else if (mPendingTransitions.isEmpty()) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition "
+ "animations finished");
+ mKnownTransitions.clear();
// Run all runnables from the run-when-idle queue.
for (int i = 0; i < mRunWhenIdleQueue.size(); i++) {
mRunWhenIdleQueue.get(i).run();
@@ -884,7 +900,7 @@ public class Transitions implements RemoteCallable<Transitions>,
ready.mStartT.apply();
}
// finish now since there's nothing to animate. Calls back into processReadyQueue
- onFinish(ready, null);
+ onFinish(ready.mToken, null);
return;
}
playTransition(ready);
@@ -943,8 +959,10 @@ public class Transitions implements RemoteCallable<Transitions>,
private void playTransition(@NonNull ActiveTransition active) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Playing animation for %s", active);
+ final var token = active.mToken;
+
for (int i = 0; i < mObservers.size(); ++i) {
- mObservers.get(i).onTransitionStarting(active.mToken);
+ mObservers.get(i).onTransitionStarting(token);
}
setupAnimHierarchy(active.mInfo, active.mStartT, active.mFinishT);
@@ -953,8 +971,8 @@ public class Transitions implements RemoteCallable<Transitions>,
if (active.mHandler != null) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try firstHandler %s",
active.mHandler);
- boolean consumed = active.mHandler.startAnimation(active.mToken, active.mInfo,
- active.mStartT, active.mFinishT, (wct) -> onFinish(active, wct));
+ boolean consumed = active.mHandler.startAnimation(token, active.mInfo,
+ active.mStartT, active.mFinishT, (wct) -> onFinish(token, wct));
if (consumed) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler");
mTransitionTracer.logDispatched(active.mInfo.getDebugId(), active.mHandler);
@@ -962,8 +980,8 @@ public class Transitions implements RemoteCallable<Transitions>,
}
}
// Otherwise give every other handler a chance
- active.mHandler = dispatchTransition(active.mToken, active.mInfo, active.mStartT,
- active.mFinishT, (wct) -> onFinish(active, wct), active.mHandler);
+ active.mHandler = dispatchTransition(token, active.mInfo, active.mStartT,
+ active.mFinishT, (wct) -> onFinish(token, wct), active.mHandler);
}
/**
@@ -1007,6 +1025,20 @@ public class Transitions implements RemoteCallable<Transitions>,
return null;
}
+ /** @deprecated */
+ @java.lang.Deprecated
+ public void setAfterMergeFinishTransaction(IBinder transition,
+ SurfaceControl.Transaction afterMergeFinishT) {
+ final ActiveTransition at = mKnownTransitions.get(transition);
+ if (at == null) return;
+ if (at.mAfterMergeFinishT != null) {
+ Log.e(TAG, "Setting after-merge-t >1 time on transition: " + at.mInfo.getDebugId());
+ at.mAfterMergeFinishT.merge(afterMergeFinishT);
+ return;
+ }
+ at.mAfterMergeFinishT = afterMergeFinishT;
+ }
+
/** Aborts a transition. This will still queue it up to maintain order. */
private void onAbort(ActiveTransition transition) {
final Track track = mTracks.get(transition.getTrack());
@@ -1039,10 +1071,15 @@ public class Transitions implements RemoteCallable<Transitions>,
info.releaseAnimSurfaces();
}
- private void onFinish(ActiveTransition active,
+ private void onFinish(IBinder token,
@Nullable WindowContainerTransaction wct) {
+ final ActiveTransition active = mKnownTransitions.get(token);
+ if (active == null) {
+ Log.e(TAG, "Trying to finish a non-existent transition: " + token);
+ return;
+ }
final Track track = mTracks.get(active.getTrack());
- if (track.mActiveTransition != active) {
+ if (track == null || track.mActiveTransition != active) {
Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or "
+ " a handler didn't properly deal with a merge. " + active,
new RuntimeException());
@@ -1062,6 +1099,7 @@ public class Transitions implements RemoteCallable<Transitions>,
}
// Merge all associated transactions together
SurfaceControl.Transaction fullFinish = active.mFinishT;
+ SurfaceControl.Transaction afterMergeFinish = active.mAfterMergeFinishT;
if (active.mMerged != null) {
for (int iM = 0; iM < active.mMerged.size(); ++iM) {
final ActiveTransition toMerge = active.mMerged.get(iM);
@@ -1081,6 +1119,21 @@ public class Transitions implements RemoteCallable<Transitions>,
fullFinish.merge(toMerge.mFinishT);
}
}
+ if (toMerge.mAfterMergeFinishT != null) {
+ if (afterMergeFinish == null) {
+ afterMergeFinish = toMerge.mAfterMergeFinishT;
+ } else {
+ afterMergeFinish.merge(toMerge.mAfterMergeFinishT);
+ }
+ toMerge.mAfterMergeFinishT = null;
+ }
+ }
+ }
+ if (afterMergeFinish != null) {
+ if (fullFinish == null) {
+ fullFinish = afterMergeFinish;
+ } else {
+ fullFinish.merge(afterMergeFinish);
}
}
if (fullFinish != null) {
@@ -1095,54 +1148,25 @@ public class Transitions implements RemoteCallable<Transitions>,
ActiveTransition merged = active.mMerged.get(iM);
mOrganizer.finishTransition(merged.mToken, null /* wct */);
releaseSurfaces(merged.mInfo);
+ mKnownTransitions.remove(merged.mToken);
}
active.mMerged.clear();
}
+ mKnownTransitions.remove(token);
// Now that this is done, check the ready queue for more work.
processReadyQueue(track);
}
- /**
- * Checks to see if the transition specified by `token` is already known. If so, it will be
- * returned.
- */
- @Nullable
- private ActiveTransition getKnownTransition(IBinder token) {
- for (int i = 0; i < mPendingTransitions.size(); ++i) {
- final ActiveTransition active = mPendingTransitions.get(i);
- if (active.mToken == token) return active;
- }
- for (int i = 0; i < mReadyDuringSync.size(); ++i) {
- final ActiveTransition active = mReadyDuringSync.get(i);
- if (active.mToken == token) return active;
- }
- for (int t = 0; t < mTracks.size(); ++t) {
- final Track tr = mTracks.get(t);
- for (int i = 0; i < tr.mReadyTransitions.size(); ++i) {
- final ActiveTransition active = tr.mReadyTransitions.get(i);
- if (active.mToken == token) return active;
- }
- final ActiveTransition active = tr.mActiveTransition;
- if (active == null) continue;
- if (active.mToken == token) return active;
- if (active.mMerged == null) continue;
- for (int m = 0; m < active.mMerged.size(); ++m) {
- final ActiveTransition merged = active.mMerged.get(m);
- if (merged.mToken == token) return merged;
- }
- }
- return null;
- }
-
void requestStartTransition(@NonNull IBinder transitionToken,
@Nullable TransitionRequestInfo request) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested (#%d): %s %s",
request.getDebugId(), transitionToken, request);
- if (getKnownTransition(transitionToken) != null) {
+ if (mKnownTransitions.containsKey(transitionToken)) {
throw new RuntimeException("Transition already started " + transitionToken);
}
- final ActiveTransition active = new ActiveTransition();
+ final ActiveTransition active = new ActiveTransition(transitionToken);
+ mKnownTransitions.put(transitionToken, active);
WindowContainerTransaction wct = null;
// If we have sleep, we use a special handler and we try to finish everything ASAP.
@@ -1182,7 +1206,6 @@ public class Transitions implements RemoteCallable<Transitions>,
wct.setBounds(request.getTriggerTask().token, null);
}
mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct);
- active.mToken = transitionToken;
// Currently, WMCore only does one transition at a time. If it makes a requestStart, it
// is already collecting that transition on core-side, so it will be the next one to
// become ready. There may already be pending transitions added as part of direct
@@ -1201,9 +1224,10 @@ public class Transitions implements RemoteCallable<Transitions>,
@NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Directly starting a new transition "
+ "type=%d wct=%s handler=%s", type, wct, handler);
- final ActiveTransition active = new ActiveTransition();
+ final ActiveTransition active =
+ new ActiveTransition(mOrganizer.startNewTransition(type, wct));
active.mHandler = handler;
- active.mToken = mOrganizer.startNewTransition(type, wct);
+ mKnownTransitions.put(active.mToken, active);
mPendingTransitions.add(active);
return active.mToken;
}
@@ -1243,14 +1267,14 @@ public class Transitions implements RemoteCallable<Transitions>,
*
* This is then repeated until there are no more pending sleep transitions.
*
- * @param reason The SLEEP transition that triggered this round of finishes. We will continue
- * looping round finishing transitions as long as this is still waiting.
+ * @param reason The token for the SLEEP transition that triggered this round of finishes.
+ * We will continue looping round finishing transitions until this is ready.
* @param forceFinish When non-null, this is the transition that we last sent the SLEEP merge
* signal to -- so it will be force-finished if it's still running.
*/
- private void finishForSync(ActiveTransition reason,
+ private void finishForSync(IBinder reason,
int trackIdx, @Nullable ActiveTransition forceFinish) {
- if (getKnownTransition(reason.mToken) == null) {
+ if (!mKnownTransitions.containsKey(reason)) {
Log.d(TAG, "finishForSleep: already played sync transition " + reason);
return;
}
@@ -1270,7 +1294,7 @@ public class Transitions implements RemoteCallable<Transitions>,
forceFinish.mHandler.onTransitionConsumed(
forceFinish.mToken, true /* aborted */, null /* finishTransaction */);
}
- onFinish(forceFinish, null);
+ onFinish(forceFinish.mToken, null);
}
}
if (track.isIdle() || mReadyDuringSync.isEmpty()) {
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 37cdbb47bfe8..e1009a0ae8bb 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
@@ -73,6 +73,7 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.jank.Cuj;
import com.android.internal.protolog.common.ProtoLog;
import com.android.window.flags.Flags;
import com.android.wm.shell.R;
@@ -81,8 +82,10 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.common.InteractionJankMonitorUtils;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator;
import com.android.wm.shell.desktopmode.DesktopTasksController;
@@ -102,6 +105,7 @@ import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionReg
import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
import java.io.PrintWriter;
+import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
@@ -151,6 +155,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
private final DisplayInsetsController mDisplayInsetsController;
private final Region mExclusionRegion = Region.obtain();
private boolean mInImmersiveMode;
+ private final String mSysUIPackageName;
private final ISystemGestureExclusionListener mGestureExclusionListener =
new ISystemGestureExclusionListener.Stub() {
@@ -246,6 +251,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
mInputManager = mContext.getSystemService(InputManager.class);
mWindowDecorByTaskId = windowDecorByTaskId;
+ mSysUIPackageName = mContext.getResources().getString(
+ com.android.internal.R.string.config_systemUi);
shellInit.addInitCallback(this::onInit, this);
}
@@ -430,7 +437,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
SplitScreenController.EXIT_REASON_DESKTOP_MODE);
} else {
WindowContainerTransaction wct = new WindowContainerTransaction();
- mDesktopTasksController.onDesktopWindowClose(wct, mTaskId);
+ mDesktopTasksController.onDesktopWindowClose(wct, mDisplayId, mTaskId);
mTaskOperations.closeTask(mTaskToken, wct);
}
} else if (id == R.id.back_button) {
@@ -438,7 +445,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
} else if (id == R.id.caption_handle || id == R.id.open_menu_button) {
if (!decoration.isHandleMenuActive()) {
moveTaskToFront(decoration.mTaskInfo);
- decoration.createHandleMenu();
+ decoration.createHandleMenu(mSplitScreenController);
} else {
decoration.closeHandleMenu();
}
@@ -447,7 +454,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
// App sometimes draws before the insets from WindowDecoration#relayout have
// been added, so they must be added here
mWindowDecorByTaskId.get(mTaskId).addCaptionInset(wct);
- mDesktopTasksController.moveToDesktop(mTaskId, wct);
+ mDesktopTasksController.moveToDesktop(mTaskId, wct,
+ DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON);
decoration.closeHandleMenu();
} else if (id == R.id.fullscreen_button) {
decoration.closeHandleMenu();
@@ -455,7 +463,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mSplitScreenController.moveTaskToFullscreen(mTaskId,
SplitScreenController.EXIT_REASON_DESKTOP_MODE);
} else {
- mDesktopTasksController.moveToFullscreen(mTaskId);
+ mDesktopTasksController.moveToFullscreen(mTaskId,
+ DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON);
}
} else if (id == R.id.split_screen_button) {
decoration.closeHandleMenu();
@@ -463,11 +472,17 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
} else if (id == R.id.collapse_menu_button) {
decoration.closeHandleMenu();
} else if (id == R.id.maximize_window) {
+ InteractionJankMonitorUtils.beginTracing(
+ Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v,
+ /* tag= */ "caption_bar_button");
final RunningTaskInfo taskInfo = decoration.mTaskInfo;
decoration.closeHandleMenu();
decoration.closeMaximizeMenu();
mDesktopTasksController.toggleDesktopTaskSize(taskInfo);
} else if (id == R.id.maximize_menu_maximize_button) {
+ InteractionJankMonitorUtils.beginTracing(
+ Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, /* view= */ v,
+ /* tag= */ "maximize_menu_option");
final RunningTaskInfo taskInfo = decoration.mTaskInfo;
mDesktopTasksController.toggleDesktopTaskSize(taskInfo);
decoration.closeHandleMenu();
@@ -705,6 +720,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
return false;
}
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+ InteractionJankMonitorUtils.beginTracing(
+ Cuj.CUJ_DESKTOP_MODE_MAXIMIZE_WINDOW, mContext,
+ /* surface= */ decoration.mTaskSurface, /* tag= */ "double_tap");
mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo);
return true;
}
@@ -1032,10 +1050,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
&& taskInfo.isFocused) {
return false;
}
+ // TODO(b/347289970): Consider replacing with API
if (Flags.enableDesktopWindowingModalsPolicy()
&& isSingleTopActivityTranslucent(taskInfo)) {
return false;
}
+ if (isSystemUIApplication(taskInfo)) {
+ return false;
+ }
return DesktopModeStatus.canEnterDesktopMode(mContext)
&& !DesktopWallpaperActivity.isWallpaperTask(taskInfo)
&& taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED
@@ -1106,6 +1128,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
&& mSplitScreenController.isTaskInSplitScreen(taskId);
}
+ // TODO(b/347289970): Consider replacing with API
+ private boolean isSystemUIApplication(RunningTaskInfo taskInfo) {
+ if (taskInfo.baseActivity != null) {
+ return (Objects.equals(taskInfo.baseActivity.getPackageName(), mSysUIPackageName));
+ }
+ return false;
+ }
+
private void dump(PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + "DesktopModeWindowDecorViewModel");
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 eced07831ff7..4d597cac889e 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
@@ -68,10 +68,11 @@ import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
-import com.android.wm.shell.windowdecor.viewholder.DesktopModeAppControlsWindowDecorationViewHolder;
-import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder;
-import com.android.wm.shell.windowdecor.viewholder.DesktopModeWindowDecorationViewHolder;
+import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder;
+import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder;
+import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder;
import kotlin.Unit;
@@ -90,7 +91,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
private final Choreographer mChoreographer;
private final SyncTransactionQueue mSyncQueue;
- private DesktopModeWindowDecorationViewHolder mWindowDecorViewHolder;
+ private WindowDecorationViewHolder mWindowDecorViewHolder;
private View.OnClickListener mOnCaptionButtonClickListener;
private View.OnTouchListener mOnCaptionTouchListener;
private View.OnLongClickListener mOnCaptionLongClickListener;
@@ -98,10 +99,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
private DragPositioningCallback mDragPositioningCallback;
private DragResizeInputListener mDragResizeListener;
private DragDetector mDragDetector;
-
+ private Runnable mCurrentViewHostRunnable = null;
private RelayoutParams mRelayoutParams = new RelayoutParams();
private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
new WindowDecoration.RelayoutResult<>();
+ private final Runnable mViewHostRunnable =
+ () -> updateViewHost(mRelayoutParams, null /* onDrawTransaction */, mResult);
private final Point mPositionInParent = new Point();
private HandleMenu mHandleMenu;
@@ -193,17 +196,88 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
// position and crop are set.
final boolean shouldSetTaskPositionAndCrop = !DesktopModeStatus.isVeiledResizeEnabled()
&& mTaskDragResizer.isResizingOrAnimating();
- // Use |applyStartTransactionOnDraw| so that the transaction (that applies task crop) is
- // synced with the buffer transaction (that draws the View). Both will be shown on screen
- // at the same, whereas applying them independently causes flickering. See b/270202228.
- relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */,
- shouldSetTaskPositionAndCrop);
+ // For headers only (i.e. in freeform): use |applyStartTransactionOnDraw| so that the
+ // transaction (that applies task crop) is synced with the buffer transaction (that draws
+ // the View). Both will be shown on screen at the same, whereas applying them independently
+ // causes flickering. See b/270202228.
+ final boolean applyTransactionOnDraw =
+ taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM;
+ relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskPositionAndCrop);
+ if (!applyTransactionOnDraw) {
+ t.apply();
+ }
}
void relayout(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
Trace.beginSection("DesktopModeWindowDecoration#relayout");
+ if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+ // The Task is in Freeform mode -> show its header in sync since it's an integral part
+ // of the window itself - a delayed header might cause bad UX.
+ relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw,
+ shouldSetTaskPositionAndCrop);
+ } else {
+ // The Task is outside Freeform mode -> allow the handle view to be delayed since the
+ // handle is just a small addition to the window.
+ relayoutWithDelayedViewHost(taskInfo, startT, finishT, applyStartTransactionOnDraw,
+ shouldSetTaskPositionAndCrop);
+ }
+ Trace.endSection();
+ }
+
+ /** Run the whole relayout phase immediately without delay. */
+ private void relayoutInSync(ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
+ boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
+ // Clear the current ViewHost runnable as we will update the ViewHost here
+ clearCurrentViewHostRunnable();
+ updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, applyStartTransactionOnDraw,
+ shouldSetTaskPositionAndCrop);
+ if (mResult.mRootView != null) {
+ updateViewHost(mRelayoutParams, startT, mResult);
+ }
+ }
+
+ /**
+ * Clear the current ViewHost runnable - to ensure it doesn't run once relayout params have been
+ * updated.
+ */
+ private void clearCurrentViewHostRunnable() {
+ if (mCurrentViewHostRunnable != null) {
+ mHandler.removeCallbacks(mCurrentViewHostRunnable);
+ mCurrentViewHostRunnable = null;
+ }
+ }
+
+ /**
+ * Relayout the window decoration but repost some of the work, to unblock the current callstack.
+ */
+ private void relayoutWithDelayedViewHost(ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
+ boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
+ if (applyStartTransactionOnDraw) {
+ throw new IllegalArgumentException(
+ "We cannot both sync viewhost ondraw and delay viewhost creation.");
+ }
+ // Clear the current ViewHost runnable as we will update the ViewHost here
+ clearCurrentViewHostRunnable();
+ updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT,
+ false /* applyStartTransactionOnDraw */, shouldSetTaskPositionAndCrop);
+ if (mResult.mRootView == null) {
+ // This means something blocks the window decor from showing, e.g. the task is hidden.
+ // Nothing is set up in this case including the decoration surface.
+ return;
+ }
+ // Store the current runnable so it can be removed if we start a new relayout.
+ mCurrentViewHostRunnable = mViewHostRunnable;
+ mHandler.post(mCurrentViewHostRunnable);
+ }
+
+ private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo,
+ SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
+ boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
+ Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces");
if (isHandleMenuActive()) {
mHandleMenu.relayout(startT);
}
@@ -215,8 +289,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
final WindowContainerTransaction wct = new WindowContainerTransaction();
- Trace.beginSection("DesktopModeWindowDecoration#relayout-inner");
- relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
+ Trace.beginSection("DesktopModeWindowDecoration#relayout-updateViewsAndSurfaces");
+ updateViewsAndSurfaces(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
Trace.endSection();
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
@@ -227,37 +301,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
if (mResult.mRootView == null) {
// This means something blocks the window decor from showing, e.g. the task is hidden.
// Nothing is set up in this case including the decoration surface.
- Trace.endSection(); // DesktopModeWindowDecoration#relayout
+ Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces
return;
}
if (oldRootView != mResult.mRootView) {
- if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) {
- mWindowDecorViewHolder = new DesktopModeFocusedWindowDecorationViewHolder(
- mResult.mRootView,
- mOnCaptionTouchListener,
- mOnCaptionButtonClickListener
- );
- } else if (mRelayoutParams.mLayoutResId
- == R.layout.desktop_mode_app_controls_window_decor) {
- loadAppInfoIfNeeded();
- mWindowDecorViewHolder = new DesktopModeAppControlsWindowDecorationViewHolder(
- mResult.mRootView,
- mOnCaptionTouchListener,
- mOnCaptionButtonClickListener,
- mOnCaptionLongClickListener,
- mOnCaptionGenericMotionListener,
- mAppName,
- mAppIconBitmap,
- () -> {
- if (!isMaximizeMenuActive()) {
- createMaximizeMenu();
- }
- return Unit.INSTANCE;
- });
- } else {
- throw new IllegalArgumentException("Unexpected layout resource id");
- }
+ mWindowDecorViewHolder = createViewHolder();
}
Trace.beginSection("DesktopModeWindowDecoration#relayout-binding");
mWindowDecorViewHolder.bindData(mTaskInfo);
@@ -268,16 +317,18 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
closeMaximizeMenu();
}
- final boolean isFreeform =
- taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM;
- final boolean isDragResizeable = isFreeform && taskInfo.isResizeable;
- if (!isDragResizeable) {
+ updateDragResizeListener(oldDecorationSurface);
+ updateMaximizeMenu(startT);
+ Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces
+ }
+
+ private void updateDragResizeListener(SurfaceControl oldDecorationSurface) {
+ if (!isDragResizable(mTaskInfo)) {
if (!mTaskInfo.positionInParent.equals(mPositionInParent)) {
// We still want to track caption bar's exclusion region on a non-resizeable task.
updateExclusionRegion();
}
closeDragResizeListener();
- Trace.endSection(); // DesktopModeWindowDecoration#relayout
return;
}
@@ -311,15 +362,51 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
|| !mTaskInfo.positionInParent.equals(mPositionInParent)) {
updateExclusionRegion();
}
+ }
- if (isMaximizeMenuActive()) {
- if (!mTaskInfo.isVisible()) {
- closeMaximizeMenu();
- } else {
- mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(), startT);
- }
+ private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
+ final boolean isFreeform =
+ taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM;
+ return isFreeform && taskInfo.isResizeable;
+ }
+
+ private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
+ if (!isDragResizable(mTaskInfo) || !isMaximizeMenuActive()) {
+ return;
+ }
+ if (!mTaskInfo.isVisible()) {
+ closeMaximizeMenu();
+ } else {
+ mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(), startT);
+ }
+ }
+
+ private WindowDecorationViewHolder createViewHolder() {
+ if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) {
+ return new AppHandleViewHolder(
+ mResult.mRootView,
+ mOnCaptionTouchListener,
+ mOnCaptionButtonClickListener
+ );
+ } else if (mRelayoutParams.mLayoutResId
+ == R.layout.desktop_mode_app_header) {
+ loadAppInfoIfNeeded();
+ return new AppHeaderViewHolder(
+ mResult.mRootView,
+ mOnCaptionTouchListener,
+ mOnCaptionButtonClickListener,
+ mOnCaptionLongClickListener,
+ mOnCaptionGenericMotionListener,
+ mAppName,
+ mAppIconBitmap,
+ () -> {
+ if (!isMaximizeMenuActive()) {
+ createMaximizeMenu();
+ }
+ return Unit.INSTANCE;
+ });
}
- Trace.endSection(); // DesktopModeWindowDecoration#relayout
+ throw new IllegalArgumentException("Unexpected layout resource id");
}
@VisibleForTesting
@@ -331,8 +418,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
boolean shouldSetTaskPositionAndCrop) {
final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode());
final boolean isAppHeader =
- captionLayoutId == R.layout.desktop_mode_app_controls_window_decor;
- final boolean isAppHandle = captionLayoutId == R.layout.desktop_mode_focused_window_decor;
+ captionLayoutId == R.layout.desktop_mode_app_header;
+ final boolean isAppHandle = captionLayoutId == R.layout.desktop_mode_app_handle;
relayoutParams.reset();
relayoutParams.mRunningTaskInfo = taskInfo;
relayoutParams.mLayoutResId = captionLayoutId;
@@ -385,7 +472,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
// Should match the density of the task. The task may have had its density overridden
// to be different that SysUI's.
windowDecorConfig.setTo(taskInfo.configuration);
- } else if (DesktopModeStatus.isDesktopDensityOverrideSet()) {
+ } else if (DesktopModeStatus.useDesktopOverrideDensity()) {
// The task has had its density overridden, but keep using the system's density to
// layout the header.
windowDecorConfig.setTo(context.getResources().getConfiguration());
@@ -405,7 +492,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
* resource. Otherwise, return ID_NULL and caption width be set to task width.
*/
private static int getCaptionWidthId(int layoutResId) {
- if (layoutResId == R.layout.desktop_mode_focused_window_decor) {
+ if (layoutResId == R.layout.desktop_mode_app_handle) {
return R.dimen.desktop_mode_fullscreen_decor_caption_width;
}
return Resources.ID_NULL;
@@ -515,8 +602,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
private void createResizeVeilIfNeeded() {
if (mResizeVeil != null) return;
loadAppInfoIfNeeded();
- mResizeVeil = new ResizeVeil(mContext, mDisplayController, mResizeVeilBitmap, mTaskInfo,
- mTaskSurface, mSurfaceControlTransactionSupplier);
+ mResizeVeil = new ResizeVeil(mContext, mDisplayController, mResizeVeilBitmap,
+ mTaskSurface, mSurfaceControlTransactionSupplier, mTaskInfo);
}
/**
@@ -524,7 +611,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
*/
public void showResizeVeil(Rect taskBounds) {
createResizeVeilIfNeeded();
- mResizeVeil.showVeil(mTaskSurface, taskBounds);
+ mResizeVeil.showVeil(mTaskSurface, taskBounds, mTaskInfo);
}
/**
@@ -532,7 +619,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
*/
public void showResizeVeil(SurfaceControl.Transaction tx, Rect taskBounds) {
createResizeVeilIfNeeded();
- mResizeVeil.showVeil(tx, mTaskSurface, taskBounds, false /* fadeIn */);
+ mResizeVeil.showVeil(tx, mTaskSurface, taskBounds, mTaskInfo, false /* fadeIn */);
}
/**
@@ -568,7 +655,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
@Override
@NonNull
Rect calculateValidDragArea() {
- final int appTextWidth = ((DesktopModeAppControlsWindowDecorationViewHolder)
+ final int appTextWidth = ((AppHeaderViewHolder)
mWindowDecorViewHolder).getAppNameTextWidth();
final int leftButtonsWidth = loadDimensionPixelSize(mContext.getResources(),
R.dimen.desktop_mode_app_details_width_minus_text) + appTextWidth;
@@ -650,7 +737,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
/**
* Create and display handle menu window.
*/
- void createHandleMenu() {
+ void createHandleMenu(SplitScreenController splitScreenController) {
loadAppInfoIfNeeded();
mHandleMenu = new HandleMenu.Builder(this)
.setAppIcon(mAppIconBitmap)
@@ -660,6 +747,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
.setLayoutId(mRelayoutParams.mLayoutResId)
.setWindowingButtonsVisible(DesktopModeStatus.canEnterDesktopMode(mContext))
.setCaptionHeight(mResult.mCaptionHeight)
+ .setDisplayController(mDisplayController)
+ .setSplitScreenController(splitScreenController)
.build();
mWindowDecorViewHolder.onHandleMenuOpened();
mHandleMenu.show();
@@ -747,7 +836,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
*/
boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) {
if (isHandleMenuActive() || !(mWindowDecorViewHolder
- instanceof DesktopModeFocusedWindowDecorationViewHolder)) {
+ instanceof AppHandleViewHolder)) {
return false;
}
@@ -815,11 +904,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
// We want handle to remain pressed if the pointer moves outside of it during a drag.
handle.setPressed((inHandle && action == ACTION_DOWN)
|| (handle.isPressed() && action != ACTION_UP && action != ACTION_CANCEL));
- if (isHandleMenuActive()) {
+ if (isHandleMenuActive() && !isHandleMenuAboveStatusBar()) {
mHandleMenu.checkMotionEvent(ev);
}
}
+ private boolean isHandleMenuAboveStatusBar() {
+ return Flags.enableAdditionalWindowsAboveStatusBar() && !mTaskInfo.isFreeform();
+ }
+
private boolean pointInView(View v, float x, float y) {
return v != null && v.getLeft() <= x && v.getRight() >= x
&& v.getTop() <= y && v.getBottom() >= y;
@@ -831,13 +924,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
closeHandleMenu();
mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId);
disposeResizeVeil();
+ clearCurrentViewHostRunnable();
super.close();
}
private static int getDesktopModeWindowDecorLayoutId(@WindowingMode int windowingMode) {
return windowingMode == WINDOWING_MODE_FREEFORM
- ? R.layout.desktop_mode_app_controls_window_decor
- : R.layout.desktop_mode_focused_window_decor;
+ ? R.layout.desktop_mode_app_header
+ : R.layout.desktop_mode_app_handle;
}
private void updatePositionInParent() {
@@ -868,6 +962,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
return exclusionRegion;
}
+ int getCaptionX() {
+ return mResult.mCaptionX;
+ }
+
@Override
int getCaptionHeightId(@WindowingMode int windowingMode) {
return getCaptionHeightIdStatic(windowingMode);
@@ -889,20 +987,20 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
void setAnimatingTaskResize(boolean animatingTaskResize) {
- if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) return;
- ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder)
+ if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_app_handle) return;
+ ((AppHeaderViewHolder) mWindowDecorViewHolder)
.setAnimatingTaskResize(animatingTaskResize);
}
/** Called when there is a {@Link ACTION_HOVER_EXIT} on the maximize window button. */
void onMaximizeWindowHoverExit() {
- ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder)
+ ((AppHeaderViewHolder) mWindowDecorViewHolder)
.onMaximizeWindowHoverExit();
}
/** Called when there is a {@Link ACTION_HOVER_ENTER} on the maximize window button. */
void onMaximizeWindowHoverEnter() {
- ((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder)
+ ((AppHeaderViewHolder) mWindowDecorViewHolder)
.onMaximizeWindowHoverEnter();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index 82c399ad8152..d48ce536f2b3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -22,12 +22,18 @@ import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP;
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED;
+import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.view.SurfaceControl;
+import androidx.annotation.NonNull;
+
+import com.android.window.flags.Flags;
+import com.android.wm.shell.R;
import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.shared.DesktopModeStatus;
/**
* Utility class that contains logic common to classes implementing {@link DragPositioningCallback}
@@ -35,11 +41,11 @@ import com.android.wm.shell.common.DisplayController;
* and applying that change to the task bounds when applicable.
*/
public class DragPositioningCallbackUtility {
-
/**
* Determine the delta between input's current point and the input start point.
- * @param inputX current input x coordinate
- * @param inputY current input y coordinate
+ *
+ * @param inputX current input x coordinate
+ * @param inputY current input y coordinate
* @param repositionStartPoint initial input coordinate
* @return delta between these two points
*/
@@ -52,13 +58,14 @@ public class DragPositioningCallbackUtility {
/**
* Based on type of resize and delta provided, calculate the new bounds to display for this
* task.
- * @param ctrlType type of drag being performed
- * @param repositionTaskBounds the bounds the task is being repositioned to
+ *
+ * @param ctrlType type of drag being performed
+ * @param repositionTaskBounds the bounds the task is being repositioned to
* @param taskBoundsAtDragStart the bounds of the task on the first drag input event
- * @param stableBounds bounds that represent the resize limit of this task
- * @param delta difference between start input and current input in x/y coordinates
- * @param displayController task's display controller
- * @param windowDecoration window decoration of the task being dragged
+ * @param stableBounds bounds that represent the resize limit of this task
+ * @param delta difference between start input and current input in x/y
+ * coordinates
+ * @param windowDecoration window decoration of the task being dragged
* @return whether this method changed repositionTaskBounds
*/
static boolean changeBounds(int ctrlType, Rect repositionTaskBounds, Rect taskBoundsAtDragStart,
@@ -101,13 +108,15 @@ public class DragPositioningCallbackUtility {
repositionTaskBounds.bottom = (candidateBottom < stableBounds.bottom)
? candidateBottom : oldBottom;
}
- // If width or height are negative or less than the minimum width or height, revert the
+ // If width or height are negative or exceeding the width or height constraints, revert the
// respective bounds to use previous bound dimensions.
- if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) {
+ if (isExceedingWidthConstraint(repositionTaskBounds, stableBounds, displayController,
+ windowDecoration)) {
repositionTaskBounds.right = oldRight;
repositionTaskBounds.left = oldLeft;
}
- if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) {
+ if (isExceedingHeightConstraint(repositionTaskBounds, stableBounds, displayController,
+ windowDecoration)) {
repositionTaskBounds.top = oldTop;
repositionTaskBounds.bottom = oldBottom;
}
@@ -142,8 +151,9 @@ public class DragPositioningCallbackUtility {
/**
* If task bounds are outside of provided drag area, snap the bounds to be just inside the
* drag area.
+ *
* @param repositionTaskBounds bounds determined by task positioner
- * @param validDragArea the area that task must be positioned inside
+ * @param validDragArea the area that task must be positioned inside
* @return whether bounds were modified
*/
public static boolean snapTaskBoundsIfNecessary(Rect repositionTaskBounds, Rect validDragArea) {
@@ -168,30 +178,80 @@ public class DragPositioningCallbackUtility {
return result;
}
+ private static boolean isExceedingWidthConstraint(@NonNull Rect repositionTaskBounds,
+ Rect maxResizeBounds, DisplayController displayController,
+ WindowDecoration windowDecoration) {
+ // Check if width is less than the minimum width constraint.
+ if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) {
+ return true;
+ }
+ // Check if width is more than the maximum resize bounds on desktop windowing mode.
+ return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)
+ && repositionTaskBounds.width() > maxResizeBounds.width();
+ }
+
+ private static boolean isExceedingHeightConstraint(@NonNull Rect repositionTaskBounds,
+ Rect maxResizeBounds, DisplayController displayController,
+ WindowDecoration windowDecoration) {
+ // Check if height is less than the minimum height constraint.
+ if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) {
+ return true;
+ }
+ // Check if height is more than the maximum resize bounds on desktop windowing mode.
+ return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)
+ && repositionTaskBounds.height() > maxResizeBounds.height();
+ }
+
private static float getMinWidth(DisplayController displayController,
WindowDecoration windowDecoration) {
- return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinSize(displayController,
+ return windowDecoration.mTaskInfo.minWidth < 0 ? getDefaultMinWidth(displayController,
windowDecoration)
: windowDecoration.mTaskInfo.minWidth;
}
private static float getMinHeight(DisplayController displayController,
WindowDecoration windowDecoration) {
- return windowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinSize(displayController,
+ return windowDecoration.mTaskInfo.minHeight < 0 ? getDefaultMinHeight(displayController,
windowDecoration)
: windowDecoration.mTaskInfo.minHeight;
}
+ private static float getDefaultMinWidth(DisplayController displayController,
+ WindowDecoration windowDecoration) {
+ if (isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)) {
+ return WindowDecoration.loadDimensionPixelSize(
+ windowDecoration.mDecorWindowContext.getResources(),
+ R.dimen.desktop_mode_minimum_window_width);
+ }
+ return getDefaultMinSize(displayController, windowDecoration);
+ }
+
+ private static float getDefaultMinHeight(DisplayController displayController,
+ WindowDecoration windowDecoration) {
+ if (isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)) {
+ return WindowDecoration.loadDimensionPixelSize(
+ windowDecoration.mDecorWindowContext.getResources(),
+ R.dimen.desktop_mode_minimum_window_height);
+ }
+ return getDefaultMinSize(displayController, windowDecoration);
+ }
+
private static float getDefaultMinSize(DisplayController displayController,
WindowDecoration windowDecoration) {
- float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId)
+ float density = displayController.getDisplayLayout(windowDecoration.mTaskInfo.displayId)
.densityDpi() * DisplayMetrics.DENSITY_DEFAULT_SCALE;
return windowDecoration.mTaskInfo.defaultMinSize * density;
}
+ private static boolean isSizeConstraintForDesktopModeEnabled(Context context) {
+ return DesktopModeStatus.canEnterDesktopMode(context)
+ && Flags.enableDesktopWindowingSizeConstraints();
+ }
+
interface DragStartListener {
/**
* Inform the implementing class that a drag resize has started
+ *
* @param taskId id of this positioner's {@link WindowDecoration}
*/
void onDragStart(int taskId);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
index badce6e93d67..d902444d4b15 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java
@@ -16,7 +16,6 @@
package com.android.wm.shell.windowdecor;
-import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY;
@@ -29,6 +28,8 @@ import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT;
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT;
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEdgeResizePermitted;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.isEventFromTouchscreen;
import android.annotation.NonNull;
import android.content.Context;
@@ -285,6 +286,9 @@ class DragResizeInputListener implements AutoCloseable {
private boolean mShouldHandleEvents;
private int mLastCursorType = PointerIcon.TYPE_DEFAULT;
private Rect mDragStartTaskBounds;
+ // The id of the particular pointer in a MotionEvent that we are listening to for drag
+ // resize events. For example, if multiple fingers are touching the screen, then each one
+ // has a separate pointer id, but we only accept drag input from one.
private int mDragPointerId = -1;
private TaskResizeInputEventReceiver(@NonNull Context context,
@@ -389,18 +393,20 @@ class DragResizeInputListener implements AutoCloseable {
boolean result = false;
// Check if this is a touch event vs mouse event.
// Touch events are tracked in four corners. Other events are tracked in resize edges.
- boolean isTouch = isTouchEvent(e);
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
- mShouldHandleEvents = mDragResizeWindowGeometry.shouldHandleEvent(e, isTouch,
+ mShouldHandleEvents = mDragResizeWindowGeometry.shouldHandleEvent(e,
new Point() /* offset */);
if (mShouldHandleEvents) {
+ // Save the id of the pointer for this drag interaction; we will use the
+ // same pointer for all subsequent MotionEvents in this interaction.
mDragPointerId = e.getPointerId(0);
float x = e.getX(0);
float y = e.getY(0);
float rawX = e.getRawX(0);
float rawY = e.getRawY(0);
- int ctrlType = mDragResizeWindowGeometry.calculateCtrlType(isTouch, x, y);
+ final int ctrlType = mDragResizeWindowGeometry.calculateCtrlType(
+ isEventFromTouchscreen(e), isEdgeResizePermitted(e), x, y);
ProtoLog.d(WM_SHELL_DESKTOP_MODE,
"%s: Handling action down, update ctrlType to %d", TAG, ctrlType);
mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType,
@@ -420,9 +426,16 @@ class DragResizeInputListener implements AutoCloseable {
break;
}
mInputManager.pilferPointers(mInputChannel.getToken());
- int dragPointerIndex = e.findPointerIndex(mDragPointerId);
- float rawX = e.getRawX(dragPointerIndex);
- float rawY = e.getRawY(dragPointerIndex);
+ final int dragPointerIndex = e.findPointerIndex(mDragPointerId);
+ if (dragPointerIndex < 0) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ "%s: Handling action move, but ignore event due to invalid "
+ + "pointer index",
+ TAG);
+ break;
+ }
+ final float rawX = e.getRawX(dragPointerIndex);
+ final float rawY = e.getRawY(dragPointerIndex);
final Rect taskBounds = mCallback.onDragPositioningMove(rawX, rawY);
updateInputSinkRegionForDrag(taskBounds);
result = true;
@@ -431,7 +444,14 @@ class DragResizeInputListener implements AutoCloseable {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
if (mShouldHandleEvents) {
- int dragPointerIndex = e.findPointerIndex(mDragPointerId);
+ final int dragPointerIndex = e.findPointerIndex(mDragPointerId);
+ if (dragPointerIndex < 0) {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ "%s: Handling action %d, but ignore event due to invalid "
+ + "pointer index",
+ TAG, e.getActionMasked());
+ break;
+ }
final Rect taskBounds = mCallback.onDragPositioningEnd(
e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex));
// If taskBounds has changed, setGeometry will be called and update the
@@ -474,8 +494,11 @@ class DragResizeInputListener implements AutoCloseable {
private void updateCursorType(int displayId, int deviceId, int pointerId, float x,
float y) {
+ // Since we are handling cursor, we know that this is not a touchscreen event, and
+ // that edge resizing should always be allowed.
@DragPositioningCallback.CtrlType int ctrlType =
- mDragResizeWindowGeometry.calculateCtrlType(/* isTouch= */ false, x, y);
+ mDragResizeWindowGeometry.calculateCtrlType(/* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ true, x, y);
int cursorType = PointerIcon.TYPE_DEFAULT;
switch (ctrlType) {
@@ -517,9 +540,5 @@ class DragResizeInputListener implements AutoCloseable {
private boolean shouldHandleEvent(MotionEvent e, Point offset) {
return mDragResizeWindowGeometry.shouldHandleEvent(e, offset);
}
-
- private boolean isTouchEvent(MotionEvent e) {
- return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
- }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
index 4f513f0a0fd8..b5d1d4a76342 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
@@ -142,40 +142,42 @@ final class DragResizeWindowGeometry {
* Returns if this MotionEvent should be handled, based on its source and position.
*/
boolean shouldHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) {
- return shouldHandleEvent(e, isTouchEvent(e), offset);
- }
-
- /**
- * Returns if this MotionEvent should be handled, based on its source and position.
- */
- boolean shouldHandleEvent(@NonNull MotionEvent e, boolean isTouch, @NonNull Point offset) {
final float x = e.getX(0) + offset.x;
final float y = e.getY(0) + offset.y;
if (enableWindowingEdgeDragResize()) {
// First check if touch falls within a corner.
// Large corner bounds are used for course input like touch, otherwise fine bounds.
- boolean result = isTouch
+ boolean result = isEventFromTouchscreen(e)
? isInCornerBounds(mLargeTaskCorners, x, y)
: isInCornerBounds(mFineTaskCorners, x, y);
- // Check if touch falls within the edge resize handle, since edge resizing can apply
- // for any input source.
- if (!result) {
+ // Check if touch falls within the edge resize handle. Limit edge resizing to stylus and
+ // mouse input.
+ if (!result && isEdgeResizePermitted(e)) {
result = isInEdgeResizeBounds(x, y);
}
return result;
} else {
// Legacy uses only fine corners for touch, and edges only for non-touch input.
- return isTouch
+ return isEventFromTouchscreen(e)
? isInCornerBounds(mFineTaskCorners, x, y)
: isInEdgeResizeBounds(x, y);
}
}
- private boolean isTouchEvent(@NonNull MotionEvent e) {
+ static boolean isEventFromTouchscreen(@NonNull MotionEvent e) {
return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
}
+ static boolean isEdgeResizePermitted(@NonNull MotionEvent e) {
+ if (enableWindowingEdgeDragResize()) {
+ return e.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS
+ || e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
+ } else {
+ return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
+ }
+ }
+
private boolean isInCornerBounds(TaskCorners corners, float xf, float yf) {
return corners.calculateCornersCtrlType(xf, yf) != 0;
}
@@ -187,24 +189,29 @@ final class DragResizeWindowGeometry {
/**
* Returns the control type for the drag-resize, based on the touch regions and this
* MotionEvent's coordinates.
+ * @param isTouchscreen Controls the size of the corner resize regions; touchscreen events
+ * (finger & stylus) are eligible for a larger area than cursor events
+ * @param isEdgeResizePermitted Indicates if the event is eligible for falling into an edge
+ * resize region.
*/
@DragPositioningCallback.CtrlType
- int calculateCtrlType(boolean isTouch, float x, float y) {
+ int calculateCtrlType(boolean isTouchscreen, boolean isEdgeResizePermitted, float x, float y) {
if (enableWindowingEdgeDragResize()) {
// First check if touch falls within a corner.
// Large corner bounds are used for course input like touch, otherwise fine bounds.
- int ctrlType = isTouch
+ int ctrlType = isTouchscreen
? mLargeTaskCorners.calculateCornersCtrlType(x, y)
: mFineTaskCorners.calculateCornersCtrlType(x, y);
+
// Check if touch falls within the edge resize handle, since edge resizing can apply
// for any input source.
- if (ctrlType == CTRL_TYPE_UNDEFINED) {
+ if (ctrlType == CTRL_TYPE_UNDEFINED && isEdgeResizePermitted) {
ctrlType = calculateEdgeResizeCtrlType(x, y);
}
return ctrlType;
} else {
// Legacy uses only fine corners for touch, and edges only for non-touch input.
- return isTouch
+ return isTouchscreen
? mFineTaskCorners.calculateCornersCtrlType(x, y)
: calculateEdgeResizeCtrlType(x, y);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
index 65adcee1567c..df0836c1121d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java
@@ -23,6 +23,9 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager.RunningTaskInfo;
@@ -33,7 +36,9 @@ import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color;
+import android.graphics.Point;
import android.graphics.PointF;
+import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.View;
@@ -42,7 +47,15 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.window.SurfaceSyncGroup;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.window.flags.Flags;
import com.android.wm.shell.R;
+import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer;
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer;
/**
* Handle menu opened when the appropriate button is clicked on.
@@ -56,15 +69,25 @@ class HandleMenu {
private static final String TAG = "HandleMenu";
private static final boolean SHOULD_SHOW_MORE_ACTIONS_PILL = false;
private final Context mContext;
- private final WindowDecoration mParentDecor;
- private WindowDecoration.AdditionalWindow mHandleMenuWindow;
- private final PointF mHandleMenuPosition = new PointF();
+ private final DesktopModeWindowDecoration mParentDecor;
+ @VisibleForTesting
+ AdditionalViewContainer mHandleMenuViewContainer;
+ // Position of the handle menu used for laying out the handle view.
+ @VisibleForTesting
+ final PointF mHandleMenuPosition = new PointF();
+ // With the introduction of {@link AdditionalSystemViewContainer}, {@link mHandleMenuPosition}
+ // may be in a different coordinate space than the input coordinates. Therefore, we still care
+ // about the menu's coordinates relative to the display as a whole, so we need to maintain
+ // those as well.
+ final Point mGlobalMenuPosition = new Point();
private final boolean mShouldShowWindowingPill;
private final Bitmap mAppIconBitmap;
private final CharSequence mAppName;
private final View.OnClickListener mOnClickListener;
private final View.OnTouchListener mOnTouchListener;
private final RunningTaskInfo mTaskInfo;
+ private final DisplayController mDisplayController;
+ private final SplitScreenController mSplitScreenController;
private final int mLayoutResId;
private int mMarginMenuTop;
private int mMarginMenuStart;
@@ -74,12 +97,16 @@ class HandleMenu {
private HandleMenuAnimator mHandleMenuAnimator;
- HandleMenu(WindowDecoration parentDecor, int layoutResId, View.OnClickListener onClickListener,
- View.OnTouchListener onTouchListener, Bitmap appIcon, CharSequence appName,
- boolean shouldShowWindowingPill, int captionHeight) {
+ HandleMenu(DesktopModeWindowDecoration parentDecor, int layoutResId,
+ View.OnClickListener onClickListener, View.OnTouchListener onTouchListener,
+ Bitmap appIcon, CharSequence appName, DisplayController displayController,
+ SplitScreenController splitScreenController, boolean shouldShowWindowingPill,
+ int captionHeight) {
mParentDecor = parentDecor;
mContext = mParentDecor.mDecorWindowContext;
mTaskInfo = mParentDecor.mTaskInfo;
+ mDisplayController = displayController;
+ mSplitScreenController = splitScreenController;
mLayoutResId = layoutResId;
mOnClickListener = onClickListener;
mOnTouchListener = onTouchListener;
@@ -95,20 +122,27 @@ class HandleMenu {
final SurfaceSyncGroup ssg = new SurfaceSyncGroup(TAG);
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
- createHandleMenuWindow(t, ssg);
+ createHandleMenuViewContainer(t, ssg);
ssg.addTransaction(t);
ssg.markSyncReady();
setupHandleMenu();
animateHandleMenu();
}
- private void createHandleMenuWindow(SurfaceControl.Transaction t, SurfaceSyncGroup ssg) {
+ private void createHandleMenuViewContainer(SurfaceControl.Transaction t,
+ SurfaceSyncGroup ssg) {
final int x = (int) mHandleMenuPosition.x;
final int y = (int) mHandleMenuPosition.y;
- mHandleMenuWindow = mParentDecor.addWindow(
- R.layout.desktop_mode_window_decor_handle_menu, "Handle Menu",
- t, ssg, x, y, mMenuWidth, mMenuHeight);
- final View handleMenuView = mHandleMenuWindow.mWindowViewHost.getView();
+ if (!mTaskInfo.isFreeform() && Flags.enableAdditionalWindowsAboveStatusBar()) {
+ mHandleMenuViewContainer = new AdditionalSystemViewContainer(mContext,
+ R.layout.desktop_mode_window_decor_handle_menu, mTaskInfo.taskId,
+ x, y, mMenuWidth, mMenuHeight);
+ } else {
+ mHandleMenuViewContainer = mParentDecor.addWindow(
+ R.layout.desktop_mode_window_decor_handle_menu, "Handle Menu",
+ t, ssg, x, y, mMenuWidth, mMenuHeight);
+ }
+ final View handleMenuView = mHandleMenuViewContainer.getView();
mHandleMenuAnimator = new HandleMenuAnimator(handleMenuView, mMenuWidth, mCaptionHeight);
}
@@ -129,7 +163,7 @@ class HandleMenu {
* pill.
*/
private void setupHandleMenu() {
- final View handleMenu = mHandleMenuWindow.mWindowViewHost.getView();
+ final View handleMenu = mHandleMenuViewContainer.getView();
handleMenu.setOnTouchListener(mOnTouchListener);
setupAppInfoPill(handleMenu);
if (mShouldShowWindowingPill) {
@@ -147,6 +181,7 @@ class HandleMenu {
final ImageView appIcon = handleMenu.findViewById(R.id.application_icon);
final TextView appName = handleMenu.findViewById(R.id.application_name);
collapseBtn.setOnClickListener(mOnClickListener);
+ collapseBtn.setTaskInfo(mTaskInfo);
appIcon.setImageBitmap(mAppIconBitmap);
appName.setText(mAppName);
}
@@ -215,33 +250,69 @@ class HandleMenu {
* Updates handle menu's position variables to reflect its next position.
*/
private void updateHandleMenuPillPositions() {
- final int menuX, menuY;
- final int captionWidth = mTaskInfo.getConfiguration()
- .windowConfiguration.getBounds().width();
- if (mLayoutResId
- == R.layout.desktop_mode_app_controls_window_decor) {
- // Align the handle menu to the left of the caption.
+ int menuX;
+ final int menuY;
+ final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds();
+ updateGlobalMenuPosition(taskBounds);
+ if (mLayoutResId == R.layout.desktop_mode_app_header) {
+ // Align the handle menu to the left side of the caption.
menuX = mMarginMenuStart;
menuY = mMarginMenuTop;
} else {
- // Position the handle menu at the center of the caption.
- menuX = (captionWidth / 2) - (mMenuWidth / 2);
- menuY = mMarginMenuStart;
+ if (Flags.enableAdditionalWindowsAboveStatusBar()) {
+ // In a focused decor, we use global coordinates for handle menu. Therefore we
+ // need to account for other factors like split stage and menu/handle width to
+ // center the menu.
+ final DisplayLayout layout = mDisplayController
+ .getDisplayLayout(mTaskInfo.displayId);
+ menuX = mGlobalMenuPosition.x + ((mMenuWidth - layout.width()) / 2);
+ menuY = mGlobalMenuPosition.y + ((mMenuHeight - layout.height()) / 2);
+ } else {
+ menuX = (taskBounds.width() / 2) - (mMenuWidth / 2);
+ menuY = mMarginMenuTop;
+ }
}
-
// Handle Menu position setup.
mHandleMenuPosition.set(menuX, menuY);
+ }
+ private void updateGlobalMenuPosition(Rect taskBounds) {
+ if (mTaskInfo.isFreeform()) {
+ mGlobalMenuPosition.set(taskBounds.left + mMarginMenuStart,
+ taskBounds.top + mMarginMenuTop);
+ } else if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+ mGlobalMenuPosition.set(
+ (taskBounds.width() / 2) - (mMenuWidth / 2) + mMarginMenuStart,
+ mMarginMenuTop
+ );
+ } else if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
+ final int splitPosition = mSplitScreenController.getSplitPosition(mTaskInfo.taskId);
+ final Rect leftOrTopStageBounds = new Rect();
+ final Rect rightOrBottomStageBounds = new Rect();
+ mSplitScreenController.getStageBounds(leftOrTopStageBounds,
+ rightOrBottomStageBounds);
+ // TODO(b/343561161): This needs to be calculated differently if the task is in
+ // top/bottom split.
+ if (splitPosition == SPLIT_POSITION_BOTTOM_OR_RIGHT) {
+ mGlobalMenuPosition.set(leftOrTopStageBounds.width()
+ + (rightOrBottomStageBounds.width() / 2)
+ - (mMenuWidth / 2) + mMarginMenuStart,
+ mMarginMenuTop);
+ } else if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
+ mGlobalMenuPosition.set((leftOrTopStageBounds.width() / 2)
+ - (mMenuWidth / 2) + mMarginMenuStart,
+ mMarginMenuTop);
+ }
+ }
}
/**
* Update pill layout, in case task changes have caused positioning to change.
*/
void relayout(SurfaceControl.Transaction t) {
- if (mHandleMenuWindow != null) {
+ if (mHandleMenuViewContainer != null) {
updateHandleMenuPillPositions();
- t.setPosition(mHandleMenuWindow.mWindowSurface,
- mHandleMenuPosition.x, mHandleMenuPosition.y);
+ mHandleMenuViewContainer.setPosition(t, mHandleMenuPosition.x, mHandleMenuPosition.y);
}
}
@@ -253,7 +324,9 @@ class HandleMenu {
* @param ev the MotionEvent to compare against.
*/
void checkMotionEvent(MotionEvent ev) {
- final View handleMenu = mHandleMenuWindow.mWindowViewHost.getView();
+ // If the menu view is above status bar, we can let the views handle input directly.
+ if (isViewAboveStatusBar()) return;
+ final View handleMenu = mHandleMenuViewContainer.getView();
final HandleMenuImageButton collapse = handleMenu.findViewById(R.id.collapse_menu_button);
final PointF inputPoint = translateInputToLocalSpace(ev);
final boolean inputInCollapseButton = pointInView(collapse, inputPoint.x, inputPoint.y);
@@ -265,6 +338,11 @@ class HandleMenu {
}
}
+ private boolean isViewAboveStatusBar() {
+ return Flags.enableAdditionalWindowsAboveStatusBar()
+ && !mTaskInfo.isFreeform();
+ }
+
// Translate the input point from display coordinates to the same space as the handle menu.
private PointF translateInputToLocalSpace(MotionEvent ev) {
return new PointF(ev.getX() - mHandleMenuPosition.x,
@@ -280,10 +358,33 @@ class HandleMenu {
*/
boolean isValidMenuInput(PointF inputPoint) {
if (!viewsLaidOut()) return true;
- return pointInView(
- mHandleMenuWindow.mWindowViewHost.getView(),
- inputPoint.x - mHandleMenuPosition.x,
- inputPoint.y - mHandleMenuPosition.y);
+ if (!isViewAboveStatusBar()) {
+ return pointInView(
+ mHandleMenuViewContainer.getView(),
+ inputPoint.x - mHandleMenuPosition.x,
+ inputPoint.y - mHandleMenuPosition.y);
+ } else {
+ // Handle menu exists in a different coordinate space when added to WindowManager.
+ // Therefore we must compare the provided input coordinates to global menu coordinates.
+ // This includes factoring for split stage as input coordinates are relative to split
+ // stage position, not relative to the display as a whole.
+ PointF inputRelativeToMenu = new PointF(
+ inputPoint.x - mGlobalMenuPosition.x,
+ inputPoint.y - mGlobalMenuPosition.y
+ );
+ if (mSplitScreenController.getSplitPosition(mTaskInfo.taskId)
+ == SPLIT_POSITION_BOTTOM_OR_RIGHT) {
+ // TODO(b/343561161): This also needs to be calculated differently if
+ // the task is in top/bottom split.
+ Rect leftStageBounds = new Rect();
+ mSplitScreenController.getStageBounds(leftStageBounds, new Rect());
+ inputRelativeToMenu.x += leftStageBounds.width();
+ }
+ return pointInView(
+ mHandleMenuViewContainer.getView(),
+ inputRelativeToMenu.x,
+ inputRelativeToMenu.y);
+ }
}
private boolean pointInView(View v, float x, float y) {
@@ -295,7 +396,7 @@ class HandleMenu {
* Check if the views for handle menu can be seen.
*/
private boolean viewsLaidOut() {
- return mHandleMenuWindow.mWindowViewHost.getView().isLaidOut();
+ return mHandleMenuViewContainer.getView().isLaidOut();
}
private void loadHandleMenuDimensions() {
@@ -334,8 +435,8 @@ class HandleMenu {
void close() {
final Runnable after = () -> {
- mHandleMenuWindow.releaseView();
- mHandleMenuWindow = null;
+ mHandleMenuViewContainer.releaseView();
+ mHandleMenuViewContainer = null;
};
if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
|| mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
@@ -346,7 +447,7 @@ class HandleMenu {
}
static final class Builder {
- private final WindowDecoration mParent;
+ private final DesktopModeWindowDecoration mParent;
private CharSequence mName;
private Bitmap mAppIcon;
private View.OnClickListener mOnClickListener;
@@ -354,9 +455,10 @@ class HandleMenu {
private int mLayoutId;
private boolean mShowWindowingPill;
private int mCaptionHeight;
+ private DisplayController mDisplayController;
+ private SplitScreenController mSplitScreenController;
-
- Builder(@NonNull WindowDecoration parent) {
+ Builder(@NonNull DesktopModeWindowDecoration parent) {
mParent = parent;
}
@@ -395,9 +497,20 @@ class HandleMenu {
return this;
}
+ Builder setDisplayController(DisplayController displayController) {
+ mDisplayController = displayController;
+ return this;
+ }
+
+ Builder setSplitScreenController(SplitScreenController splitScreenController) {
+ mSplitScreenController = splitScreenController;
+ return this;
+ }
+
HandleMenu build() {
- return new HandleMenu(mParent, mLayoutId, mOnClickListener, mOnTouchListener,
- mAppIcon, mName, mShowWindowingPill, mCaptionHeight);
+ return new HandleMenu(mParent, mLayoutId, mOnClickListener,
+ mOnTouchListener, mAppIcon, mName, mDisplayController, mSplitScreenController,
+ mShowWindowingPill, mCaptionHeight);
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt
index 7898567b70e9..18757ef6ff40 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt
@@ -15,6 +15,10 @@
*/
package com.android.wm.shell.windowdecor
+import android.app.ActivityManager.RunningTaskInfo
+import com.android.window.flags.Flags
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
@@ -25,10 +29,20 @@ import android.widget.ImageButton
* This is due to the hover events being handled by [DesktopModeWindowDecorViewModel]
* in order to take the status bar layer into account. Handling it in both classes results in a
* flicker when the hover moves from outside to inside status bar layer.
+ * TODO(b/342229481): Remove this and all uses of it once [AdditionalSystemViewContainer] is no longer
+ * guarded by a flag.
*/
-class HandleMenuImageButton(context: Context?, attrs: AttributeSet?) :
- ImageButton(context, attrs) {
+class HandleMenuImageButton(
+ context: Context?,
+ attrs: AttributeSet?
+) : ImageButton(context, attrs) {
+ lateinit var taskInfo: RunningTaskInfo
+
override fun onHoverEvent(motionEvent: MotionEvent): Boolean {
- return false
+ if (Flags.enableAdditionalWindowsAboveStatusBar() || taskInfo.isFreeform) {
+ return super.onHoverEvent(motionEvent)
+ } else {
+ return false
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index 22f0adc42f5d..0470367015ea 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
@@ -19,17 +19,28 @@ package com.android.wm.shell.windowdecor
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
+import android.annotation.ColorInt
import android.annotation.IdRes
import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
+import android.content.res.ColorStateList
import android.content.res.Resources
+import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.PointF
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.LayerDrawable
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.StateListDrawable
+import android.graphics.drawable.shapes.RoundRectShape
+import android.util.StateSet
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.SurfaceControl
import android.view.SurfaceControl.Transaction
import android.view.SurfaceControlViewHost
+import android.view.View
import android.view.View.OnClickListener
import android.view.View.OnGenericMotionListener
import android.view.View.OnTouchListener
@@ -39,18 +50,21 @@ import android.view.View.TRANSLATION_Z
import android.view.WindowManager
import android.view.WindowlessWindowManager
import android.widget.Button
-import android.widget.FrameLayout
-import android.widget.LinearLayout
import android.widget.TextView
import android.window.TaskConstants
-import androidx.core.content.withStyledAttributes
-import com.android.internal.R.attr.colorAccentPrimary
+import androidx.compose.material3.ColorScheme
+import androidx.compose.ui.graphics.toArgb
+import androidx.core.animation.addListener
import com.android.wm.shell.R
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.SyncTransactionQueue
-import com.android.wm.shell.windowdecor.WindowDecoration.AdditionalWindow
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer
+import com.android.wm.shell.windowdecor.common.DecorThemeUtil
+import com.android.wm.shell.windowdecor.common.OPACITY_12
+import com.android.wm.shell.windowdecor.common.OPACITY_40
+import com.android.wm.shell.windowdecor.common.withAlpha
import java.util.function.Supplier
@@ -70,10 +84,10 @@ class MaximizeMenu(
private val menuPosition: PointF,
private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() }
) {
- private var maximizeMenu: AdditionalWindow? = null
+ private var maximizeMenu: AdditionalViewHostViewContainer? = null
+ private var maximizeMenuView: MaximizeMenuView? = null
private lateinit var viewHost: SurfaceControlViewHost
private lateinit var leash: SurfaceControl
- private val openMenuAnimatorSet = AnimatorSet()
private val cornerRadius = loadDimensionPixelSize(
R.dimen.desktop_mode_maximize_menu_corner_radius
).toFloat()
@@ -81,12 +95,6 @@ class MaximizeMenu(
private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height)
private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding)
- private lateinit var snapRightButton: Button
- private lateinit var snapLeftButton: Button
- private lateinit var maximizeButton: Button
- private lateinit var maximizeButtonLayout: FrameLayout
- private lateinit var snapButtonsLayout: LinearLayout
-
/** Position the menu relative to the caption's position. */
fun positionMenu(position: PointF, t: Transaction) {
menuPosition.set(position)
@@ -97,24 +105,20 @@ class MaximizeMenu(
fun show() {
if (maximizeMenu != null) return
createMaximizeMenu()
- setupMaximizeMenu()
- animateOpenMenu()
+ maximizeMenuView?.animateOpenMenu()
}
/** Closes the maximize window and releases its view. */
fun close() {
- openMenuAnimatorSet.cancel()
+ maximizeMenuView?.cancelAnimation()
maximizeMenu?.releaseView()
maximizeMenu = null
+ maximizeMenuView = null
}
/** Create a maximize menu that is attached to the display area. */
private fun createMaximizeMenu() {
val t = transactionSupplier.get()
- val v = LayoutInflater.from(decorWindowContext).inflate(
- R.layout.desktop_mode_window_decor_maximize_menu,
- null // Root
- )
val builder = SurfaceControl.Builder()
rootTdaOrganizer.attachToDisplayArea(taskInfo.displayId, builder)
leash = builder
@@ -138,14 +142,25 @@ class MaximizeMenu(
viewHost = SurfaceControlViewHost(decorWindowContext,
displayController.getDisplay(taskInfo.displayId), windowManager,
"MaximizeMenu")
- viewHost.setView(v, lp)
+ maximizeMenuView = MaximizeMenuView(
+ context = decorWindowContext,
+ menuHeight = menuHeight,
+ menuPadding = menuPadding,
+ onClickListener = onClickListener,
+ onTouchListener = onTouchListener,
+ onGenericMotionListener = onGenericMotionListener,
+ ).also { menuView ->
+ menuView.bind(taskInfo)
+ viewHost.setView(menuView.rootView, lp)
+ }
// Bring menu to front when open
t.setLayer(leash, TaskConstants.TASK_CHILD_LAYER_FLOATING_MENU)
.setPosition(leash, menuPosition.x, menuPosition.y)
.setCornerRadius(leash, cornerRadius)
.show(leash)
- maximizeMenu = AdditionalWindow(leash, viewHost, transactionSupplier)
+ maximizeMenu =
+ AdditionalViewHostViewContainer(leash, viewHost, transactionSupplier)
syncQueue.runInSync { transaction ->
transaction.merge(t)
@@ -153,77 +168,6 @@ class MaximizeMenu(
}
}
- private fun animateOpenMenu() {
- val viewHost = maximizeMenu?.mWindowViewHost
- val maximizeMenuView = viewHost?.view ?: return
- val maximizeWindowText = maximizeMenuView.requireViewById<TextView>(
- R.id.maximize_menu_maximize_window_text)
- val snapWindowText = maximizeMenuView.requireViewById<TextView>(
- R.id.maximize_menu_snap_window_text)
-
- openMenuAnimatorSet.playTogether(
- ObjectAnimator.ofFloat(maximizeMenuView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f)
- .apply {
- duration = MENU_HEIGHT_ANIMATION_DURATION_MS
- interpolator = EMPHASIZED_DECELERATE
- },
- ValueAnimator.ofFloat(STARTING_MENU_HEIGHT_SCALE, 1f)
- .apply {
- duration = MENU_HEIGHT_ANIMATION_DURATION_MS
- interpolator = EMPHASIZED_DECELERATE
- addUpdateListener {
- // Animate padding so that controls stay pinned to the bottom of
- // the menu.
- val value = animatedValue as Float
- val topPadding = menuPadding -
- ((1 - value) * menuHeight).toInt()
- maximizeMenuView.setPadding(menuPadding, topPadding,
- menuPadding, menuPadding)
- }
- },
- ValueAnimator.ofFloat(1 / STARTING_MENU_HEIGHT_SCALE, 1f).apply {
- duration = MENU_HEIGHT_ANIMATION_DURATION_MS
- interpolator = EMPHASIZED_DECELERATE
- addUpdateListener {
- // Scale up the children of the maximize menu so that the menu
- // scale is cancelled out and only the background is scaled.
- val value = animatedValue as Float
- maximizeButtonLayout.scaleY = value
- snapButtonsLayout.scaleY = value
- maximizeWindowText.scaleY = value
- snapWindowText.scaleY = value
- }
- },
- ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Y,
- (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply {
- duration = MENU_HEIGHT_ANIMATION_DURATION_MS
- interpolator = EMPHASIZED_DECELERATE
- },
- ObjectAnimator.ofInt(maximizeMenuView.background, "alpha",
- MAX_DRAWABLE_ALPHA_VALUE).apply {
- duration = ALPHA_ANIMATION_DURATION_MS
- },
- ValueAnimator.ofFloat(0f, 1f)
- .apply {
- duration = ALPHA_ANIMATION_DURATION_MS
- startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
- addUpdateListener {
- val value = animatedValue as Float
- maximizeButtonLayout.alpha = value
- snapButtonsLayout.alpha = value
- maximizeWindowText.alpha = value
- snapWindowText.alpha = value
- }
- },
- ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Z, MENU_Z_TRANSLATION)
- .apply {
- duration = ELEVATION_ANIMATION_DURATION_MS
- startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
- }
- )
- openMenuAnimatorSet.start()
- }
-
private fun loadDimensionPixelSize(resourceId: Int): Int {
return if (resourceId == Resources.ID_NULL) {
0
@@ -232,31 +176,6 @@ class MaximizeMenu(
}
}
- private fun setupMaximizeMenu() {
- val maximizeMenuView = maximizeMenu?.mWindowViewHost?.view ?: return
-
- maximizeMenuView.setOnGenericMotionListener(onGenericMotionListener)
- maximizeMenuView.setOnTouchListener(onTouchListener)
-
- maximizeButtonLayout = maximizeMenuView.requireViewById(
- R.id.maximize_menu_maximize_button_layout)
-
- maximizeButton = maximizeMenuView.requireViewById(R.id.maximize_menu_maximize_button)
- maximizeButton.setOnClickListener(onClickListener)
- maximizeButton.setOnGenericMotionListener(onGenericMotionListener)
-
- snapRightButton = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_right_button)
- snapRightButton.setOnClickListener(onClickListener)
- snapRightButton.setOnGenericMotionListener(onGenericMotionListener)
-
- snapLeftButton = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_left_button)
- snapLeftButton.setOnClickListener(onClickListener)
- snapLeftButton.setOnGenericMotionListener(onGenericMotionListener)
-
- snapButtonsLayout = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_menu_layout)
- snapButtonsLayout.setOnGenericMotionListener(onGenericMotionListener)
- }
-
/**
* A valid menu input is one of the following:
* An input that happens in the menu views.
@@ -275,68 +194,438 @@ class MaximizeMenu(
* Check if the views for maximize menu can be seen.
*/
private fun viewsLaidOut(): Boolean {
- return maximizeMenu?.mWindowViewHost?.view?.isLaidOut ?: false
+ return maximizeMenu?.view?.isLaidOut ?: false
}
+ /**
+ * Called when a [MotionEvent.ACTION_HOVER_ENTER] is triggered on any of the menu's views.
+ *
+ * TODO(b/346440693): this is only needed for the left/right snap options that don't support
+ * selector states to manage its hover state. Look into whether that can be added to avoid
+ * manually tracking hover enter/exit motion events. Also because those button colors/states
+ * aren't updating correctly for pressed, focused and selected states.
+ * See also [onMaximizeMenuHoverMove] and [onMaximizeMenuHoverExit].
+ */
fun onMaximizeMenuHoverEnter(viewId: Int, ev: MotionEvent) {
setSnapButtonsColorOnHover(viewId, ev)
}
+ /** Called when a [MotionEvent.ACTION_HOVER_MOVE] is triggered on any of the menu's views. */
fun onMaximizeMenuHoverMove(viewId: Int, ev: MotionEvent) {
setSnapButtonsColorOnHover(viewId, ev)
}
+ /** Called when a [MotionEvent.ACTION_HOVER_EXIT] is triggered on any of the menu's views. */
fun onMaximizeMenuHoverExit(id: Int, ev: MotionEvent) {
- val inSnapMenuBounds = ev.x >= 0 && ev.x <= snapButtonsLayout.width &&
- ev.y >= 0 && ev.y <= snapButtonsLayout.height
- val colorList = decorWindowContext.getColorStateList(
- R.color.desktop_mode_maximize_menu_button_color_selector)
-
- if (id == R.id.maximize_menu_maximize_button) {
- maximizeButton.background?.setTintList(colorList)
- maximizeButtonLayout.setBackgroundResource(
- R.drawable.desktop_mode_maximize_menu_layout_background)
- } else if (id == R.id.maximize_menu_snap_menu_layout && !inSnapMenuBounds) {
+ val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return
+ val snapOptionsHeight = maximizeMenuView?.snapOptionsHeight ?: return
+ val inSnapMenuBounds = ev.x >= 0 && ev.x <= snapOptionsWidth &&
+ ev.y >= 0 && ev.y <= snapOptionsHeight
+
+ if (id == R.id.maximize_menu_snap_menu_layout && !inSnapMenuBounds) {
// After exiting the snap menu layout area, checks to see that user is not still
// hovering within the snap menu layout bounds which would indicate that the user is
// hovering over a snap button within the snap menu layout rather than having exited.
- snapLeftButton.background?.setTintList(colorList)
- snapLeftButton.background?.alpha = 255
- snapRightButton.background?.setTintList(colorList)
- snapRightButton.background?.alpha = 255
- snapButtonsLayout.setBackgroundResource(
- R.drawable.desktop_mode_maximize_menu_layout_background)
+ maximizeMenuView?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.NONE)
}
}
private fun setSnapButtonsColorOnHover(viewId: Int, ev: MotionEvent) {
- decorWindowContext.withStyledAttributes(null, intArrayOf(colorAccentPrimary), 0, 0) {
- val materialColor = getColor(0, 0)
- val snapMenuCenter = snapButtonsLayout.width / 2
- if (viewId == R.id.maximize_menu_maximize_button) {
- // Highlight snap maximize window button
- maximizeButton.background?.setTint(materialColor)
- maximizeButtonLayout.setBackgroundResource(
- R.drawable.desktop_mode_maximize_menu_layout_background_on_hover)
- } else if (viewId == R.id.maximize_menu_snap_left_button ||
- (viewId == R.id.maximize_menu_snap_menu_layout && ev.x <= snapMenuCenter)) {
- // Highlight snap left button
- snapRightButton.background?.setTint(materialColor)
- snapLeftButton.background?.setTint(materialColor)
- snapButtonsLayout.setBackgroundResource(
- R.drawable.desktop_mode_maximize_menu_layout_background_on_hover)
- snapRightButton.background?.alpha = 102
- snapLeftButton.background?.alpha = 255
- } else if (viewId == R.id.maximize_menu_snap_right_button ||
- (viewId == R.id.maximize_menu_snap_menu_layout && ev.x > snapMenuCenter)) {
- // Highlight snap right button
- snapRightButton.background?.setTint(materialColor)
- snapLeftButton.background?.setTint(materialColor)
- snapButtonsLayout.setBackgroundResource(
- R.drawable.desktop_mode_maximize_menu_layout_background_on_hover)
- snapRightButton.background?.alpha = 255
- snapLeftButton.background?.alpha = 102
+ val snapOptionsWidth = maximizeMenuView?.snapOptionsWidth ?: return
+ val snapMenuCenter = snapOptionsWidth / 2
+ when {
+ viewId == R.id.maximize_menu_snap_left_button ||
+ (viewId == R.id.maximize_menu_snap_menu_layout && ev.x <= snapMenuCenter) -> {
+ maximizeMenuView
+ ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.LEFT)
}
+ viewId == R.id.maximize_menu_snap_right_button ||
+ (viewId == R.id.maximize_menu_snap_menu_layout && ev.x > snapMenuCenter) -> {
+ maximizeMenuView
+ ?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.RIGHT)
+ }
+ }
+ }
+
+ /**
+ * The view within the Maximize Menu, presents maximize, restore and snap-to-side options for
+ * resizing a Task.
+ */
+ class MaximizeMenuView(
+ context: Context,
+ private val menuHeight: Int,
+ private val menuPadding: Int,
+ onClickListener: OnClickListener,
+ onTouchListener: OnTouchListener,
+ onGenericMotionListener: OnGenericMotionListener,
+ ) {
+ val rootView: View = LayoutInflater.from(context)
+ .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */)
+ private val maximizeText =
+ requireViewById(R.id.maximize_menu_maximize_window_text) as TextView
+ private val maximizeButton =
+ requireViewById(R.id.maximize_menu_maximize_button) as Button
+ private val snapWindowText =
+ requireViewById(R.id.maximize_menu_snap_window_text) as TextView
+ private val snapRightButton =
+ requireViewById(R.id.maximize_menu_snap_right_button) as Button
+ private val snapLeftButton =
+ requireViewById(R.id.maximize_menu_snap_left_button) as Button
+ private val snapButtonsLayout =
+ requireViewById(R.id.maximize_menu_snap_menu_layout)
+
+ private val decorThemeUtil = DecorThemeUtil(context)
+
+ private val outlineRadius = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_radius)
+ private val outlineStroke = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_stroke)
+ private val fillPadding = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_padding)
+ private val fillRadius = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius)
+
+ private val openMenuAnimatorSet = AnimatorSet()
+ private lateinit var taskInfo: RunningTaskInfo
+ private lateinit var style: MenuStyle
+
+ /** The width of the snap menu option view, including both left and right snaps. */
+ val snapOptionsWidth: Int
+ get() = snapButtonsLayout.width
+ /** The height of the snap menu option view, including both left and right snaps .*/
+ val snapOptionsHeight: Int
+ get() = snapButtonsLayout.height
+
+ init {
+ // TODO(b/346441962): encapsulate menu hover enter/exit logic inside this class and
+ // expose only what is actually relevant to outside classes so that specific checks
+ // against resource IDs aren't needed outside this class.
+ rootView.setOnGenericMotionListener(onGenericMotionListener)
+ rootView.setOnTouchListener(onTouchListener)
+ maximizeButton.setOnClickListener(onClickListener)
+ maximizeButton.setOnGenericMotionListener(onGenericMotionListener)
+ snapRightButton.setOnClickListener(onClickListener)
+ snapRightButton.setOnGenericMotionListener(onGenericMotionListener)
+ snapLeftButton.setOnClickListener(onClickListener)
+ snapLeftButton.setOnGenericMotionListener(onGenericMotionListener)
+ snapButtonsLayout.setOnGenericMotionListener(onGenericMotionListener)
+
+ // To prevent aliasing.
+ maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+ maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+ }
+
+ /** Bind the menu views to the new [RunningTaskInfo] data. */
+ fun bind(taskInfo: RunningTaskInfo) {
+ this.taskInfo = taskInfo
+ this.style = calculateMenuStyle(taskInfo)
+
+ rootView.background.setTint(style.backgroundColor)
+
+ // Maximize option.
+ maximizeButton.background = style.maximizeOption.drawable
+ maximizeText.setTextColor(style.textColor)
+
+ // Snap options.
+ snapWindowText.setTextColor(style.textColor)
+ updateSplitSnapSelection(SnapToHalfSelection.NONE)
+ }
+
+ /** Animate the opening of the menu */
+ fun animateOpenMenu() {
+ maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null)
+ maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null)
+ openMenuAnimatorSet.playTogether(
+ ObjectAnimator.ofFloat(rootView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f)
+ .apply {
+ duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+ interpolator = EMPHASIZED_DECELERATE
+ },
+ ValueAnimator.ofFloat(STARTING_MENU_HEIGHT_SCALE, 1f)
+ .apply {
+ duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+ interpolator = EMPHASIZED_DECELERATE
+ addUpdateListener {
+ // Animate padding so that controls stay pinned to the bottom of
+ // the menu.
+ val value = animatedValue as Float
+ val topPadding = menuPadding -
+ ((1 - value) * menuHeight).toInt()
+ rootView.setPadding(menuPadding, topPadding,
+ menuPadding, menuPadding)
+ }
+ },
+ ValueAnimator.ofFloat(1 / STARTING_MENU_HEIGHT_SCALE, 1f).apply {
+ duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+ interpolator = EMPHASIZED_DECELERATE
+ addUpdateListener {
+ // Scale up the children of the maximize menu so that the menu
+ // scale is cancelled out and only the background is scaled.
+ val value = animatedValue as Float
+ maximizeButton.scaleY = value
+ snapButtonsLayout.scaleY = value
+ maximizeText.scaleY = value
+ snapWindowText.scaleY = value
+ }
+ },
+ ObjectAnimator.ofFloat(rootView, TRANSLATION_Y,
+ (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply {
+ duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+ interpolator = EMPHASIZED_DECELERATE
+ },
+ ObjectAnimator.ofInt(rootView.background, "alpha",
+ MAX_DRAWABLE_ALPHA_VALUE).apply {
+ duration = ALPHA_ANIMATION_DURATION_MS
+ },
+ ValueAnimator.ofFloat(0f, 1f)
+ .apply {
+ duration = ALPHA_ANIMATION_DURATION_MS
+ startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
+ addUpdateListener {
+ val value = animatedValue as Float
+ maximizeButton.alpha = value
+ snapButtonsLayout.alpha = value
+ maximizeText.alpha = value
+ snapWindowText.alpha = value
+ }
+ },
+ ObjectAnimator.ofFloat(rootView, TRANSLATION_Z, MENU_Z_TRANSLATION)
+ .apply {
+ duration = ELEVATION_ANIMATION_DURATION_MS
+ startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
+ }
+ )
+ openMenuAnimatorSet.addListener(
+ onEnd = {
+ maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+ maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
+ }
+ )
+ openMenuAnimatorSet.start()
+ }
+
+ /** Cancel the open menu animation. */
+ fun cancelAnimation() {
+ openMenuAnimatorSet.cancel()
+ }
+
+ /** Update the view state to a new snap to half selection. */
+ fun updateSplitSnapSelection(selection: SnapToHalfSelection) {
+ when (selection) {
+ SnapToHalfSelection.NONE -> deactivateSnapOptions()
+ SnapToHalfSelection.LEFT -> activateSnapOption(activateLeft = true)
+ SnapToHalfSelection.RIGHT -> activateSnapOption(activateLeft = false)
+ }
+ }
+
+ private fun calculateMenuStyle(taskInfo: RunningTaskInfo): MenuStyle {
+ val colorScheme = decorThemeUtil.getColorScheme(taskInfo)
+ val menuBackgroundColor = colorScheme.surfaceContainerLow.toArgb()
+ return MenuStyle(
+ backgroundColor = menuBackgroundColor,
+ textColor = colorScheme.onSurface.toArgb(),
+ maximizeOption = MenuStyle.MaximizeOption(
+ drawable = createMaximizeDrawable(menuBackgroundColor, colorScheme)
+ ),
+ snapOptions = MenuStyle.SnapOptions(
+ inactiveSnapSideColor = colorScheme.outlineVariant.toArgb(),
+ semiActiveSnapSideColor = colorScheme.primary.toArgb().withAlpha(OPACITY_40),
+ activeSnapSideColor = colorScheme.primary.toArgb(),
+ inactiveStrokeColor = colorScheme.outlineVariant.toArgb(),
+ activeStrokeColor = colorScheme.primary.toArgb(),
+ inactiveBackgroundColor = menuBackgroundColor,
+ activeBackgroundColor = colorScheme.primary.toArgb().withAlpha(OPACITY_12)
+ ),
+ )
+ }
+
+ private fun deactivateSnapOptions() {
+ // TODO(b/346440693): the background/colorStateList set on these buttons is overridden
+ // to a static resource & color on manually tracked hover events, which defeats the
+ // point of state lists and selector states. Look into whether changing that is
+ // possible, similar to the maximize option. Also to include support for the
+ // semi-active state (when the "other" snap option is selected).
+ val snapSideColorList = ColorStateList(
+ arrayOf(
+ intArrayOf(android.R.attr.state_pressed),
+ intArrayOf(android.R.attr.state_focused),
+ intArrayOf(android.R.attr.state_selected),
+ intArrayOf(),
+ ),
+ intArrayOf(
+ style.snapOptions.activeSnapSideColor,
+ style.snapOptions.activeSnapSideColor,
+ style.snapOptions.activeSnapSideColor,
+ style.snapOptions.inactiveSnapSideColor
+ )
+ )
+ snapLeftButton.background?.setTintList(snapSideColorList)
+ snapRightButton.background?.setTintList(snapSideColorList)
+ with (snapButtonsLayout) {
+ setBackgroundResource(R.drawable.desktop_mode_maximize_menu_layout_background)
+ (background as GradientDrawable).apply {
+ setColor(style.snapOptions.inactiveBackgroundColor)
+ setStroke(outlineStroke, style.snapOptions.inactiveStrokeColor)
+ }
+ }
+ }
+
+ private fun activateSnapOption(activateLeft: Boolean) {
+ // Regardless of which side is active, the background of the snap options layout (that
+ // includes both sides) is considered "active".
+ with (snapButtonsLayout) {
+ setBackgroundResource(
+ R.drawable.desktop_mode_maximize_menu_layout_background_on_hover)
+ (background as GradientDrawable).apply {
+ setColor(style.snapOptions.activeBackgroundColor)
+ setStroke(outlineStroke, style.snapOptions.activeStrokeColor)
+ }
+ }
+ if (activateLeft) {
+ // Highlight snap left button, partially highlight the other side.
+ snapLeftButton.background.setTint(style.snapOptions.activeSnapSideColor)
+ snapRightButton.background.setTint(style.snapOptions.semiActiveSnapSideColor)
+ } else {
+ // Highlight snap right button, partially highlight the other side.
+ snapRightButton.background.setTint(style.snapOptions.activeSnapSideColor)
+ snapLeftButton.background.setTint(style.snapOptions.semiActiveSnapSideColor)
+ }
+ }
+
+ private fun createMaximizeDrawable(
+ @ColorInt menuBackgroundColor: Int,
+ colorScheme: ColorScheme
+ ): StateListDrawable {
+ val activeStrokeAndFill = colorScheme.primary.toArgb()
+ val activeBackground = colorScheme.primary.toArgb().withAlpha(OPACITY_12)
+ val activeDrawable = createMaximizeButtonDrawable(
+ strokeAndFillColor = activeStrokeAndFill,
+ backgroundColor = activeBackground,
+ // Add a mask with the menu background's color because the active background color is
+ // semi transparent, otherwise the transparency will reveal the stroke/fill color
+ // behind it.
+ backgroundMask = menuBackgroundColor
+ )
+ return StateListDrawable().apply {
+ addState(intArrayOf(android.R.attr.state_pressed), activeDrawable)
+ addState(intArrayOf(android.R.attr.state_focused), activeDrawable)
+ addState(intArrayOf(android.R.attr.state_selected), activeDrawable)
+ addState(intArrayOf(android.R.attr.state_hovered), activeDrawable)
+ // Inactive drawable.
+ addState(
+ StateSet.WILD_CARD,
+ createMaximizeButtonDrawable(
+ strokeAndFillColor = colorScheme.outlineVariant.toArgb(),
+ backgroundColor = colorScheme.surfaceContainerLow.toArgb(),
+ backgroundMask = null // not needed because the bg color is fully opaque
+ )
+ )
+ }
+ }
+
+ private fun createMaximizeButtonDrawable(
+ @ColorInt strokeAndFillColor: Int,
+ @ColorInt backgroundColor: Int,
+ @ColorInt backgroundMask: Int?
+ ): LayerDrawable {
+ val layers = mutableListOf<Drawable>()
+ // First (bottom) layer, effectively the button's border ring once its inner shape is
+ // covered by the next layers.
+ layers.add(ShapeDrawable().apply {
+ shape = RoundRectShape(
+ FloatArray(8) { outlineRadius.toFloat() },
+ null /* inset */,
+ null /* innerRadii */
+ )
+ paint.color = strokeAndFillColor
+ paint.style = Paint.Style.FILL
+ })
+ // Second layer, a mask for the next (background) layer if needed because of
+ // transparency.
+ backgroundMask?.let { color ->
+ layers.add(
+ ShapeDrawable().apply {
+ shape = RoundRectShape(
+ FloatArray(8) { outlineRadius.toFloat() },
+ null /* inset */,
+ null /* innerRadii */
+ )
+ paint.color = color
+ paint.style = Paint.Style.FILL
+ }
+ )
+ }
+ // Third layer, the "background" padding between the border and the fill.
+ layers.add(ShapeDrawable().apply {
+ shape = RoundRectShape(
+ FloatArray(8) { outlineRadius.toFloat() },
+ null /* inset */,
+ null /* innerRadii */
+ )
+ paint.color = backgroundColor
+ paint.style = Paint.Style.FILL
+ })
+ // Final layer, the inner most rounded-rect "fill".
+ layers.add(ShapeDrawable().apply {
+ shape = RoundRectShape(
+ FloatArray(8) { fillRadius.toFloat() },
+ null /* inset */,
+ null /* innerRadii */
+ )
+ paint.color = strokeAndFillColor
+ paint.style = Paint.Style.FILL
+ })
+ return LayerDrawable(layers.toTypedArray()).apply {
+ when (numberOfLayers) {
+ 3 -> {
+ setLayerInset(1, outlineStroke)
+ setLayerInset(2, fillPadding)
+ }
+ 4 -> {
+ setLayerInset(intArrayOf(1, 2), outlineStroke)
+ setLayerInset(3, fillPadding)
+ }
+ else -> error("Unexpected number of layers: $numberOfLayers")
+ }
+ }
+ }
+
+ private fun LayerDrawable.setLayerInset(index: IntArray, inset: Int) {
+ for (i in index) {
+ setLayerInset(i, inset, inset, inset, inset)
+ }
+ }
+
+ private fun LayerDrawable.setLayerInset(index: Int, inset: Int) {
+ setLayerInset(index, inset, inset, inset, inset)
+ }
+
+ private fun requireViewById(id: Int) = rootView.requireViewById<View>(id)
+
+ /** The style to apply to the menu. */
+ data class MenuStyle(
+ @ColorInt val backgroundColor: Int,
+ @ColorInt val textColor: Int,
+ val maximizeOption: MaximizeOption,
+ val snapOptions: SnapOptions,
+ ) {
+ data class MaximizeOption(
+ val drawable: StateListDrawable,
+ )
+ data class SnapOptions(
+ @ColorInt val inactiveSnapSideColor: Int,
+ @ColorInt val semiActiveSnapSideColor: Int,
+ @ColorInt val activeSnapSideColor: Int,
+ @ColorInt val inactiveStrokeColor: Int,
+ @ColorInt val activeStrokeColor: Int,
+ @ColorInt val inactiveBackgroundColor: Int,
+ @ColorInt val activeBackgroundColor: Int,
+ )
+ }
+
+ /** The possible selection states of the half-snap menu option. */
+ enum class SnapToHalfSelection {
+ NONE, LEFT, RIGHT
}
}
@@ -352,7 +641,6 @@ class MaximizeMenu(
fun isMaximizeMenuView(@IdRes viewId: Int): Boolean {
return viewId == R.id.maximize_menu ||
viewId == R.id.maximize_menu_maximize_button ||
- viewId == R.id.maximize_menu_maximize_button_layout ||
viewId == R.id.maximize_menu_snap_left_button ||
viewId == R.id.maximize_menu_snap_right_button ||
viewId == R.id.maximize_menu_snap_menu_layout ||
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
index 4f2d945e49f9..cd2dac806a7f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
@@ -20,7 +20,6 @@ import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
-import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.PixelFormat
@@ -36,10 +35,15 @@ import android.view.WindowManager
import android.view.WindowlessWindowManager
import android.widget.ImageView
import android.window.TaskConstants
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.ui.graphics.toArgb
import com.android.wm.shell.R
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory
+import com.android.wm.shell.windowdecor.common.DecorThemeUtil
+import com.android.wm.shell.windowdecor.common.Theme
import java.util.function.Supplier
/**
@@ -49,14 +53,18 @@ class ResizeVeil @JvmOverloads constructor(
private val context: Context,
private val displayController: DisplayController,
private val appIcon: Bitmap,
- private val taskInfo: RunningTaskInfo,
private var parentSurface: SurfaceControl,
private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>,
private val surfaceControlBuilderFactory: SurfaceControlBuilderFactory =
object : SurfaceControlBuilderFactory {},
private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory =
- object : SurfaceControlViewHostFactory {}
+ object : SurfaceControlViewHostFactory {},
+ taskInfo: RunningTaskInfo,
) {
+ private val decorThemeUtil = DecorThemeUtil(context)
+ private val lightColors = dynamicLightColorScheme(context)
+ private val darkColors = dynamicDarkColorScheme(context)
+
private val surfaceSession = SurfaceSession()
private lateinit var iconView: ImageView
private var iconSize = 0
@@ -86,21 +94,10 @@ class ResizeVeil @JvmOverloads constructor(
return
}
displayController.removeDisplayWindowListener(this)
- setupResizeVeil()
+ setupResizeVeil(taskInfo)
}
}
- private val backgroundColorId: Int
- get() {
- val configuration = context.resources.configuration
- return if (configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
- == Configuration.UI_MODE_NIGHT_YES) {
- R.color.desktop_mode_resize_veil_dark
- } else {
- R.color.desktop_mode_resize_veil_light
- }
- }
-
/**
* Whether the resize veil is ready to be shown.
*/
@@ -108,14 +105,14 @@ class ResizeVeil @JvmOverloads constructor(
get() = viewHost != null
init {
- setupResizeVeil()
+ setupResizeVeil(taskInfo)
}
/**
* Create the veil in its default invisible state.
*/
- private fun setupResizeVeil() {
- if (!obtainDisplayOrRegisterListener()) {
+ private fun setupResizeVeil(taskInfo: RunningTaskInfo) {
+ if (!obtainDisplayOrRegisterListener(taskInfo.displayId)) {
// Display may not be available yet, skip this until then.
return
}
@@ -162,8 +159,8 @@ class ResizeVeil @JvmOverloads constructor(
Trace.endSection()
}
- private fun obtainDisplayOrRegisterListener(): Boolean {
- display = displayController.getDisplay(taskInfo.displayId)
+ private fun obtainDisplayOrRegisterListener(displayId: Int): Boolean {
+ display = displayController.getDisplay(displayId)
if (display == null) {
displayController.addDisplayWindowListener(onDisplaysChangedListener)
return false
@@ -184,7 +181,8 @@ class ResizeVeil @JvmOverloads constructor(
t: SurfaceControl.Transaction,
parent: SurfaceControl,
taskBounds: Rect,
- fadeIn: Boolean
+ taskInfo: RunningTaskInfo,
+ fadeIn: Boolean,
) {
if (!isReady || isVisible) {
t.apply()
@@ -202,13 +200,15 @@ class ResizeVeil @JvmOverloads constructor(
parentSurface = parent
}
-
+ val backgroundColor = when (decorThemeUtil.getAppTheme(taskInfo)) {
+ Theme.LIGHT -> lightColors.surfaceContainer
+ Theme.DARK -> darkColors.surfaceContainer
+ }
t.show(veil)
.setLayer(veil, VEIL_CONTAINER_LAYER)
.setLayer(icon, VEIL_ICON_LAYER)
.setLayer(background, VEIL_BACKGROUND_LAYER)
- .setColor(background,
- Color.valueOf(context.getColor(backgroundColorId)).components)
+ .setColor(background, Color.valueOf(backgroundColor.toArgb()).components)
relayout(taskBounds, t)
if (fadeIn) {
cancelAnimation()
@@ -270,12 +270,12 @@ class ResizeVeil @JvmOverloads constructor(
/**
* Animate veil's alpha to 1, fading it in.
*/
- fun showVeil(parentSurface: SurfaceControl, taskBounds: Rect) {
+ fun showVeil(parentSurface: SurfaceControl, taskBounds: Rect, taskInfo: RunningTaskInfo) {
if (!isReady || isVisible) {
return
}
val t = surfaceControlTransactionSupplier.get()
- showVeil(t, parentSurface, taskBounds, true /* fadeIn */)
+ showVeil(t, parentSurface, taskBounds, taskInfo, true /* fadeIn */)
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
index 5fce5d228d71..956d04c548f7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java
@@ -18,6 +18,8 @@ package com.android.wm.shell.windowdecor;
import static android.view.WindowManager.TRANSIT_CHANGE;
+import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_RESIZE_WINDOW;
+
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -33,6 +35,7 @@ import androidx.annotation.Nullable;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.InteractionJankMonitorUtils;
import com.android.wm.shell.transition.Transitions;
import java.util.function.Supplier;
@@ -89,6 +92,10 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback,
mDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds());
mRepositionStartPoint.set(x, y);
if (isResizing()) {
+ // Capture CUJ for re-sizing window in DW mode.
+ InteractionJankMonitorUtils.beginTracing(CUJ_DESKTOP_MODE_RESIZE_WINDOW,
+ mDesktopWindowDecoration.mContext, mDesktopWindowDecoration.mTaskSurface,
+ /* tag= */ null);
if (!mDesktopWindowDecoration.mTaskInfo.isFocused) {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true);
@@ -146,6 +153,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback,
// won't be called.
resetVeilIfVisible();
}
+ InteractionJankMonitorUtils.endTracing(CUJ_DESKTOP_MODE_RESIZE_WINDOW);
} else {
final WindowContainerTransaction wct = new WindowContainerTransaction();
DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds,
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 2ae3cb9ef3c0..216990c35247 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
@@ -22,6 +22,7 @@ import static android.content.res.Configuration.DENSITY_DPI_UNDEFINED;
import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.mandatorySystemGestures;
import static android.view.WindowInsets.Type.statusBars;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -56,6 +57,7 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.shared.DesktopModeStatus;
import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams.OccludingCaptionElement;
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer;
import java.util.ArrayList;
import java.util.Arrays;
@@ -197,8 +199,16 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
void relayout(RelayoutParams params, SurfaceControl.Transaction startT,
SurfaceControl.Transaction finishT, WindowContainerTransaction wct, T rootView,
RelayoutResult<T> outResult) {
- outResult.reset();
+ updateViewsAndSurfaces(params, startT, finishT, wct, rootView, outResult);
+ if (outResult.mRootView != null) {
+ updateViewHost(params, startT, outResult);
+ }
+ }
+ protected void updateViewsAndSurfaces(RelayoutParams params,
+ SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
+ WindowContainerTransaction wct, T rootView, RelayoutResult<T> outResult) {
+ outResult.reset();
if (params.mRunningTaskInfo != null) {
mTaskInfo = params.mRunningTaskInfo;
}
@@ -211,13 +221,38 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
return;
}
+ inflateIfNeeded(params, wct, rootView, oldLayoutResId, outResult);
+ if (outResult.mRootView == null) {
+ // Didn't manage to create a root view, early out.
+ return;
+ }
+ rootView = null; // Clear it just in case we use it accidentally
+
+ updateCaptionVisibility(outResult.mRootView, mTaskInfo.displayId);
+
+ final Rect taskBounds = mTaskInfo.getConfiguration().windowConfiguration.getBounds();
+ outResult.mWidth = taskBounds.width();
+ outResult.mHeight = taskBounds.height();
+ outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused);
+ final Resources resources = mDecorWindowContext.getResources();
+ outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
+ outResult.mCaptionWidth = params.mCaptionWidthId != Resources.ID_NULL
+ ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width();
+ outResult.mCaptionX = (outResult.mWidth - outResult.mCaptionWidth) / 2;
+
+ updateDecorationContainerSurface(startT, outResult);
+ updateCaptionContainerSurface(startT, outResult);
+ updateCaptionInsets(params, wct, outResult, taskBounds);
+ updateTaskSurface(params, startT, finishT, outResult);
+ }
+
+ private void inflateIfNeeded(RelayoutParams params, WindowContainerTransaction wct,
+ T rootView, int oldLayoutResId, RelayoutResult<T> outResult) {
if (rootView == null && params.mLayoutResId == 0) {
throw new IllegalArgumentException("layoutResId and rootView can't both be invalid.");
}
outResult.mRootView = rootView;
- rootView = null; // Clear it just in case we use it accidentally
-
final int oldDensityDpi = mWindowDecorConfig != null
? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED;
final int oldNightMode = mWindowDecorConfig != null
@@ -251,25 +286,17 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
outResult.mRootView = (T) LayoutInflater.from(mDecorWindowContext)
.inflate(params.mLayoutResId, null);
}
+ }
- updateCaptionVisibility(outResult.mRootView, mTaskInfo.displayId);
-
- final Resources resources = mDecorWindowContext.getResources();
- final Configuration taskConfig = mTaskInfo.getConfiguration();
- final Rect taskBounds = taskConfig.windowConfiguration.getBounds();
- final boolean isFullscreen = taskConfig.windowConfiguration.getWindowingMode()
- == WINDOWING_MODE_FULLSCREEN;
- outResult.mWidth = taskBounds.width();
- outResult.mHeight = taskBounds.height();
-
- // DecorationContainerSurface
+ private void updateDecorationContainerSurface(
+ SurfaceControl.Transaction startT, RelayoutResult<T> outResult) {
if (mDecorationContainerSurface == null) {
final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
mDecorationContainerSurface = builder
.setName("Decor container of Task=" + mTaskInfo.taskId)
.setContainerLayer()
.setParent(mTaskSurface)
- .setCallsite("WindowDecoration.relayout_1")
+ .setCallsite("WindowDecoration.updateDecorationContainerSurface")
.build();
startT.setTrustedOverlay(mDecorationContainerSurface, true)
@@ -279,101 +306,101 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
startT.setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight)
.show(mDecorationContainerSurface);
+ }
- // CaptionContainerSurface, CaptionWindowManager
+ private void updateCaptionContainerSurface(
+ SurfaceControl.Transaction startT, RelayoutResult<T> outResult) {
if (mCaptionContainerSurface == null) {
final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
mCaptionContainerSurface = builder
.setName("Caption container of Task=" + mTaskInfo.taskId)
.setContainerLayer()
.setParent(mDecorationContainerSurface)
- .setCallsite("WindowDecoration.relayout_2")
+ .setCallsite("WindowDecoration.updateCaptionContainerSurface")
.build();
}
- outResult.mCaptionHeight = loadDimensionPixelSize(resources, params.mCaptionHeightId);
- outResult.mCaptionWidth = params.mCaptionWidthId != Resources.ID_NULL
- ? loadDimensionPixelSize(resources, params.mCaptionWidthId) : taskBounds.width();
- outResult.mCaptionX = (outResult.mWidth - outResult.mCaptionWidth) / 2;
-
startT.setWindowCrop(mCaptionContainerSurface, outResult.mCaptionWidth,
outResult.mCaptionHeight)
.setPosition(mCaptionContainerSurface, outResult.mCaptionX, 0 /* y */)
.setLayer(mCaptionContainerSurface, CAPTION_LAYER_Z_ORDER)
.show(mCaptionContainerSurface);
+ }
- outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused);
-
- // Caption insets
- if (mIsCaptionVisible) {
- // Caption inset is the full width of the task with the |captionHeight| and
- // positioned at the top of the task bounds, also in absolute coordinates.
- // So just reuse the task bounds and adjust the bottom coordinate.
- final Rect captionInsetsRect = new Rect(taskBounds);
- captionInsetsRect.bottom = captionInsetsRect.top + outResult.mCaptionHeight;
-
- // Caption bounding rectangles: these are optional, and are used to present finer
- // insets than traditional |Insets| to apps about where their content is occluded.
- // These are also in absolute coordinates.
- final Rect[] boundingRects;
- final int numOfElements = params.mOccludingCaptionElements.size();
- if (numOfElements == 0) {
- boundingRects = null;
- } else {
- // The customizable region can at most be equal to the caption bar.
- if (params.hasInputFeatureSpy()) {
- outResult.mCustomizableCaptionRegion.set(captionInsetsRect);
- }
- boundingRects = new Rect[numOfElements];
- for (int i = 0; i < numOfElements; i++) {
- final OccludingCaptionElement element =
- params.mOccludingCaptionElements.get(i);
- final int elementWidthPx =
- resources.getDimensionPixelSize(element.mWidthResId);
- boundingRects[i] =
- calculateBoundingRect(element, elementWidthPx, captionInsetsRect);
- // Subtract the regions used by the caption elements, the rest is
- // customizable.
- if (params.hasInputFeatureSpy()) {
- outResult.mCustomizableCaptionRegion.op(boundingRects[i],
- Region.Op.DIFFERENCE);
- }
- }
- }
-
- final WindowDecorationInsets newInsets = new WindowDecorationInsets(
- mTaskInfo.token, mOwner, captionInsetsRect, boundingRects);
- if (!newInsets.equals(mWindowDecorationInsets)) {
- // Add or update this caption as an insets source.
- mWindowDecorationInsets = newInsets;
- mWindowDecorationInsets.addOrUpdate(wct);
- }
- } else {
+ private void updateCaptionInsets(RelayoutParams params, WindowContainerTransaction wct,
+ RelayoutResult<T> outResult, Rect taskBounds) {
+ if (!mIsCaptionVisible) {
if (mWindowDecorationInsets != null) {
mWindowDecorationInsets.remove(wct);
mWindowDecorationInsets = null;
}
+ return;
}
-
- // Task surface itself
- float shadowRadius;
- final Point taskPosition = mTaskInfo.positionInParent;
- if (isFullscreen) {
- // Shadow is not needed for fullscreen tasks
- shadowRadius = 0;
+ // Caption inset is the full width of the task with the |captionHeight| and
+ // positioned at the top of the task bounds, also in absolute coordinates.
+ // So just reuse the task bounds and adjust the bottom coordinate.
+ final Rect captionInsetsRect = new Rect(taskBounds);
+ captionInsetsRect.bottom = captionInsetsRect.top + outResult.mCaptionHeight;
+
+ // Caption bounding rectangles: these are optional, and are used to present finer
+ // insets than traditional |Insets| to apps about where their content is occluded.
+ // These are also in absolute coordinates.
+ final Rect[] boundingRects;
+ final int numOfElements = params.mOccludingCaptionElements.size();
+ if (numOfElements == 0) {
+ boundingRects = null;
} else {
- shadowRadius = loadDimension(resources, params.mShadowRadiusId);
+ // The customizable region can at most be equal to the caption bar.
+ if (params.hasInputFeatureSpy()) {
+ outResult.mCustomizableCaptionRegion.set(captionInsetsRect);
+ }
+ final Resources resources = mDecorWindowContext.getResources();
+ boundingRects = new Rect[numOfElements];
+ for (int i = 0; i < numOfElements; i++) {
+ final OccludingCaptionElement element =
+ params.mOccludingCaptionElements.get(i);
+ final int elementWidthPx =
+ resources.getDimensionPixelSize(element.mWidthResId);
+ boundingRects[i] =
+ calculateBoundingRect(element, elementWidthPx, captionInsetsRect);
+ // Subtract the regions used by the caption elements, the rest is
+ // customizable.
+ if (params.hasInputFeatureSpy()) {
+ outResult.mCustomizableCaptionRegion.op(boundingRects[i],
+ Region.Op.DIFFERENCE);
+ }
+ }
}
+ final WindowDecorationInsets newInsets = new WindowDecorationInsets(
+ mTaskInfo.token, mOwner, captionInsetsRect, boundingRects);
+ if (!newInsets.equals(mWindowDecorationInsets)) {
+ // Add or update this caption as an insets source.
+ mWindowDecorationInsets = newInsets;
+ mWindowDecorationInsets.addOrUpdate(wct);
+ }
+ }
+
+ private void updateTaskSurface(RelayoutParams params, SurfaceControl.Transaction startT,
+ SurfaceControl.Transaction finishT, RelayoutResult<T> outResult) {
if (params.mSetTaskPositionAndCrop) {
+ final Point taskPosition = mTaskInfo.positionInParent;
startT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight);
finishT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight)
.setPosition(mTaskSurface, taskPosition.x, taskPosition.y);
}
- startT.setShadowRadius(mTaskSurface, shadowRadius)
- .show(mTaskSurface);
+ float shadowRadius;
+ if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+ // Shadow is not needed for fullscreen tasks
+ shadowRadius = 0;
+ } else {
+ shadowRadius =
+ loadDimension(mDecorWindowContext.getResources(), params.mShadowRadiusId);
+ }
+ startT.setShadowRadius(mTaskSurface, shadowRadius).show(mTaskSurface);
finishT.setShadowRadius(mTaskSurface, shadowRadius);
+
if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
if (!DesktopModeStatus.isVeiledResizeEnabled()) {
// When fluid resize is enabled, add a background to freeform tasks
@@ -388,7 +415,19 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
} else if (!DesktopModeStatus.isVeiledResizeEnabled()) {
startT.unsetColor(mTaskSurface);
}
+ }
+ /**
+ * Updates a {@link SurfaceControlViewHost} to connect the window decoration surfaces with our
+ * View hierarchy.
+ *
+ * @param params parameters to use from the last relayout
+ * @param onDrawTransaction a transaction to apply in sync with #onDraw
+ * @param outResult results to use from the last relayout
+ *
+ */
+ protected void updateViewHost(RelayoutParams params,
+ SurfaceControl.Transaction onDrawTransaction, RelayoutResult<T> outResult) {
Trace.beginSection("CaptionViewHostLayout");
if (mCaptionWindowManager == null) {
// Put caption under a container surface because ViewRootImpl sets the destination frame
@@ -397,12 +436,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
mTaskInfo.getConfiguration(), mCaptionContainerSurface,
null /* hostInputToken */);
}
-
- // Caption view
- mCaptionWindowManager.setConfiguration(taskConfig);
+ mCaptionWindowManager.setConfiguration(mTaskInfo.getConfiguration());
final WindowManager.LayoutParams lp =
new WindowManager.LayoutParams(outResult.mCaptionWidth, outResult.mCaptionHeight,
- WindowManager.LayoutParams.TYPE_APPLICATION,
+ TYPE_APPLICATION,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
lp.setTitle("Caption of Task=" + mTaskInfo.taskId);
lp.setTrustedOverlay();
@@ -412,14 +449,20 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
mViewHost = mSurfaceControlViewHostFactory.create(mDecorWindowContext, mDisplay,
mCaptionWindowManager);
if (params.mApplyStartTransactionOnDraw) {
- mViewHost.getRootSurfaceControl().applyTransactionOnDraw(startT);
+ if (onDrawTransaction == null) {
+ throw new IllegalArgumentException("Trying to sync a null Transaction");
+ }
+ mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction);
}
mViewHost.setView(outResult.mRootView, lp);
Trace.endSection();
} else {
Trace.beginSection("CaptionViewHostLayout-relayout");
if (params.mApplyStartTransactionOnDraw) {
- mViewHost.getRootSurfaceControl().applyTransactionOnDraw(startT);
+ if (onDrawTransaction == null) {
+ throw new IllegalArgumentException("Trying to sync a null Transaction");
+ }
+ mViewHost.getRootSurfaceControl().applyTransactionOnDraw(onDrawTransaction);
}
mViewHost.relayout(lp);
Trace.endSection();
@@ -569,10 +612,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
* @param yPos y position of new window
* @param width width of new window
* @param height height of new window
- * @return the {@link AdditionalWindow} that was added.
+ * @return the {@link AdditionalViewHostViewContainer} that was added.
*/
- AdditionalWindow addWindow(int layoutId, String namePrefix, SurfaceControl.Transaction t,
- SurfaceSyncGroup ssg, int xPos, int yPos, int width, int height) {
+ AdditionalViewHostViewContainer addWindow(int layoutId, String namePrefix,
+ SurfaceControl.Transaction t, SurfaceSyncGroup ssg, int xPos, int yPos,
+ int width, int height) {
final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
SurfaceControl windowSurfaceControl = builder
.setName(namePrefix + " of Task=" + mTaskInfo.taskId)
@@ -586,9 +630,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
.setWindowCrop(windowSurfaceControl, width, height)
.show(windowSurfaceControl);
final WindowManager.LayoutParams lp =
- new WindowManager.LayoutParams(width, height,
- WindowManager.LayoutParams.TYPE_APPLICATION,
- WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT);
+ new WindowManager.LayoutParams(width, height, TYPE_APPLICATION,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSPARENT);
lp.setTitle("Additional window of Task=" + mTaskInfo.taskId);
lp.setTrustedOverlay();
WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration,
@@ -596,7 +640,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
SurfaceControlViewHost viewHost = mSurfaceControlViewHostFactory
.create(mDecorWindowContext, mDisplay, windowManager);
ssg.add(viewHost.getSurfacePackage(), () -> viewHost.setView(v, lp));
- return new AdditionalWindow(windowSurfaceControl, viewHost,
+ return new AdditionalViewHostViewContainer(windowSurfaceControl, viewHost,
mSurfaceControlTransactionSupplier);
}
@@ -739,41 +783,4 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
return Objects.hash(mToken, mOwner, mFrame, Arrays.hashCode(mBoundingRects));
}
}
-
- /**
- * Subclass for additional windows associated with this WindowDecoration
- */
- static class AdditionalWindow {
- SurfaceControl mWindowSurface;
- SurfaceControlViewHost mWindowViewHost;
- Supplier<SurfaceControl.Transaction> mTransactionSupplier;
-
- AdditionalWindow(SurfaceControl surfaceControl,
- SurfaceControlViewHost surfaceControlViewHost,
- Supplier<SurfaceControl.Transaction> transactionSupplier) {
- mWindowSurface = surfaceControl;
- mWindowViewHost = surfaceControlViewHost;
- mTransactionSupplier = transactionSupplier;
- }
-
- void releaseView() {
- WindowlessWindowManager windowManager = mWindowViewHost.getWindowlessWM();
-
- if (mWindowViewHost != null) {
- mWindowViewHost.release();
- mWindowViewHost = null;
- }
- windowManager = null;
- final SurfaceControl.Transaction t = mTransactionSupplier.get();
- boolean released = false;
- if (mWindowSurface != null) {
- t.remove(mWindowSurface);
- mWindowSurface = null;
- released = true;
- }
- if (released) {
- t.apply();
- }
- }
- }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
new file mode 100644
index 000000000000..6c2c8fd46bc9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.wm.shell.windowdecor.additionalviewcontainer
+
+import android.content.Context
+import android.graphics.PixelFormat
+import android.view.LayoutInflater
+import android.view.SurfaceControl
+import android.view.View
+import android.view.WindowManager
+
+/**
+ * An [AdditionalViewContainer] that uses the system [WindowManager] instance. Intended
+ * for view containers that should be above the status bar layer.
+ */
+class AdditionalSystemViewContainer(
+ private val context: Context,
+ layoutId: Int,
+ taskId: Int,
+ x: Int,
+ y: Int,
+ width: Int,
+ height: Int
+) : AdditionalViewContainer() {
+ override val view: View
+
+ init {
+ view = LayoutInflater.from(context).inflate(layoutId, null)
+ val lp = WindowManager.LayoutParams(
+ width, height, x, y,
+ WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSPARENT
+ )
+ lp.title = "Additional view container of Task=$taskId"
+ lp.setTrustedOverlay()
+ val wm: WindowManager? = context.getSystemService(WindowManager::class.java)
+ wm?.addView(view, lp)
+ }
+
+ override fun releaseView() {
+ context.getSystemService(WindowManager::class.java)?.removeViewImmediate(view)
+ }
+
+ override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) {
+ val lp = (view.layoutParams as WindowManager.LayoutParams).apply {
+ this.x = x.toInt()
+ this.y = y.toInt()
+ }
+ view.layoutParams = lp
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewContainer.kt
new file mode 100644
index 000000000000..2650648a2cde
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewContainer.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.wm.shell.windowdecor.additionalviewcontainer
+
+import android.view.SurfaceControl
+import android.view.View
+import com.android.wm.shell.windowdecor.WindowDecoration
+
+/**
+ * Class for additional view containers associated with a [WindowDecoration].
+ */
+abstract class AdditionalViewContainer internal constructor(
+) {
+ abstract val view: View?
+
+ /** Release the view associated with this container and perform needed cleanup. */
+ abstract fun releaseView()
+
+ /** Reposition the view container using provided coordinates. */
+ abstract fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainer.kt
new file mode 100644
index 000000000000..222761260289
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainer.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.wm.shell.windowdecor.additionalviewcontainer
+
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import java.util.function.Supplier
+
+/**
+ * An [AdditionalViewContainer] that uses a [SurfaceControlViewHost] to show the window.
+ * Intended for view containers in freeform tasks that do not extend beyond task bounds.
+ */
+class AdditionalViewHostViewContainer(
+ private val windowSurface: SurfaceControl,
+ private val windowViewHost: SurfaceControlViewHost,
+ private val transactionSupplier: Supplier<SurfaceControl.Transaction>,
+) : AdditionalViewContainer() {
+
+ override val view
+ get() = windowViewHost.view
+
+ override fun releaseView() {
+ windowViewHost.release()
+ val t = transactionSupplier.get()
+ t.remove(windowSurface)
+ t.apply()
+ }
+
+ override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) {
+ t.setPosition(windowSurface, x, y)
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt
new file mode 100644
index 000000000000..f7cfbfa88485
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.wm.shell.windowdecor.common
+
+import android.annotation.ColorInt
+import android.annotation.IntRange
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.content.res.Configuration
+import android.content.res.Configuration.UI_MODE_NIGHT_MASK
+import android.graphics.Color
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+
+/** The theme of a window decoration. */
+internal enum class Theme { LIGHT, DARK }
+
+/** Whether a [Theme] is light. */
+internal fun Theme.isLight(): Boolean = this == Theme.LIGHT
+
+/** Whether a [Theme] is dark. */
+internal fun Theme.isDark(): Boolean = this == Theme.DARK
+
+/** Returns a copy of the color with its [alpha] component replaced with the given value. */
+@ColorInt
+internal fun @receiver:ColorInt Int.withAlpha(@IntRange(from = 0, to = 255) alpha: Int): Int =
+ Color.argb(
+ alpha,
+ Color.red(this),
+ Color.green(this),
+ Color.blue(this)
+ )
+
+/** Common opacity values used in window decoration views. */
+const val OPACITY_100 = 255
+const val OPACITY_11 = 28
+const val OPACITY_12 = 31
+const val OPACITY_15 = 38
+const val OPACITY_40 = 102
+const val OPACITY_55 = 140
+const val OPACITY_65 = 166
+
+/**
+ * Utility class for determining themes based on system settings and app's [RunningTaskInfo].
+ */
+internal class DecorThemeUtil(private val context: Context) {
+ private val lightColors = dynamicLightColorScheme(context)
+ private val darkColors = dynamicDarkColorScheme(context)
+
+ private val systemTheme: Theme
+ get() = if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) ==
+ Configuration.UI_MODE_NIGHT_YES) {
+ Theme.DARK
+ } else {
+ Theme.LIGHT
+ }
+
+ /**
+ * Returns the [Theme] used by the app with the given [RunningTaskInfo].
+ */
+ fun getAppTheme(task: RunningTaskInfo): Theme {
+ // TODO: use app's uiMode to find its actual light/dark value. It needs to be added to the
+ // TaskInfo/TaskDescription.
+ val backgroundColor = task.taskDescription?.backgroundColor ?: return systemTheme
+ return if (Color.valueOf(backgroundColor).luminance() < 0.5) {
+ Theme.DARK
+ } else {
+ Theme.LIGHT
+ }
+ }
+
+ /**
+ * Returns the [ColorScheme] to use to style window decorations based on the given
+ * [RunningTaskInfo].
+ */
+ fun getColorScheme(task: RunningTaskInfo): ColorScheme = when (getAppTheme(task)) {
+ Theme.LIGHT -> lightColors
+ Theme.DARK -> darkColors
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
index 96bc4a146ebd..8d822c252288 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeFocusedWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt
@@ -1,3 +1,18 @@
+/*
+ * 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.wm.shell.windowdecor.viewholder
import android.animation.ObjectAnimator
@@ -12,14 +27,14 @@ import com.android.wm.shell.R
import com.android.wm.shell.animation.Interpolators
/**
- * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen). It
- * hosts a simple handle bar from which to initiate a drag motion to enter desktop mode.
+ * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split).
+ * It hosts a simple handle bar from which to initiate a drag motion to enter desktop mode.
*/
-internal class DesktopModeFocusedWindowDecorationViewHolder(
+internal class AppHandleViewHolder(
rootView: View,
onCaptionTouchListener: View.OnTouchListener,
onCaptionButtonClickListener: View.OnClickListener
-) : DesktopModeWindowDecorationViewHolder(rootView) {
+) : WindowDecorationViewHolder(rootView) {
companion object {
private const val CAPTION_HANDLE_ANIMATION_DURATION: Long = 100
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 3c12da2d6620..46127b177bc3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -1,13 +1,26 @@
+/*
+ * 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.wm.shell.windowdecor.viewholder
import android.annotation.ColorInt
import android.app.ActivityManager.RunningTaskInfo
import android.content.res.ColorStateList
import android.content.res.Configuration
-import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.graphics.Bitmap
import android.graphics.Color
-import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.ShapeDrawable
@@ -17,19 +30,27 @@ import android.view.View.OnLongClickListener
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.ui.graphics.toArgb
import androidx.core.content.withStyledAttributes
import androidx.core.view.isVisible
import com.android.internal.R.attr.materialColorOnSecondaryContainer
import com.android.internal.R.attr.materialColorOnSurface
-import com.android.internal.R.attr.materialColorOnSurfaceInverse
import com.android.internal.R.attr.materialColorSecondaryContainer
import com.android.internal.R.attr.materialColorSurfaceContainerHigh
import com.android.internal.R.attr.materialColorSurfaceContainerLow
import com.android.internal.R.attr.materialColorSurfaceDim
-import com.android.internal.R.attr.materialColorSurfaceInverse
import com.android.window.flags.Flags
import com.android.wm.shell.R
import com.android.wm.shell.windowdecor.MaximizeButtonView
+import com.android.wm.shell.windowdecor.common.DecorThemeUtil
+import com.android.wm.shell.windowdecor.common.OPACITY_100
+import com.android.wm.shell.windowdecor.common.OPACITY_11
+import com.android.wm.shell.windowdecor.common.OPACITY_15
+import com.android.wm.shell.windowdecor.common.OPACITY_55
+import com.android.wm.shell.windowdecor.common.OPACITY_65
+import com.android.wm.shell.windowdecor.common.Theme
import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance
import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance
@@ -38,7 +59,7 @@ import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppeara
* finer controls such as a close window button and an "app info" section to pull up additional
* controls.
*/
-internal class DesktopModeAppControlsWindowDecorationViewHolder(
+internal class AppHeaderViewHolder(
rootView: View,
onCaptionTouchListener: View.OnTouchListener,
onCaptionButtonClickListener: View.OnClickListener,
@@ -47,7 +68,11 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder(
appName: CharSequence,
appIconBitmap: Bitmap,
onMaximizeHoverAnimationFinishedListener: () -> Unit
-) : DesktopModeWindowDecorationViewHolder(rootView) {
+) : WindowDecorationViewHolder(rootView) {
+
+ private val decorThemeUtil = DecorThemeUtil(context)
+ private val lightColors = dynamicLightColorScheme(context)
+ private val darkColors = dynamicDarkColorScheme(context)
/**
* The corner radius to apply to the app chip, maximize and close button's background drawable.
@@ -153,19 +178,12 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder(
val headerStyle = getHeaderStyle(header)
// Caption Background
- val headerBackground = captionView.background as LayerDrawable
- val backLayer = headerBackground.findDrawableByLayerId(R.id.backLayer) as GradientDrawable
- val frontLayer = headerBackground.findDrawableByLayerId(R.id.frontLayer) as GradientDrawable
when (headerStyle.background) {
is HeaderStyle.Background.Opaque -> {
- backLayer.setColor(headerStyle.background.backLayerColor ?: Color.BLACK)
- frontLayer.setColor(headerStyle.background.frontLayerColor)
- frontLayer.alpha = headerStyle.background.frontLayerOpacity
+ captionView.setBackgroundColor(headerStyle.background.color)
}
HeaderStyle.Background.Transparent -> {
- backLayer.setColor(Color.TRANSPARENT)
- frontLayer.setColor(Color.TRANSPARENT)
- frontLayer.alpha = OPACITY_100
+ captionView.setBackgroundColor(Color.TRANSPARENT)
}
}
@@ -189,7 +207,7 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder(
}
// Maximize button.
maximizeButtonView.setAnimationTints(
- darkMode = header.appTheme == Header.Theme.DARK,
+ darkMode = header.appTheme == Theme.DARK,
iconForegroundColor = colorStateList,
baseForegroundColor = foregroundColor,
rippleDrawable = createRippleDrawable(
@@ -236,186 +254,88 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder(
)
}
- private fun getHeaderBackground(
- header: Header
- ): HeaderStyle.Background {
- when (header.type) {
+ private fun getHeaderBackground(header: Header): HeaderStyle.Background {
+ return when (header.type) {
Header.Type.DEFAULT -> {
- if (header.systemTheme.isLight() && header.appTheme.isLight() && header.isFocused) {
- return HeaderStyle.Background.Opaque(
- frontLayerColor = attrToColor(materialColorSecondaryContainer),
- frontLayerOpacity = OPACITY_100,
- backLayerColor = null
- )
- }
- if (header.systemTheme.isLight() && header.appTheme.isLight() &&
- !header.isFocused) {
- return HeaderStyle.Background.Opaque(
- frontLayerColor = attrToColor(materialColorSurfaceContainerLow),
- frontLayerOpacity = OPACITY_100,
- backLayerColor = null
- )
- }
- if (header.systemTheme.isDark() && header.appTheme.isDark() && header.isFocused) {
- return HeaderStyle.Background.Opaque(
- frontLayerColor = attrToColor(materialColorSurfaceContainerHigh),
- frontLayerOpacity = OPACITY_100,
- backLayerColor = null
- )
- }
- if (header.systemTheme.isDark() && header.appTheme.isDark() && !header.isFocused) {
- return HeaderStyle.Background.Opaque(
- frontLayerColor = attrToColor(materialColorSurfaceDim),
- frontLayerOpacity = OPACITY_100,
- backLayerColor = null
- )
- }
- if (header.systemTheme.isLight() && header.appTheme.isDark() && header.isFocused) {
- return HeaderStyle.Background.Opaque(
- frontLayerColor = attrToColor(materialColorSurfaceInverse),
- frontLayerOpacity = OPACITY_100,
- backLayerColor = null
- )
- }
- if (header.systemTheme.isLight() && header.appTheme.isDark() && !header.isFocused) {
- return HeaderStyle.Background.Opaque(
- frontLayerColor = attrToColor(materialColorSurfaceInverse),
- frontLayerOpacity = OPACITY_30,
- backLayerColor = Color.BLACK
- )
- }
- if (header.systemTheme.isDark() && header.appTheme.isLight() && header.isFocused) {
- return HeaderStyle.Background.Opaque(
- frontLayerColor = attrToColor(materialColorSurfaceInverse),
- frontLayerOpacity = OPACITY_100,
- backLayerColor = null
- )
- }
- if (header.systemTheme.isDark() && header.appTheme.isLight() && !header.isFocused) {
- return HeaderStyle.Background.Opaque(
- frontLayerColor = attrToColor(materialColorSurfaceInverse),
- frontLayerOpacity = OPACITY_55,
- backLayerColor = Color.WHITE
- )
+ when (header.appTheme) {
+ Theme.LIGHT -> {
+ if (header.isFocused) {
+ HeaderStyle.Background.Opaque(lightColors.secondaryContainer.toArgb())
+ } else {
+ HeaderStyle.Background.Opaque(lightColors.surfaceContainerLow.toArgb())
+ }
+ }
+ Theme.DARK -> {
+ if (header.isFocused) {
+ HeaderStyle.Background.Opaque(darkColors.surfaceContainerHigh.toArgb())
+ } else {
+ HeaderStyle.Background.Opaque(darkColors.surfaceDim.toArgb())
+ }
+ }
}
- error("No other combination expected header=$header")
}
- Header.Type.CUSTOM -> return HeaderStyle.Background.Transparent
+ Header.Type.CUSTOM -> HeaderStyle.Background.Transparent
}
}
private fun getHeaderForeground(header: Header): HeaderStyle.Foreground {
- when (header.type) {
+ return when (header.type) {
Header.Type.DEFAULT -> {
- if (header.systemTheme.isLight() && header.appTheme.isLight() && header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSecondaryContainer),
- opacity = OPACITY_100
- )
- }
- if (header.systemTheme.isLight() && header.appTheme.isLight() &&
- !header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSecondaryContainer),
- opacity = OPACITY_65
- )
- }
- if (header.systemTheme.isDark() && header.appTheme.isDark() && header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSurface),
- opacity = OPACITY_100
- )
- }
- if (header.systemTheme.isDark() && header.appTheme.isDark() && !header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSurface),
- opacity = OPACITY_55
- )
- }
- if (header.systemTheme.isLight() && header.appTheme.isDark() && header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSurfaceInverse),
- opacity = OPACITY_100
- )
- }
- if (header.systemTheme.isLight() && header.appTheme.isDark() && !header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSurfaceInverse),
- opacity = OPACITY_65
- )
+ when (header.appTheme) {
+ Theme.LIGHT -> {
+ if (header.isFocused) {
+ HeaderStyle.Foreground(
+ color = lightColors.onSecondaryContainer.toArgb(),
+ opacity = OPACITY_100
+ )
+ } else {
+ HeaderStyle.Foreground(
+ color = lightColors.onSecondaryContainer.toArgb(),
+ opacity = OPACITY_65
+ )
+ }
+ }
+ Theme.DARK -> {
+ if (header.isFocused) {
+ HeaderStyle.Foreground(
+ color = darkColors.onSurface.toArgb(),
+ opacity = OPACITY_100
+ )
+ } else {
+ HeaderStyle.Foreground(
+ color = darkColors.onSurface.toArgb(),
+ opacity = OPACITY_55
+ )
+ }
+ }
}
- if (header.systemTheme.isDark() && header.appTheme.isLight() && header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSurfaceInverse),
- opacity = OPACITY_100
- )
- }
- if (header.systemTheme.isDark() && header.appTheme.isLight() && !header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSurfaceInverse),
- opacity = OPACITY_70
- )
- }
- error("No other combination expected header=$header")
}
- Header.Type.CUSTOM -> {
- if (header.systemTheme.isLight() && header.isAppearanceCaptionLight &&
- header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSecondaryContainer),
+ Header.Type.CUSTOM -> when {
+ header.isAppearanceCaptionLight && header.isFocused -> {
+ HeaderStyle.Foreground(
+ color = lightColors.onSecondaryContainer.toArgb(),
opacity = OPACITY_100
)
}
- if (header.systemTheme.isLight() && header.isAppearanceCaptionLight &&
- !header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSecondaryContainer),
+ header.isAppearanceCaptionLight && !header.isFocused -> {
+ HeaderStyle.Foreground(
+ color = lightColors.onSecondaryContainer.toArgb(),
opacity = OPACITY_65
)
}
- if (header.systemTheme.isDark() && !header.isAppearanceCaptionLight &&
- header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSurface),
+ !header.isAppearanceCaptionLight && header.isFocused -> {
+ HeaderStyle.Foreground(
+ color = darkColors.onSurface.toArgb(),
opacity = OPACITY_100
)
}
- if (header.systemTheme.isDark() && !header.isAppearanceCaptionLight &&
- !header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSurface),
+ !header.isAppearanceCaptionLight && !header.isFocused -> {
+ HeaderStyle.Foreground(
+ color = darkColors.onSurface.toArgb(),
opacity = OPACITY_55
)
}
- if (header.systemTheme.isLight() && !header.isAppearanceCaptionLight &&
- header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSurfaceInverse),
- opacity = OPACITY_100
- )
- }
- if (header.systemTheme.isLight() && !header.isAppearanceCaptionLight &&
- !header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSurfaceInverse),
- opacity = OPACITY_65
- )
- }
- if (header.systemTheme.isDark() && header.isAppearanceCaptionLight &&
- header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSurfaceInverse),
- opacity = OPACITY_100
- )
- }
- if (header.systemTheme.isDark() && header.isAppearanceCaptionLight &&
- !header.isFocused) {
- return HeaderStyle.Foreground(
- color = attrToColor(materialColorOnSurfaceInverse),
- opacity = OPACITY_70
- )
- }
- error("No other combination expected header=$header")
+ else -> error("No other combination expected header=$header")
}
}
}
@@ -427,41 +347,12 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder(
} else {
Header.Type.DEFAULT
},
- systemTheme = getSystemTheme(),
- appTheme = getAppTheme(taskInfo),
+ appTheme = decorThemeUtil.getAppTheme(taskInfo),
isFocused = taskInfo.isFocused,
isAppearanceCaptionLight = taskInfo.isLightCaptionBarAppearance
)
}
- private fun getSystemTheme(): Header.Theme {
- return if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) ==
- Configuration.UI_MODE_NIGHT_YES) {
- Header.Theme.DARK
- } else {
- Header.Theme.LIGHT
- }
- }
-
- private fun getAppTheme(taskInfo: RunningTaskInfo): Header.Theme {
- // TODO: use app's uiMode to find its actual light/dark value. It needs to be added to the
- // TaskInfo/TaskDescription.
- val backgroundColor = taskInfo.taskDescription?.backgroundColor ?: return getSystemTheme()
- return if (Color.valueOf(backgroundColor).luminance() < 0.5) {
- Header.Theme.DARK
- } else {
- Header.Theme.LIGHT
- }
- }
-
- @ColorInt
- private fun attrToColor(attr: Int): Int {
- context.withStyledAttributes(null, intArrayOf(attr), 0, 0) {
- return getColor(0, 0)
- }
- return Color.WHITE
- }
-
@ColorInt
private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
return Color.argb(
@@ -515,19 +406,13 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder(
private data class Header(
val type: Type,
- val systemTheme: Theme,
val appTheme: Theme,
val isFocused: Boolean,
val isAppearanceCaptionLight: Boolean,
) {
enum class Type { DEFAULT, CUSTOM }
- enum class Theme { LIGHT, DARK }
}
- private fun Header.Theme.isLight(): Boolean = this == Header.Theme.LIGHT
-
- private fun Header.Theme.isDark(): Boolean = this == Header.Theme.DARK
-
private data class HeaderStyle(
val background: Background,
val foreground: Foreground
@@ -539,11 +424,7 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder(
sealed class Background {
data object Transparent : Background()
- data class Opaque(
- @ColorInt val frontLayerColor: Int,
- val frontLayerOpacity: Int,
- @ColorInt val backLayerColor: Int?
- ) : Background()
+ data class Opaque(@ColorInt val color: Int) : Background()
}
}
@@ -615,13 +496,5 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder(
private const val DARK_THEME_UNFOCUSED_OPACITY = 140 // 55%
private const val LIGHT_THEME_UNFOCUSED_OPACITY = 166 // 65%
private const val FOCUSED_OPACITY = 255
-
- private const val OPACITY_100 = 255
- private const val OPACITY_11 = 28
- private const val OPACITY_15 = 38
- private const val OPACITY_30 = 77
- private const val OPACITY_55 = 140
- private const val OPACITY_65 = 166
- private const val OPACITY_70 = 179
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
index 81bc34c876b6..5ae8d252a908 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeWindowDecorationViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/WindowDecorationViewHolder.kt
@@ -1,3 +1,18 @@
+/*
+ * 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.wm.shell.windowdecor.viewholder
import android.app.ActivityManager.RunningTaskInfo
@@ -8,7 +23,7 @@ import android.view.View
* Encapsulates the root [View] of a window decoration and its children to facilitate looking up
* children (via findViewById) and updating to the latest data from [RunningTaskInfo].
*/
-internal abstract class DesktopModeWindowDecorationViewHolder(rootView: View) {
+internal abstract class WindowDecorationViewHolder(rootView: View) {
val context: Context = rootView.context
/**
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp
index f813b0d3b0b7..3f2603aec86a 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp
@@ -37,17 +37,51 @@ filegroup {
"src/**/B*.kt",
"src/**/C*.kt",
"src/**/D*.kt",
- "src/**/E*.kt",
],
}
filegroup {
name: "WMShellFlickerTestsSplitScreenGroup2-src",
srcs: [
+ "src/**/E*.kt",
+ ],
+}
+
+filegroup {
+ name: "WMShellFlickerTestsSplitScreenGroup3-src",
+ srcs: [
+ "src/**/S*.kt",
+ ],
+}
+
+filegroup {
+ name: "WMShellFlickerTestsSplitScreenGroupOther-src",
+ srcs: [
"src/**/*.kt",
],
}
+java_library {
+ name: "WMShellFlickerTestsSplitScreenBase",
+ srcs: [
+ ":WMShellFlickerTestsSplitScreenBase-src",
+ ],
+ static_libs: [
+ "WMShellFlickerTestsBase",
+ "wm-shell-flicker-utils",
+ "androidx.test.ext.junit",
+ "flickertestapplib",
+ "flickerlib",
+ "flickerlib-helpers",
+ "flickerlib-trace_processor_shell",
+ "platform-test-annotations",
+ "wm-flicker-common-app-helpers",
+ "wm-flicker-common-assertions",
+ "launcher-helper-lib",
+ "launcher-aosp-tapl",
+ ],
+}
+
android_test {
name: "WMShellFlickerTestsSplitScreenGroup1",
defaults: ["WMShellFlickerTestsDefault"],
@@ -56,25 +90,67 @@ android_test {
instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen",
test_config_template: "AndroidTestTemplate.xml",
srcs: [
- ":WMShellFlickerTestsSplitScreenBase-src",
":WMShellFlickerTestsSplitScreenGroup1-src",
],
- static_libs: ["WMShellFlickerTestsBase"],
+ static_libs: [
+ "WMShellFlickerTestsBase",
+ "WMShellFlickerTestsSplitScreenBase",
+ ],
data: ["trace_config/*"],
}
android_test {
name: "WMShellFlickerTestsSplitScreenGroup2",
+ defaults: ["WMShellFlickerTestsDefault"],
manifest: "AndroidManifest.xml",
package_name: "com.android.wm.shell.flicker.splitscreen",
instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen",
+ test_config_template: "AndroidTestTemplate.xml",
srcs: [
- ":WMShellFlickerTestsSplitScreenBase-src",
- ":WMShellFlickerTestsSplitScreenGroup2-src",
+ ":WMShellFlickerTestsSplitScreenGroup1-src",
+ ],
+ static_libs: [
+ "WMShellFlickerTestsBase",
+ "WMShellFlickerTestsSplitScreenBase",
+ ],
+ data: ["trace_config/*"],
+}
+
+android_test {
+ name: "WMShellFlickerTestsSplitScreenGroup3",
+ defaults: ["WMShellFlickerTestsDefault"],
+ manifest: "AndroidManifest.xml",
+ package_name: "com.android.wm.shell.flicker.splitscreen",
+ instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen",
+ test_config_template: "AndroidTestTemplate.xml",
+ srcs: [
+ ":WMShellFlickerTestsSplitScreenGroup1-src",
+ ],
+ static_libs: [
+ "WMShellFlickerTestsBase",
+ "WMShellFlickerTestsSplitScreenBase",
+ ],
+ data: ["trace_config/*"],
+}
+
+android_test {
+ name: "WMShellFlickerTestsSplitScreenGroupOther",
+ defaults: ["WMShellFlickerTestsDefault"],
+ manifest: "AndroidManifest.xml",
+ package_name: "com.android.wm.shell.flicker.splitscreen",
+ instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen",
+ test_config_template: "AndroidTestTemplate.xml",
+ srcs: [
+ ":WMShellFlickerTestsSplitScreenGroupOther-src",
],
exclude_srcs: [
":WMShellFlickerTestsSplitScreenGroup1-src",
+ ":WMShellFlickerTestsSplitScreenGroup2-src",
+ ":WMShellFlickerTestsSplitScreenGroup3-src",
+ ],
+ static_libs: [
+ "WMShellFlickerTestsBase",
+ "WMShellFlickerTestsSplitScreenBase",
],
- static_libs: ["WMShellFlickerTestsBase"],
data: ["trace_config/*"],
}
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt
new file mode 100644
index 000000000000..dad5db94d062
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.Presubmit
+import android.tools.Rotation
+import android.tools.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.flicker.legacy.FlickerBuilder
+import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.traces.component.ComponentNameMatcher
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitscreen.benchmark.MultipleShowImeRequestsInSplitScreenBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test quick switch between two split pairs.
+ *
+ * To run this test: `atest WMShellFlickerTestsSplitScreenGroup2:MultipleShowImeRequestsInSplitScreen`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class MultipleShowImeRequestsInSplitScreen(override val flicker: LegacyFlickerTest) :
+ MultipleShowImeRequestsInSplitScreenBenchmark(flicker), ICommonAssertions {
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @Presubmit
+ @Test
+ fun imeLayerAlwaysVisible() =
+ flicker.assertLayers {
+ this.isVisible(ComponentNameMatcher.IME)
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams() = LegacyFlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
index 90453640c91a..d34998815fca 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
@@ -47,7 +47,7 @@ import org.junit.runners.Parameterized
* To run this test: `atest WMShellFlickerTestsSplitScreen:UnlockKeyguardToSplitScreen`
*/
@RequiresDevice
-@Postsubmit
+@FlakyTest(bugId = 293578017)
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@@ -61,7 +61,6 @@ class UnlockKeyguardToSplitScreen(override val flicker: LegacyFlickerTest) :
}
@Test
- @FlakyTest(bugId = 293578017)
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt
new file mode 100644
index 000000000000..249253185607
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.wm.shell.flicker.splitscreen.benchmark
+
+import android.tools.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.flicker.legacy.FlickerBuilder
+import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+abstract class MultipleShowImeRequestsInSplitScreenBenchmark(
+ override val flicker: LegacyFlickerTest
+) : SplitScreenBase(flicker) {
+ override val primaryApp = ImeAppHelper(instrumentation)
+ override val defaultTeardown: FlickerBuilder.() -> Unit
+ get() = {
+ teardown {
+ primaryApp.closeIME(wmHelper)
+ super.defaultTeardown
+ }
+ }
+
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ SplitScreenUtils.enterSplit(
+ wmHelper,
+ tapl,
+ device,
+ primaryApp,
+ secondaryApp,
+ flicker.scenario.startRotation
+ )
+ // initially open the IME
+ primaryApp.openIME(wmHelper)
+ }
+ transitions {
+ for (i in 1..OPEN_IME_COUNT) {
+ primaryApp.openIME(wmHelper)
+ }
+ }
+ }
+
+ companion object {
+ const val OPEN_IME_COUNT = 30
+
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt
index 4b106034b2b5..51074f634e30 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt
@@ -25,7 +25,7 @@ import com.android.wm.shell.flicker.utils.SplitScreenUtils
abstract class SplitScreenBase(flicker: LegacyFlickerTest) : BaseBenchmarkTest(flicker) {
protected val context: Context = instrumentation.context
- protected val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+ protected open val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
protected val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
protected open val defaultSetup: FlickerBuilder.() -> Unit = {
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index 13f95ccea640..92be4f9f0374 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -46,6 +46,7 @@ android_test {
"androidx.dynamicanimation_dynamicanimation",
"dagger2",
"frameworks-base-testutils",
+ "kotlin-test",
"kotlinx-coroutines-android",
"kotlinx-coroutines-core",
"mockito-kotlin2",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
index ea522cdf2509..731f75bf9f5d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java
@@ -20,9 +20,11 @@ import static android.view.WindowManager.TRANSIT_OPEN;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
+import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationRunner.calculateParentBounds;
import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
@@ -32,14 +34,22 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.animation.Animator;
+import android.annotation.NonNull;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.window.TransitionInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.window.flags.Flags;
import com.android.wm.shell.transition.TransitionInfoBuilder;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
@@ -56,12 +66,16 @@ import java.util.ArrayList;
@RunWith(AndroidJUnit4.class)
public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase {
+ @Rule
+ public SetFlagsRule mRule = new SetFlagsRule();
+
@Before
public void setup() {
super.setUp();
doNothing().when(mController).onAnimationFinished(any());
}
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
public void testStartAnimation() {
final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
@@ -87,6 +101,7 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim
verify(mController).onAnimationFinished(mTransition);
}
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
public void testChangesBehindStartingWindow() {
final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
@@ -101,6 +116,7 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim
assertEquals(0, animator.getDuration());
}
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
public void testTransitionTypeDragResize() {
final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TASK_FRAGMENT_DRAG_RESIZE, 0)
@@ -115,10 +131,11 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim
assertEquals(0, animator.getDuration());
}
+ @DisableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
- public void testInvalidCustomAnimation() {
+ public void testInvalidCustomAnimation_disableAnimationOptionsPerChange() {
final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
- .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY))
+ .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY, TRANSIT_OPEN))
.build();
info.setAnimationOptions(TransitionInfo.AnimationOptions
.makeCustomAnimOptions("packageName", 0 /* enterResId */, 0 /* exitResId */,
@@ -131,4 +148,146 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim
// An invalid custom animation is equivalent to jump-cut.
assertEquals(0, animator.getDuration());
}
+
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
+ @Test
+ public void testInvalidCustomAnimation_enableAnimationOptionsPerChange() {
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
+ .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY, TRANSIT_OPEN))
+ .build();
+ info.getChanges().getFirst().setAnimationOptions(TransitionInfo.AnimationOptions
+ .makeCustomAnimOptions("packageName", 0 /* enterResId */, 0 /* exitResId */,
+ 0 /* backgroundColor */, false /* overrideTaskTransition */));
+ final Animator animator = mAnimRunner.createAnimator(
+ info, mStartTransaction, mFinishTransaction,
+ () -> mFinishCallback.onTransitionFinished(null /* wct */),
+ new ArrayList<>());
+
+ // An invalid custom animation is equivalent to jump-cut.
+ assertEquals(0, animator.getDuration());
+ }
+
+ @DisableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG)
+ @Test
+ public void testCalculateParentBounds_flagDisabled() {
+ final Rect parentBounds = new Rect(0, 0, 2000, 2000);
+ final Rect primaryBounds = new Rect();
+ final Rect secondaryBounds = new Rect();
+ parentBounds.splitVertically(primaryBounds, secondaryBounds);
+
+ final TransitionInfo.Change change = createChange(0 /* flags */);
+ change.setStartAbsBounds(secondaryBounds);
+
+ final TransitionInfo.Change boundsAnimationChange = createChange(0 /* flags */);
+ boundsAnimationChange.setStartAbsBounds(primaryBounds);
+ boundsAnimationChange.setEndAbsBounds(primaryBounds);
+ final Rect actualParentBounds = new Rect();
+
+ calculateParentBounds(change, boundsAnimationChange, actualParentBounds);
+
+ assertEquals(parentBounds, actualParentBounds);
+
+ actualParentBounds.setEmpty();
+
+ boundsAnimationChange.setStartAbsBounds(secondaryBounds);
+ boundsAnimationChange.setEndAbsBounds(primaryBounds);
+
+ calculateParentBounds(boundsAnimationChange, boundsAnimationChange, actualParentBounds);
+
+ assertEquals(parentBounds, actualParentBounds);
+ }
+
+ // TODO(b/243518738): Rewrite with TestParameter
+ @EnableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG)
+ @Test
+ public void testCalculateParentBounds_flagEnabled() {
+ TransitionInfo.Change change;
+ final TransitionInfo.Change stubChange = createChange(0 /* flags */);
+ final Rect actualParentBounds = new Rect();
+ Rect parentBounds = new Rect(0, 0, 2000, 2000);
+ Rect endAbsBounds = new Rect(0, 0, 2000, 2000);
+ change = prepareChangeForParentBoundsCalculationTest(
+ new Point(0, 0) /* endRelOffset */,
+ endAbsBounds,
+ new Point() /* endParentSize */
+ );
+
+ calculateParentBounds(change, stubChange, actualParentBounds);
+
+ assertTrue("Parent bounds must be empty because end parent size is not set.",
+ actualParentBounds.isEmpty());
+
+ String testString = "Parent start with (0, 0)";
+ change = prepareChangeForParentBoundsCalculationTest(
+ new Point(endAbsBounds.left - parentBounds.left,
+ endAbsBounds.top - parentBounds.top),
+ endAbsBounds, new Point(parentBounds.width(), parentBounds.height()));
+
+ calculateParentBounds(change, stubChange, actualParentBounds);
+
+ assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds,
+ actualParentBounds);
+
+ testString = "Container not start with (0, 0)";
+ parentBounds = new Rect(0, 0, 2000, 2000);
+ endAbsBounds = new Rect(1000, 500, 2000, 1500);
+ change = prepareChangeForParentBoundsCalculationTest(
+ new Point(endAbsBounds.left - parentBounds.left,
+ endAbsBounds.top - parentBounds.top),
+ endAbsBounds, new Point(parentBounds.width(), parentBounds.height()));
+
+ calculateParentBounds(change, stubChange, actualParentBounds);
+
+ assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds,
+ actualParentBounds);
+
+ testString = "Parent container on the right";
+ parentBounds = new Rect(1000, 0, 2000, 2000);
+ endAbsBounds = new Rect(1000, 500, 1500, 1500);
+ change = prepareChangeForParentBoundsCalculationTest(
+ new Point(endAbsBounds.left - parentBounds.left,
+ endAbsBounds.top - parentBounds.top),
+ endAbsBounds, new Point(parentBounds.width(), parentBounds.height()));
+
+ calculateParentBounds(change, stubChange, actualParentBounds);
+
+ assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds,
+ actualParentBounds);
+
+ testString = "Parent container on the bottom";
+ parentBounds = new Rect(0, 1000, 2000, 2000);
+ endAbsBounds = new Rect(500, 1500, 1500, 2000);
+ change = prepareChangeForParentBoundsCalculationTest(
+ new Point(endAbsBounds.left - parentBounds.left,
+ endAbsBounds.top - parentBounds.top),
+ endAbsBounds, new Point(parentBounds.width(), parentBounds.height()));
+
+ calculateParentBounds(change, stubChange, actualParentBounds);
+
+ assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds,
+ actualParentBounds);
+
+ testString = "Parent container in the middle";
+ parentBounds = new Rect(500, 500, 1500, 1500);
+ endAbsBounds = new Rect(1000, 500, 1500, 1000);
+ change = prepareChangeForParentBoundsCalculationTest(
+ new Point(endAbsBounds.left - parentBounds.left,
+ endAbsBounds.top - parentBounds.top),
+ endAbsBounds, new Point(parentBounds.width(), parentBounds.height()));
+
+ calculateParentBounds(change, stubChange, actualParentBounds);
+
+ assertEquals(testString + ": Parent bounds must be " + parentBounds, parentBounds,
+ actualParentBounds);
+ }
+
+ @NonNull
+ private static TransitionInfo.Change prepareChangeForParentBoundsCalculationTest(
+ @NonNull Point endRelOffset, @NonNull Rect endAbsBounds, @NonNull Point endParentSize) {
+ final TransitionInfo.Change change = createChange(0 /* flags */);
+ change.setEndRelOffset(endRelOffset.x, endRelOffset.y);
+ change.setEndAbsBounds(endAbsBounds);
+ change.setEndParentSize(endParentSize.x, endParentSize.y);
+ return change;
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
index 0b2265d4ce9c..c18d7ec821b6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationTestBase.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.activityembedding;
+import static android.view.WindowManager.TRANSIT_NONE;
import static android.window.TransitionInfo.FLAG_FILLS_TASK;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
@@ -31,6 +32,7 @@ import android.annotation.NonNull;
import android.graphics.Rect;
import android.os.IBinder;
import android.view.SurfaceControl;
+import android.view.WindowManager;
import android.window.TransitionInfo;
import android.window.WindowContainerToken;
@@ -82,11 +84,27 @@ abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase {
spyOn(mFinishCallback);
}
- /** Creates a mock {@link TransitionInfo.Change}. */
+ /**
+ * Creates a mock {@link TransitionInfo.Change}.
+ *
+ * @param flags the {@link TransitionInfo.ChangeFlags} of the change
+ */
static TransitionInfo.Change createChange(@TransitionInfo.ChangeFlags int flags) {
+ return createChange(flags, TRANSIT_NONE);
+ }
+
+ /**
+ * Creates a mock {@link TransitionInfo.Change}.
+ *
+ * @param flags the {@link TransitionInfo.ChangeFlags} of the change
+ * @param mode the transition mode of the change
+ */
+ static TransitionInfo.Change createChange(@TransitionInfo.ChangeFlags int flags,
+ @WindowManager.TransitionType int mode) {
TransitionInfo.Change c = new TransitionInfo.Change(mock(WindowContainerToken.class),
mock(SurfaceControl.class));
c.setFlags(flags);
+ c.setMode(mode);
return c;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
index 974d69b2ac5d..39d55079ca3a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
@@ -32,6 +32,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.graphics.Rect;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
@@ -39,9 +42,11 @@ import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.window.flags.Flags;
import com.android.wm.shell.transition.TransitionInfoBuilder;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -59,6 +64,9 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
private static final Rect EMBEDDED_LEFT_BOUNDS = new Rect(0, 0, 500, 500);
private static final Rect EMBEDDED_RIGHT_BOUNDS = new Rect(500, 0, 1000, 500);
+ @Rule
+ public SetFlagsRule mRule = new SetFlagsRule();
+
@Before
public void setup() {
super.setUp();
@@ -66,11 +74,13 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
any());
}
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
public void testInstantiate() {
verify(mShellInit).addInitCallback(any(), any());
}
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
public void testOnInit() {
mController.onInit();
@@ -78,6 +88,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
verify(mTransitions).addHandler(mController);
}
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
public void testSetAnimScaleSetting() {
mController.setAnimScaleSetting(1.0f);
@@ -86,6 +97,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
verify(mAnimSpec).setAnimScaleSetting(1.0f);
}
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
public void testStartAnimation_containsNonActivityEmbeddingChange() {
final TransitionInfo.Change nonEmbeddedOpen = createChange(0 /* flags */);
@@ -122,6 +134,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
assertFalse(info2.getChanges().contains(nonEmbeddedClose));
}
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
public void testStartAnimation_containsOnlyFillTaskActivityEmbeddingChange() {
final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
@@ -138,6 +151,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
verifyNoMoreInteractions(mFinishCallback);
}
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
public void testStartAnimation_containsActivityEmbeddingSplitChange() {
// Change that occupies only part of the Task.
@@ -155,6 +169,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
verifyNoMoreInteractions(mFinishTransaction);
}
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
public void testStartAnimation_containsChangeEnterActivityEmbeddingSplit() {
// Change that is entering ActivityEmbedding split.
@@ -171,6 +186,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
verifyNoMoreInteractions(mFinishTransaction);
}
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
public void testStartAnimation_containsChangeExitActivityEmbeddingSplit() {
// Change that is exiting ActivityEmbedding split.
@@ -187,8 +203,9 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
verifyNoMoreInteractions(mFinishTransaction);
}
+ @DisableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
- public void testShouldAnimate_containsAnimationOptions() {
+ public void testShouldAnimate_containsAnimationOptions_disableAnimOptionsPerChange() {
final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
.addChange(createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS, TASK_BOUNDS, TASK_BOUNDS))
.build();
@@ -206,6 +223,28 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
assertFalse(mController.shouldAnimate(info));
}
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
+ @Test
+ public void testShouldAnimate_containsAnimationOptions_enableAnimOptionsPerChange() {
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0)
+ .addChange(createEmbeddedChange(EMBEDDED_RIGHT_BOUNDS, TASK_BOUNDS, TASK_BOUNDS))
+ .build();
+ final TransitionInfo.Change change = info.getChanges().getFirst();
+
+ change.setAnimationOptions(TransitionInfo.AnimationOptions
+ .makeCustomAnimOptions("packageName", 0 /* enterResId */, 0 /* exitResId */,
+ 0 /* backgroundColor */, false /* overrideTaskTransition */));
+ assertTrue(mController.shouldAnimate(info));
+
+ change.setAnimationOptions(TransitionInfo.AnimationOptions
+ .makeSceneTransitionAnimOptions());
+ assertFalse(mController.shouldAnimate(info));
+
+ change.setAnimationOptions(TransitionInfo.AnimationOptions.makeCrossProfileAnimOptions());
+ assertFalse(mController.shouldAnimate(info));
+ }
+
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@UiThreadTest
@Test
public void testMergeAnimation() {
@@ -242,6 +281,7 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation
verify(mFinishCallback).onTransitionFinished(any());
}
+ @EnableFlags(Flags.FLAG_MOVE_ANIMATION_OPTIONS_TO_CHANGE)
@Test
public void testOnAnimationFinished() {
// Should not call finish when there is no transition.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index f6f3aa49bc6e..57e469d5cbd2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -123,6 +123,7 @@ public class BackAnimationControllerTest extends ShellTestCase {
private DefaultCrossActivityBackAnimation mDefaultCrossActivityBackAnimation;
private CrossTaskBackAnimation mCrossTaskBackAnimation;
private ShellBackAnimationRegistry mShellBackAnimationRegistry;
+ private Rect mTouchableRegion;
@Before
public void setUp() throws Exception {
@@ -158,6 +159,8 @@ public class BackAnimationControllerTest extends ShellTestCase {
mShellCommandHandler);
mShellInit.init();
mShellExecutor.flushAll();
+ mTouchableRegion = new Rect(0, 0, 100, 100);
+ mController.mTouchableArea.set(mTouchableRegion);
}
private void createNavigationInfo(int backType,
@@ -169,7 +172,8 @@ public class BackAnimationControllerTest extends ShellTestCase {
.setOnBackNavigationDone(new RemoteCallback((bundle) -> {}))
.setOnBackInvokedCallback(mAppCallback)
.setPrepareRemoteAnimation(enableAnimation)
- .setAnimationCallback(isAnimationCallback);
+ .setAnimationCallback(isAnimationCallback)
+ .setTouchableRegion(mTouchableRegion);
createNavigationInfo(builder);
}
@@ -234,7 +238,8 @@ public class BackAnimationControllerTest extends ShellTestCase {
.setType(type)
.setOnBackInvokedCallback(mAppCallback)
.setPrepareRemoteAnimation(true)
- .setOnBackNavigationDone(new RemoteCallback(result)));
+ .setOnBackNavigationDone(new RemoteCallback(result))
+ .setTouchableRegion(mTouchableRegion));
triggerBackGesture();
simulateRemoteAnimationStart();
mShellExecutor.flushAll();
@@ -512,7 +517,8 @@ public class BackAnimationControllerTest extends ShellTestCase {
.setType(type)
.setOnBackInvokedCallback(mAppCallback)
.setPrepareRemoteAnimation(true)
- .setOnBackNavigationDone(new RemoteCallback(result)));
+ .setOnBackNavigationDone(new RemoteCallback(result))
+ .setTouchableRegion(mTouchableRegion));
triggerBackGesture();
simulateRemoteAnimationStart();
mShellExecutor.flushAll();
@@ -543,7 +549,9 @@ public class BackAnimationControllerTest extends ShellTestCase {
createNavigationInfo(new BackNavigationInfo.Builder()
.setType(type)
.setOnBackInvokedCallback(mAppCallback)
- .setOnBackNavigationDone(new RemoteCallback(result)));
+ .setOnBackNavigationDone(new RemoteCallback(result))
+ .setTouchableRegion(mTouchableRegion)
+ .setAppProgressAllowed(true));
triggerBackGesture();
mShellExecutor.flushAll();
releaseBackGesture();
@@ -570,7 +578,8 @@ public class BackAnimationControllerTest extends ShellTestCase {
createNavigationInfo(new BackNavigationInfo.Builder()
.setType(type)
.setOnBackInvokedCallback(mAppCallback)
- .setOnBackNavigationDone(new RemoteCallback(result)));
+ .setOnBackNavigationDone(new RemoteCallback(result))
+ .setTouchableRegion(mTouchableRegion));
doMotionEvent(MotionEvent.ACTION_CANCEL, 0);
mShellExecutor.flushAll();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java
index 8932e60048e6..4d0348b4f470 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java
@@ -19,6 +19,7 @@ package com.android.wm.shell.back;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import android.os.Handler;
import android.os.Looper;
@@ -95,16 +96,33 @@ public class BackProgressAnimatorTest {
// Trigger animation cancel, the target progress should be 0.
mTargetProgress = 0;
mTargetProgressCalled = new CountDownLatch(1);
- CountDownLatch cancelCallbackCalled = new CountDownLatch(1);
+ CountDownLatch finishCallbackCalled = new CountDownLatch(1);
mMainThreadHandler.post(
- () -> mProgressAnimator.onBackCancelled(() -> cancelCallbackCalled.countDown()));
- cancelCallbackCalled.await(1, TimeUnit.SECONDS);
+ () -> mProgressAnimator.onBackCancelled(finishCallbackCalled::countDown));
+ finishCallbackCalled.await(1, TimeUnit.SECONDS);
mTargetProgressCalled.await(1, TimeUnit.SECONDS);
assertNotNull(mReceivedBackEvent);
assertEquals(mReceivedBackEvent.getProgress(), mTargetProgress, 0 /* delta */);
}
@Test
+ public void testBackInvoked() throws InterruptedException {
+ // Give the animator some progress.
+ final BackMotionEvent backEvent = backMotionEventFrom(100, mTargetProgress);
+ mMainThreadHandler.post(
+ () -> mProgressAnimator.onBackProgressed(backEvent));
+ mTargetProgressCalled.await(1, TimeUnit.SECONDS);
+ assertNotNull(mReceivedBackEvent);
+
+ // Trigger back invoked animation
+ CountDownLatch finishCallbackCalled = new CountDownLatch(1);
+ mMainThreadHandler.post(
+ () -> mProgressAnimator.onBackInvoked(finishCallbackCalled::countDown));
+ assertTrue("onBackInvoked finishCallback never called",
+ finishCallbackCalled.await(1, TimeUnit.SECONDS));
+ }
+
+ @Test
public void testResetCallsCancelCallbackImmediately() throws InterruptedException {
// Give the animator some progress.
final BackMotionEvent backEvent = backMotionEventFrom(100, mTargetProgress);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
index 0f433770777e..93e405131a58 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java
@@ -1170,9 +1170,9 @@ public class BubbleDataTest extends ShellTestCase {
// Verify the update has the removals.
BubbleData.Update update = mUpdateCaptor.getValue();
assertThat(update.removedBubbles.get(0)).isEqualTo(
- Pair.create(mBubbleA2, Bubbles.DISMISS_USER_REMOVED));
+ Pair.create(mBubbleA2, Bubbles.DISMISS_USER_ACCOUNT_REMOVED));
assertThat(update.removedBubbles.get(1)).isEqualTo(
- Pair.create(mBubbleA1, Bubbles.DISMISS_USER_REMOVED));
+ Pair.create(mBubbleA1, Bubbles.DISMISS_USER_ACCOUNT_REMOVED));
// Verify no A bubbles in active or overflow.
assertBubbleListContains(mBubbleC1, mBubbleB3);
@@ -1203,6 +1203,25 @@ public class BubbleDataTest extends ShellTestCase {
assertThat(update.currentBubbleList.get(0).getKey()).isEqualTo(mEntryA2.getKey());
assertThat(update.currentBubbleList.get(1).getKey()).isEqualTo(mEntryA1.getKey());
assertThat(update.bubbleBarLocation).isEqualTo(BubbleBarLocation.LEFT);
+ assertThat(update.expandedChanged).isFalse();
+ assertThat(update.selectedBubbleKey).isEqualTo(mEntryA2.getKey());
+ }
+
+ @Test
+ public void test_getInitialStateForBubbleBar_includesExpandedState() {
+ sendUpdatedEntryAtTime(mEntryA1, 1000);
+ sendUpdatedEntryAtTime(mEntryA2, 2000);
+ mPositioner.setBubbleBarLocation(BubbleBarLocation.LEFT);
+ mBubbleData.setExpanded(true);
+
+ BubbleBarUpdate update = mBubbleData.getInitialStateForBubbleBar();
+ assertThat(update.currentBubbleList).hasSize(2);
+ assertThat(update.currentBubbleList.get(0).getKey()).isEqualTo(mEntryA2.getKey());
+ assertThat(update.currentBubbleList.get(1).getKey()).isEqualTo(mEntryA1.getKey());
+ assertThat(update.bubbleBarLocation).isEqualTo(BubbleBarLocation.LEFT);
+ assertThat(update.expandedChanged).isTrue();
+ assertThat(update.expanded).isTrue();
+ assertThat(update.selectedBubbleKey).isEqualTo(mEntryA2.getKey());
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt
index 432909f18813..5b22eddcb6ee 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleInfoTest.kt
@@ -32,7 +32,17 @@ class BubbleInfoTest : ShellTestCase() {
@Test
fun bubbleInfo() {
val bubbleInfo =
- BubbleInfo("key", 0, "shortcut id", null, 6, "com.some.package", "title", true)
+ BubbleInfo(
+ "key",
+ 0,
+ "shortcut id",
+ null,
+ 6,
+ "com.some.package",
+ "title",
+ "Some app",
+ true
+ )
val parcel = Parcel.obtain()
bubbleInfo.writeToParcel(parcel, PARCELABLE_WRITE_RETURN_VALUE)
parcel.setDataPosition(0)
@@ -46,6 +56,7 @@ class BubbleInfoTest : ShellTestCase() {
assertThat(bubbleInfo.userId).isEqualTo(bubbleInfoFromParcel.userId)
assertThat(bubbleInfo.packageName).isEqualTo(bubbleInfoFromParcel.packageName)
assertThat(bubbleInfo.title).isEqualTo(bubbleInfoFromParcel.title)
+ assertThat(bubbleInfo.appName).isEqualTo(bubbleInfoFromParcel.appName)
assertThat(bubbleInfo.isImportantConversation)
.isEqualTo(bubbleInfoFromParcel.isImportantConversation)
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
index 2a2483df0792..665bed0c8a88 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -19,6 +19,8 @@ import android.app.ActivityManager
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.content.Context
+import android.graphics.Point
+import android.graphics.Rect
import android.os.IBinder
import android.testing.AndroidTestingRunner
import android.view.SurfaceControl
@@ -36,30 +38,41 @@ import android.window.TransitionInfo
import android.window.TransitionInfo.Change
import android.window.WindowContainerToken
import androidx.test.filters.SmallTest
-import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
import com.android.modules.utils.testing.ExtendedMockitoRule
+import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN
import com.android.wm.shell.shared.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.TransitionInfoBuilder
import com.android.wm.shell.transition.Transitions
-import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertNotNull
+import junit.framework.Assert.assertNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.same
+import org.mockito.kotlin.spy
import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyZeroInteractions
+import org.mockito.kotlin.whenever
/**
* Test class for {@link DesktopModeLoggerTransitionObserver}
@@ -68,301 +81,613 @@ import org.mockito.kotlin.times
*/
@SmallTest
@RunWith(AndroidTestingRunner::class)
-class DesktopModeLoggerTransitionObserverTest {
-
- @JvmField
- @Rule
- val extendedMockitoRule = ExtendedMockitoRule.Builder(this)
- .mockStatic(DesktopModeEventLogger::class.java)
- .mockStatic(DesktopModeStatus::class.java).build()!!
-
- @Mock
- lateinit var testExecutor: ShellExecutor
- @Mock
- private lateinit var mockShellInit: ShellInit
- @Mock
- private lateinit var transitions: Transitions
- @Mock
- private lateinit var context: Context
-
- private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver
- private lateinit var shellInit: ShellInit
- private lateinit var desktopModeEventLogger: DesktopModeEventLogger
-
- @Before
- fun setup() {
- doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) }
- shellInit = Mockito.spy(ShellInit(testExecutor))
- desktopModeEventLogger = mock(DesktopModeEventLogger::class.java)
-
- transitionObserver = DesktopModeLoggerTransitionObserver(
+class DesktopModeLoggerTransitionObserverTest : ShellTestCase() {
+
+ @JvmField
+ @Rule
+ val extendedMockitoRule =
+ ExtendedMockitoRule.Builder(this).mockStatic(DesktopModeStatus::class.java).build()!!
+
+ private val testExecutor = mock<ShellExecutor>()
+ private val mockShellInit = mock<ShellInit>()
+ private val transitions = mock<Transitions>()
+ private val context = mock<Context>()
+
+ private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver
+ private lateinit var shellInit: ShellInit
+ private lateinit var desktopModeEventLogger: DesktopModeEventLogger
+
+ @Before
+ fun setup() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true)
+ shellInit = spy(ShellInit(testExecutor))
+ desktopModeEventLogger = mock<DesktopModeEventLogger>()
+
+ transitionObserver =
+ DesktopModeLoggerTransitionObserver(
context, mockShellInit, transitions, desktopModeEventLogger)
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- val initRunnableCaptor = ArgumentCaptor.forClass(
- Runnable::class.java)
- verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(),
- same(transitionObserver))
- initRunnableCaptor.value.run()
- } else {
- transitionObserver.onInit()
- }
- }
-
- @Test
- fun testRegistersObserverAtInit() {
- verify(transitions)
- .registerObserver(same(
- transitionObserver))
- }
-
- @Test
- fun taskCreated_notFreeformWindow_doesNotLogSessionEnterOrTaskAdded() {
- val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
- val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
-
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
- verify(desktopModeEventLogger, never()).logTaskAdded(any(), any())
- }
-
- @Test
- fun taskCreated_FreeformWindowOpen_logSessionEnterAndTaskAdded() {
- val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
-
- callOnTransitionReady(transitionInfo)
- val sessionId = transitionObserver.getLoggerSessionId()
-
- assertThat(sessionId).isNotNull()
- verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
- eq(EnterReason.APP_FREEFORM_INTENT))
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- }
-
- @Test
- fun taskChanged_taskMovedToDesktopByDrag_logSessionEnterAndTaskAdded() {
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- // task change is finalised when drag ends
- val transitionInfo = TransitionInfoBuilder(
- Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0).addChange(change).build()
-
- callOnTransitionReady(transitionInfo)
- val sessionId = transitionObserver.getLoggerSessionId()
-
- assertThat(sessionId).isNotNull()
- verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
- eq(EnterReason.APP_HANDLE_DRAG))
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- }
-
- @Test
- fun taskChanged_taskMovedToDesktopByButtonTap_logSessionEnterAndTaskAdded() {
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0)
- .addChange(change).build()
-
- callOnTransitionReady(transitionInfo)
- val sessionId = transitionObserver.getLoggerSessionId()
-
- assertThat(sessionId).isNotNull()
- verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
- eq(EnterReason.APP_HANDLE_MENU_BUTTON))
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- }
-
- @Test
- fun taskChanged_existingFreeformTaskMadeVisible_logSessionEnterAndTaskAdded() {
- val taskInfo = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
- taskInfo.isVisibleRequested = true
- val change = createChange(TRANSIT_CHANGE, taskInfo)
- val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0)
- .addChange(change).build()
-
- callOnTransitionReady(transitionInfo)
- val sessionId = transitionObserver.getLoggerSessionId()
-
- assertThat(sessionId).isNotNull()
- verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
- eq(EnterReason.APP_HANDLE_MENU_BUTTON))
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- }
-
- @Test
- fun taskToFront_screenWake_logSessionStartedAndTaskAdded() {
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0)
- .addChange(change).build()
-
- callOnTransitionReady(transitionInfo)
- val sessionId = transitionObserver.getLoggerSessionId()
-
- assertThat(sessionId).isNotNull()
- verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
- eq(EnterReason.SCREEN_ON))
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- }
-
- @Test
- fun freeformTaskVisible_screenTurnOff_logSessionExitAndTaskRemoved_sessionIdNull() {
- val sessionId = 1
- // add a freeform task
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
- eq(ExitReason.SCREEN_OFF))
- assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+ verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
+ initRunnableCaptor.value.run()
+ } else {
+ transitionObserver.onInit()
}
+ }
+
+ @Test
+ fun testRegistersObserverAtInit() {
+ verify(transitions).registerObserver(same(transitionObserver))
+ }
+
+ @Test
+ fun transitOpen_notFreeformWindow_doesNotLogTaskAddedOrSessionEnter() {
+ val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
+ verify(desktopModeEventLogger, never()).logTaskAdded(any(), any())
+ }
- @Test
- fun freeformTaskVisible_exitDesktopUsingDrag_logSessionExitAndTaskRemoved_sessionIdNull() {
- val sessionId = 1
- // add a freeform task
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // window mode changing from FREEFORM to FULLSCREEN
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
- val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_EXIT_DESKTOP_MODE)
- .addChange(change).build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
- eq(ExitReason.DRAG_TO_EXIT))
- assertThat(transitionObserver.getLoggerSessionId()).isNull()
- }
-
- @Test
- fun freeformTaskVisible_exitDesktopBySwipeUp_logSessionExitAndTaskRemoved_sessionIdNull() {
- val sessionId = 1
- // add a freeform task
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // recents transition
- val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
- .addChange(change).build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
- eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
- assertThat(transitionObserver.getLoggerSessionId()).isNull()
- }
+ @Test
+ fun transitOpen_logTaskAddedAndEnterReasonAppFreeformIntent() {
+ val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
- @Test
- fun freeformTaskVisible_taskFinished_logSessionExitAndTaskRemoved_sessionIdNull() {
- val sessionId = 1
- // add a freeform task
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // task closing
- val change = createChange(TRANSIT_CLOSE, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
- val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
- eq(ExitReason.TASK_FINISHED))
- assertThat(transitionObserver.getLoggerSessionId()).isNull()
- }
-
- @Test
- fun sessionExitByRecents_cancelledAnimation_sessionRestored() {
- val sessionId = 1
- // add a freeform task to an existing session
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // recents transition sent freeform window to back
- val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- val transitionInfo1 =
- TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change)
- .build()
- callOnTransitionReady(transitionInfo1)
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
- eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
- assertThat(transitionObserver.getLoggerSessionId()).isNull()
-
- val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build()
- callOnTransitionReady(transitionInfo2)
-
- verify(desktopModeEventLogger, times(1)).logSessionEnter(any(), any())
- verify(desktopModeEventLogger, times(1)).logTaskAdded(any(), any())
- }
-
- @Test
- fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() {
- val sessionId = 1
- // add an existing freeform task
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // new freeform task added
- val change = createChange(TRANSIT_OPEN, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
- val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
- }
-
- @Test
- fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() {
- val sessionId = 1
- // add two existing freeform tasks
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(2, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // new freeform task added
- val change = createChange(TRANSIT_CLOSE, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
- val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, never()).logSessionExit(any(), any())
- }
-
- /**
- * Simulate calling the onTransitionReady() method
- */
- private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
- val transition = mock(IBinder::class.java)
- val startT = mock(
- SurfaceControl.Transaction::class.java)
- val finishT = mock(
- SurfaceControl.Transaction::class.java)
-
- transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT)
- }
-
- companion object {
- fun createTaskInfo(taskId: Int, windowMode: Int): ActivityManager.RunningTaskInfo {
- val taskInfo = ActivityManager.RunningTaskInfo()
- taskInfo.taskId = taskId
- taskInfo.configuration.windowConfiguration.windowingMode = windowMode
-
- return taskInfo
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskAddedAndEnterLogging(EnterReason.APP_FREEFORM_INTENT, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitEndDragToDesktop_logTaskAddedAndEnterReasonAppHandleDrag() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+ // task change is finalised when drag ends
+ val transitionInfo =
+ TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0)
+ .addChange(change)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_DRAG, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitEnterDesktopByButtonTap_logTaskAddedAndEnterReasonButtonTap() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, 0)
+ .addChange(change)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_MENU_BUTTON, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitEnterDesktopFromAppFromOverview_logTaskAddedAndEnterReasonAppFromOverview() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0)
+ .addChange(change)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitEnterDesktopFromKeyboardShortcut_logTaskAddedAndEnterReasonKeyboardShortcut() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, 0)
+ .addChange(change)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskAddedAndEnterLogging(EnterReason.KEYBOARD_SHORTCUT_ENTER, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitToFront_logTaskAddedAndEnterReasonOverview() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitToFront_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() {
+ // previous exit to overview transition
+ val previousSessionId = 1
+ // add a freeform task
+ val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+ transitionObserver.addTaskInfosToCachedMap(previousTaskInfo)
+ transitionObserver.setLoggerSessionId(previousSessionId)
+ val previousTransitionInfo =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+ .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo))
+ .build()
+
+ callOnTransitionReady(previousTransitionInfo)
+
+ verifyTaskRemovedAndExitLogging(
+ previousSessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE)
+
+ // Enter desktop mode from cancelled recents has no transition. Enter is detected on the
+ // next transition involving freeform windows
+
+ // TRANSIT_TO_FRONT
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitChange_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() {
+ // previous exit to overview transition
+ val previousSessionId = 1
+ // add a freeform task
+ val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+ transitionObserver.addTaskInfosToCachedMap(previousTaskInfo)
+ transitionObserver.setLoggerSessionId(previousSessionId)
+ val previousTransitionInfo =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+ .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo))
+ .build()
+
+ callOnTransitionReady(previousTransitionInfo)
+
+ verifyTaskRemovedAndExitLogging(
+ previousSessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE)
+
+ // Enter desktop mode from cancelled recents has no transition. Enter is detected on the
+ // next transition involving freeform windows
+
+ // TRANSIT_CHANGE
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_CHANGE, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitOpen_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() {
+ // previous exit to overview transition
+ val previousSessionId = 1
+ // add a freeform task
+ val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+ transitionObserver.addTaskInfosToCachedMap(previousTaskInfo)
+ transitionObserver.setLoggerSessionId(previousSessionId)
+ val previousTransitionInfo =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+ .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo))
+ .build()
+
+ callOnTransitionReady(previousTransitionInfo)
+
+ verifyTaskRemovedAndExitLogging(
+ previousSessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE)
+
+ // Enter desktop mode from cancelled recents has no transition. Enter is detected on the
+ // next transition involving freeform windows
+
+ // TRANSIT_OPEN
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ @Suppress("ktlint:standard:max-line-length")
+ fun transitEnterDesktopFromAppFromOverview_previousTransitionExitToOverview_logTaskAddedAndEnterReasonAppFromOverview() {
+ // Tests for AppFromOverview precedence in compared to cancelled Overview
+
+ // previous exit to overview transition
+ val previousSessionId = 1
+ // add a freeform task
+ val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+ transitionObserver.addTaskInfosToCachedMap(previousTaskInfo)
+ transitionObserver.setLoggerSessionId(previousSessionId)
+ val previousTransitionInfo =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+ .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo))
+ .build()
+
+ callOnTransitionReady(previousTransitionInfo)
+
+ verifyTaskRemovedAndExitLogging(
+ previousSessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE)
+
+ // TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0)
+ .addChange(change)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitEnterDesktopFromUnknown_logTaskAddedAndEnterReasonUnknown() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskAddedAndEnterLogging(EnterReason.UNKNOWN_ENTER, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitWake_logTaskAddedAndEnterReasonScreenOn() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskAddedAndEnterLogging(EnterReason.SCREEN_ON, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitSleep_logTaskRemovedAndExitReasonScreenOff_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build()
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskRemovedAndExitLogging(sessionId, ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitExitDesktopTaskDrag_logTaskRemovedAndExitReasonDragToExit_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // window mode changing from FREEFORM to FULLSCREEN
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskRemovedAndExitLogging(sessionId, ExitReason.DRAG_TO_EXIT, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitExitDesktopAppHandleButton_logTaskRemovedAndExitReasonButton_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // window mode changing from FREEFORM to FULLSCREEN
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON)
+ .addChange(change)
+ .build()
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskRemovedAndExitLogging(
+ sessionId, ExitReason.APP_HANDLE_MENU_BUTTON_EXIT, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitExitDesktopUsingKeyboard_logTaskRemovedAndExitReasonKeyboard_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // window mode changing from FREEFORM to FULLSCREEN
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskRemovedAndExitLogging(
+ sessionId, ExitReason.KEYBOARD_SHORTCUT_EXIT, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitExitDesktopUnknown_logTaskRemovedAndExitReasonUnknown_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // window mode changing from FREEFORM to FULLSCREEN
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskRemovedAndExitLogging(sessionId, ExitReason.UNKNOWN_EXIT, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitToFrontWithFlagRecents_logTaskRemovedAndExitReasonOverview_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // recents transition
+ val change = createChange(TRANSIT_TO_BACK, createTaskInfo(WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskRemovedAndExitLogging(
+ sessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun transitClose_logTaskRemovedAndExitReasonTaskFinished_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // task closing
+ val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verifyTaskRemovedAndExitLogging(sessionId, ExitReason.TASK_FINISHED, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun sessionExitByRecents_cancelledAnimation_sessionRestored() {
+ val sessionId = 1
+ // add a freeform task to an existing session
+ val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+ transitionObserver.addTaskInfosToCachedMap(taskInfo)
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // recents transition sent freeform window to back
+ val change = createChange(TRANSIT_TO_BACK, taskInfo)
+ val transitionInfo1 =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build()
+ callOnTransitionReady(transitionInfo1)
+
+ verifyTaskRemovedAndExitLogging(
+ sessionId, ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE)
+
+ val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build()
+ callOnTransitionReady(transitionInfo2)
+
+ verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, DEFAULT_TASK_UPDATE)
+ }
+
+ @Test
+ fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() {
+ val sessionId = 1
+ // add an existing freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // new freeform task added
+ val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1))
+ .logTaskAdded(eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2)))
+ verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
+ }
+
+ @Test
+ fun sessionAlreadyStarted_taskPositionChanged_logsTaskUpdate() {
+ val sessionId = 1
+ // add an existing freeform task
+ val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+ transitionObserver.addTaskInfosToCachedMap(taskInfo)
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // task position changed
+ val newTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100)
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_CHANGE, 0)
+ .addChange(createChange(TRANSIT_CHANGE, newTaskInfo))
+ .build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1))
+ .logTaskInfoChanged(
+ eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100)))
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun sessionAlreadyStarted_taskResized_logsTaskUpdate() {
+ val sessionId = 1
+ // add an existing freeform task
+ val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM)
+ transitionObserver.addTaskInfosToCachedMap(taskInfo)
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // task resized
+ val newTaskInfo =
+ createTaskInfo(
+ WINDOWING_MODE_FREEFORM,
+ taskWidth = DEFAULT_TASK_WIDTH + 100,
+ taskHeight = DEFAULT_TASK_HEIGHT - 100)
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_CHANGE, 0)
+ .addChange(createChange(TRANSIT_CHANGE, newTaskInfo))
+ .build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1))
+ .logTaskInfoChanged(
+ eq(sessionId),
+ eq(
+ DEFAULT_TASK_UPDATE.copy(
+ taskWidth = DEFAULT_TASK_WIDTH + 100, taskHeight = DEFAULT_TASK_HEIGHT - 100)))
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun sessionAlreadyStarted_multipleTasksUpdated_logsTaskUpdateForCorrectTask() {
+ val sessionId = 1
+ // add 2 existing freeform task
+ val taskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM)
+ val taskInfo2 = createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)
+ transitionObserver.addTaskInfosToCachedMap(taskInfo1)
+ transitionObserver.addTaskInfosToCachedMap(taskInfo2)
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // task 1 position update
+ val newTaskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100)
+ val transitionInfo1 =
+ TransitionInfoBuilder(TRANSIT_CHANGE, 0)
+ .addChange(createChange(TRANSIT_CHANGE, newTaskInfo1))
+ .build()
+ callOnTransitionReady(transitionInfo1)
+
+ verify(desktopModeEventLogger, times(1))
+ .logTaskInfoChanged(
+ eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100)))
+ verifyZeroInteractions(desktopModeEventLogger)
+
+ // task 2 resize
+ val newTaskInfo2 =
+ createTaskInfo(
+ WINDOWING_MODE_FREEFORM,
+ id = 2,
+ taskWidth = DEFAULT_TASK_WIDTH + 100,
+ taskHeight = DEFAULT_TASK_HEIGHT - 100)
+ val transitionInfo2 =
+ TransitionInfoBuilder(TRANSIT_CHANGE, 0)
+ .addChange(createChange(TRANSIT_CHANGE, newTaskInfo2))
+ .build()
+
+ callOnTransitionReady(transitionInfo2)
+
+ verify(desktopModeEventLogger, times(1))
+ .logTaskInfoChanged(
+ eq(sessionId),
+ eq(
+ DEFAULT_TASK_UPDATE.copy(
+ instanceId = 2,
+ taskWidth = DEFAULT_TASK_WIDTH + 100,
+ taskHeight = DEFAULT_TASK_HEIGHT - 100)))
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() {
+ val sessionId = 1
+ // add two existing freeform tasks
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM))
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // new freeform task closed
+ val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1))
+ .logTaskRemoved(eq(sessionId), eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2)))
+ verify(desktopModeEventLogger, never()).logSessionExit(any(), any())
+ }
+
+ /** Simulate calling the onTransitionReady() method */
+ private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
+ val transition = mock<IBinder>()
+ val startT = mock<SurfaceControl.Transaction>()
+ val finishT = mock<SurfaceControl.Transaction>()
+
+ transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT)
+ }
+
+ private fun verifyTaskAddedAndEnterLogging(enterReason: EnterReason, taskUpdate: TaskUpdate) {
+ val sessionId = transitionObserver.getLoggerSessionId()
+ assertNotNull(sessionId)
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!), eq(enterReason))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), eq(taskUpdate))
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ private fun verifyTaskRemovedAndExitLogging(
+ sessionId: Int,
+ exitReason: ExitReason,
+ taskUpdate: TaskUpdate
+ ) {
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), eq(taskUpdate))
+ verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId), eq(exitReason))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertNull(transitionObserver.getLoggerSessionId())
+ }
+
+ private companion object {
+ const val DEFAULT_TASK_ID = 1
+ const val DEFAULT_TASK_UID = 2
+ const val DEFAULT_TASK_HEIGHT = 100
+ const val DEFAULT_TASK_WIDTH = 200
+ const val DEFAULT_TASK_X = 30
+ const val DEFAULT_TASK_Y = 70
+ val DEFAULT_TASK_UPDATE =
+ TaskUpdate(
+ DEFAULT_TASK_ID,
+ DEFAULT_TASK_UID,
+ DEFAULT_TASK_HEIGHT,
+ DEFAULT_TASK_WIDTH,
+ DEFAULT_TASK_X,
+ DEFAULT_TASK_Y,
+ )
+
+ fun createTaskInfo(
+ windowMode: Int,
+ id: Int = DEFAULT_TASK_ID,
+ uid: Int = DEFAULT_TASK_UID,
+ taskHeight: Int = DEFAULT_TASK_HEIGHT,
+ taskWidth: Int = DEFAULT_TASK_WIDTH,
+ taskX: Int = DEFAULT_TASK_X,
+ taskY: Int = DEFAULT_TASK_Y,
+ ) =
+ ActivityManager.RunningTaskInfo().apply {
+ taskId = id
+ userId = uid
+ configuration.windowConfiguration.apply {
+ windowingMode = windowMode
+ positionInParent = Point(taskX, taskY)
+ bounds.set(Rect(taskX, taskY, taskX + taskWidth, taskY + taskHeight))
+ }
}
- fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change {
- val change = Change(
- WindowContainerToken(mock(
- IWindowContainerToken::class.java)),
- mock(SurfaceControl::class.java))
- change.mode = mode
- change.taskInfo = taskInfo
- return change
- }
+ fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change {
+ val change =
+ Change(WindowContainerToken(mock<IWindowContainerToken>()), mock<SurfaceControl>())
+ change.mode = mode
+ change.taskInfo = taskInfo
+ return change
}
-} \ No newline at end of file
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 310ccc252469..6612aee0cd12 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
@@ -119,54 +119,91 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
}
@Test
- fun isOnlyActiveTask_noActiveTasks() {
- // Not an active task
- assertThat(repo.isOnlyActiveTask(1)).isFalse()
+ fun isOnlyVisibleNonClosingTask_noTasks() {
+ // No visible tasks
+ assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
+ assertThat(repo.isClosingTask(1)).isFalse()
}
@Test
- fun isOnlyActiveTask_singleActiveTask() {
- repo.addActiveTask(DEFAULT_DISPLAY, 1)
- // The only active task
- assertThat(repo.isActiveTask(1)).isTrue()
- assertThat(repo.isOnlyActiveTask(1)).isTrue()
- // Not an active task
- assertThat(repo.isActiveTask(99)).isFalse()
- assertThat(repo.isOnlyActiveTask(99)).isFalse()
+ fun isOnlyVisibleNonClosingTask_singleVisibleNonClosingTask() {
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+
+ // The only visible task
+ assertThat(repo.isVisibleTask(1)).isTrue()
+ assertThat(repo.isClosingTask(1)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(1)).isTrue()
+ // Not a visible task
+ assertThat(repo.isVisibleTask(99)).isFalse()
+ assertThat(repo.isClosingTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
+ }
+
+ @Test
+ fun isOnlyVisibleNonClosingTask_singleVisibleClosingTask() {
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+ repo.addClosingTask(DEFAULT_DISPLAY, 1)
+
+ // A visible task that's closing
+ assertThat(repo.isVisibleTask(1)).isTrue()
+ assertThat(repo.isClosingTask(1)).isTrue()
+ assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
+ // Not a visible task
+ assertThat(repo.isVisibleTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
+ }
+
+ @Test
+ fun isOnlyVisibleNonClosingTask_singleVisibleMinimizedTask() {
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+ repo.minimizeTask(DEFAULT_DISPLAY, 1)
+
+ // The visible task that's closing
+ assertThat(repo.isVisibleTask(1)).isTrue()
+ assertThat(repo.isMinimizedTask(1)).isTrue()
+ assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
+ // Not a visible task
+ assertThat(repo.isVisibleTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
}
@Test
- fun isOnlyActiveTask_multipleActiveTasks() {
- repo.addActiveTask(DEFAULT_DISPLAY, 1)
- repo.addActiveTask(DEFAULT_DISPLAY, 2)
+ fun isOnlyVisibleNonClosingTask_multipleVisibleNonClosingTasks() {
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+
// Not the only task
- assertThat(repo.isActiveTask(1)).isTrue()
- assertThat(repo.isOnlyActiveTask(1)).isFalse()
+ assertThat(repo.isVisibleTask(1)).isTrue()
+ assertThat(repo.isClosingTask(1)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
// Not the only task
- assertThat(repo.isActiveTask(2)).isTrue()
- assertThat(repo.isOnlyActiveTask(2)).isFalse()
- // Not an active task
- assertThat(repo.isActiveTask(99)).isFalse()
- assertThat(repo.isOnlyActiveTask(99)).isFalse()
+ assertThat(repo.isVisibleTask(2)).isTrue()
+ assertThat(repo.isClosingTask(2)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse()
+ // Not a visible task
+ assertThat(repo.isVisibleTask(99)).isFalse()
+ assertThat(repo.isClosingTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
}
@Test
- fun isOnlyActiveTask_multipleDisplays() {
- repo.addActiveTask(DEFAULT_DISPLAY, 1)
- repo.addActiveTask(DEFAULT_DISPLAY, 2)
- repo.addActiveTask(SECOND_DISPLAY, 3)
+ fun isOnlyVisibleNonClosingTask_multipleDisplays() {
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+ repo.updateVisibleFreeformTasks(SECOND_DISPLAY, taskId = 3, visible = true)
+
// Not the only task on DEFAULT_DISPLAY
- assertThat(repo.isActiveTask(1)).isTrue()
- assertThat(repo.isOnlyActiveTask(1)).isFalse()
+ assertThat(repo.isVisibleTask(1)).isTrue()
+ assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse()
// Not the only task on DEFAULT_DISPLAY
- assertThat(repo.isActiveTask(2)).isTrue()
- assertThat(repo.isOnlyActiveTask(2)).isFalse()
- // The only active task on SECOND_DISPLAY
- assertThat(repo.isActiveTask(3)).isTrue()
- assertThat(repo.isOnlyActiveTask(3)).isTrue()
- // Not an active task
- assertThat(repo.isActiveTask(99)).isFalse()
- assertThat(repo.isOnlyActiveTask(99)).isFalse()
+ assertThat(repo.isVisibleTask(2)).isTrue()
+ assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse()
+ // The only visible task on SECOND_DISPLAY
+ assertThat(repo.isVisibleTask(3)).isTrue()
+ assertThat(repo.isOnlyVisibleNonClosingTask(3)).isTrue()
+ // Not a visible task
+ assertThat(repo.isVisibleTask(99)).isFalse()
+ assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse()
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt
new file mode 100644
index 000000000000..518c00d377ad
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.wm.shell.desktopmode
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.APP_FROM_OVERVIEW
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.KEYBOARD_SHORTCUT
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.TASK_DRAG
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getEnterTransitionType
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getExitTransitionType
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Test class for [DesktopModeTransitionTypes]
+ *
+ * Usage: atest WMShellUnitTests:DesktopModeTransitionTypesTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopModeTransitionTypesTest {
+
+ @Test
+ fun testGetEnterTransitionType() {
+ assertThat(UNKNOWN.getEnterTransitionType()).isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN)
+ assertThat(APP_HANDLE_MENU_BUTTON.getEnterTransitionType())
+ .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON)
+ assertThat(APP_FROM_OVERVIEW.getEnterTransitionType())
+ .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW)
+ assertThat(TASK_DRAG.getEnterTransitionType())
+ .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN)
+ assertThat(KEYBOARD_SHORTCUT.getEnterTransitionType())
+ .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT)
+ }
+
+ @Test
+ fun testGetExitTransitionType() {
+ assertThat(UNKNOWN.getExitTransitionType()).isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN)
+ assertThat(APP_HANDLE_MENU_BUTTON.getExitTransitionType())
+ .isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON)
+ assertThat(APP_FROM_OVERVIEW.getExitTransitionType())
+ .isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN)
+ assertThat(TASK_DRAG.getExitTransitionType()).isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG)
+ assertThat(KEYBOARD_SHORTCUT.getExitTransitionType())
+ .isEqualTo(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT)
+ }
+}
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 cf6cea2b34a7..14fa0f1a338d 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
@@ -24,6 +24,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.content.ComponentName
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.ActivityInfo.CONFIG_DENSITY
@@ -44,6 +45,7 @@ import android.view.Display.DEFAULT_DISPLAY
import android.view.SurfaceControl
import android.view.WindowManager
import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
@@ -76,6 +78,7 @@ import com.android.wm.shell.common.LaunchAdjacentController
import com.android.wm.shell.common.MultiInstanceHelper
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource.UNKNOWN
import com.android.wm.shell.common.split.SplitScreenConstants
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask
@@ -94,11 +97,14 @@ import com.android.wm.shell.transition.OneShotRemoteHandler
import com.android.wm.shell.transition.TestRemoteTransition
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
-import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_DESKTOP_MODE
import com.android.wm.shell.transition.Transitions.TransitionHandler
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.util.Optional
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
import org.junit.After
import org.junit.Assume.assumeTrue
import org.junit.Before
@@ -117,13 +123,11 @@ import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.capture
import org.mockito.quality.Strictness
-import junit.framework.Assert.assertFalse
-import junit.framework.Assert.assertTrue
-import org.mockito.Mockito.`when` as whenever
/**
* Test class for {@link DesktopTasksController}
@@ -134,1725 +138,2057 @@ import org.mockito.Mockito.`when` as whenever
@RunWith(AndroidTestingRunner::class)
class DesktopTasksControllerTest : ShellTestCase() {
- @JvmField
- @Rule
- val setFlagsRule = SetFlagsRule()
-
- @Mock lateinit var testExecutor: ShellExecutor
- @Mock lateinit var shellCommandHandler: ShellCommandHandler
- @Mock lateinit var shellController: ShellController
- @Mock lateinit var displayController: DisplayController
- @Mock lateinit var displayLayout: DisplayLayout
- @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
- @Mock lateinit var syncQueue: SyncTransactionQueue
- @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
- @Mock lateinit var transitions: Transitions
- @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler
- @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler
- @Mock lateinit var toggleResizeDesktopTaskTransitionHandler:
- ToggleResizeDesktopTaskTransitionHandler
- @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
- @Mock lateinit var launchAdjacentController: LaunchAdjacentController
- @Mock lateinit var splitScreenController: SplitScreenController
- @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
- @Mock lateinit var dragAndDropController: DragAndDropController
- @Mock lateinit var multiInstanceHelper: MultiInstanceHelper
- @Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver
- @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator
- @Mock lateinit var recentTasksController: RecentTasksController
-
- private lateinit var mockitoSession: StaticMockitoSession
- private lateinit var controller: DesktopTasksController
- private lateinit var shellInit: ShellInit
- private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository
- private lateinit var desktopTasksLimiter: DesktopTasksLimiter
- private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener
-
- private val shellExecutor = TestShellExecutor()
-
- // Mock running tasks are registered here so we can get the list from mock shell task organizer
- private val runningTasks = mutableListOf<RunningTaskInfo>()
-
- private val DISPLAY_DIMENSION_SHORT = 1600
- private val DISPLAY_DIMENSION_LONG = 2560
- private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 200, 2240, 1400)
- private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 320, 1400, 2240)
- private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 680, 1575, 1880)
- private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 200, 1880, 1400)
- private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 699, 1575, 1861)
- private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 200, 1730, 1400)
-
- @Before
- fun setUp() {
- mockitoSession = mockitoSession().strictness(Strictness.LENIENT)
- .spyStatic(DesktopModeStatus::class.java).startMocking()
- whenever(DesktopModeStatus.isEnabled()).thenReturn(true)
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
-
- shellInit = spy(ShellInit(testExecutor))
- desktopModeTaskRepository = DesktopModeTaskRepository()
- desktopTasksLimiter =
- DesktopTasksLimiter(transitions, desktopModeTaskRepository, shellTaskOrganizer)
-
- whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
- whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
- whenever(enterDesktopTransitionHandler.moveToDesktop(any())).thenAnswer { Binder() }
- whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
- whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
- (i.arguments.first() as Rect).set(STABLE_BOUNDS)
- }
-
- val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
- tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
- whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
-
- controller = createController()
- controller.setSplitScreenController(splitScreenController)
-
- shellInit.init()
-
- val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java)
- verify(recentsTransitionHandler).addTransitionStateListener(captor.capture())
- recentsTransitionStateListener = captor.value
- }
-
- private fun createController(): DesktopTasksController {
- return DesktopTasksController(
- context,
- shellInit,
- shellCommandHandler,
- shellController,
- displayController,
- shellTaskOrganizer,
- syncQueue,
- rootTaskDisplayAreaOrganizer,
- dragAndDropController,
- transitions,
- enterDesktopTransitionHandler,
- exitDesktopTransitionHandler,
- toggleResizeDesktopTaskTransitionHandler,
- dragToDesktopTransitionHandler,
- desktopModeTaskRepository,
- desktopModeLoggerTransitionObserver,
- launchAdjacentController,
- recentsTransitionHandler,
- multiInstanceHelper,
- shellExecutor,
- Optional.of(desktopTasksLimiter),
- recentTasksController
- )
- }
-
- @After
- fun tearDown() {
- mockitoSession.finishMocking()
-
- runningTasks.clear()
- }
-
- @Test
- fun instantiate_addInitCallback() {
- verify(shellInit).addInitCallback(any(), any<DesktopTasksController>())
- }
-
- @Test
- fun instantiate_flagOff_doNotAddInitCallback() {
- whenever(DesktopModeStatus.isEnabled()).thenReturn(false)
- clearInvocations(shellInit)
-
- createController()
-
- verify(shellInit, never()).addInitCallback(any(), any<DesktopTasksController>())
- }
-
- @Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() {
- val homeTask = setUpHomeTask()
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
- markTaskHidden(task1)
- markTaskHidden(task2)
-
- controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
- val wct =
- getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(3)
- // Expect order to be from bottom: home, task1, task2
- wct.assertReorderAt(index = 0, homeTask)
- wct.assertReorderAt(index = 1, task1)
- wct.assertReorderAt(index = 2, task2)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() {
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
- markTaskHidden(task1)
- markTaskHidden(task2)
-
- controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
- val wct =
- getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(3)
- // Expect order to be from bottom: wallpaper intent, task1, task2
- wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
- wct.assertReorderAt(index = 1, task1)
- wct.assertReorderAt(index = 2, task2)
- }
-
- @Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() {
- val homeTask = setUpHomeTask()
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
- markTaskVisible(task1)
- markTaskVisible(task2)
-
- controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
- val wct =
- getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(3)
- // Expect order to be from bottom: home, task1, task2
- wct.assertReorderAt(index = 0, homeTask)
- wct.assertReorderAt(index = 1, task1)
- wct.assertReorderAt(index = 2, task2)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() {
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
- markTaskVisible(task1)
- markTaskVisible(task2)
-
- controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
- val wct =
- getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(3)
- // Expect order to be from bottom: wallpaper intent, task1, task2
- wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
- wct.assertReorderAt(index = 1, task1)
- wct.assertReorderAt(index = 2, task2)
- }
-
- @Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperDisabled() {
- val homeTask = setUpHomeTask()
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
- markTaskHidden(task1)
- markTaskVisible(task2)
-
- controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
- val wct =
- getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(3)
- // Expect order to be from bottom: home, task1, task2
- wct.assertReorderAt(index = 0, homeTask)
- wct.assertReorderAt(index = 1, task1)
- wct.assertReorderAt(index = 2, task2)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() {
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
- markTaskHidden(task1)
- markTaskVisible(task2)
-
- controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
- val wct =
- getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(3)
- // Expect order to be from bottom: wallpaper intent, task1, task2
- wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
- wct.assertReorderAt(index = 1, task1)
- wct.assertReorderAt(index = 2, task2)
- }
-
- @Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() {
- val homeTask = setUpHomeTask()
-
- controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
- val wct =
- getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(1)
- wct.assertReorderAt(index = 0, homeTask)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() {
- controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
- val wct =
- getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
- }
-
- @Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() {
- val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
- val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
- setUpHomeTask(SECOND_DISPLAY)
- val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY)
- markTaskHidden(taskDefaultDisplay)
- markTaskHidden(taskSecondDisplay)
-
- controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
- val wct =
- getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(2)
- // Expect order to be from bottom: home, task
- wct.assertReorderAt(index = 0, homeTaskDefaultDisplay)
- wct.assertReorderAt(index = 1, taskDefaultDisplay)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() {
- val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
- setUpHomeTask(SECOND_DISPLAY)
- val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY)
- markTaskHidden(taskDefaultDisplay)
- markTaskHidden(taskSecondDisplay)
-
- controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
- val wct =
- getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(2)
- // Expect order to be from bottom: wallpaper intent, task
- wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
- wct.assertReorderAt(index = 1, taskDefaultDisplay)
- }
-
- @Test
- fun showDesktopApps_dontReorderMinimizedTask() {
- val homeTask = setUpHomeTask()
- val freeformTask = setUpFreeformTask()
- val minimizedTask = setUpFreeformTask()
- markTaskHidden(freeformTask)
- markTaskHidden(minimizedTask)
- desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId)
-
- controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
-
- val wct = getLatestWct(
- type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
- assertThat(wct.hierarchyOps).hasSize(2)
- // Reorder home and freeform task to top, don't reorder the minimized task
- wct.assertReorderAt(index = 0, homeTask, toTop = true)
- wct.assertReorderAt(index = 1, freeformTask, toTop = true)
- }
-
- @Test
- fun getVisibleTaskCount_noTasks_returnsZero() {
- assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
- }
-
- @Test
- fun getVisibleTaskCount_twoTasks_bothVisible_returnsTwo() {
- setUpHomeTask()
- setUpFreeformTask().also(::markTaskVisible)
- setUpFreeformTask().also(::markTaskVisible)
- assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2)
- }
-
- @Test
- fun getVisibleTaskCount_twoTasks_oneVisible_returnsOne() {
- setUpHomeTask()
- setUpFreeformTask().also(::markTaskVisible)
- setUpFreeformTask().also(::markTaskHidden)
- assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
- }
-
- @Test
- fun getVisibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() {
- setUpHomeTask()
- setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible)
- setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible)
- assertThat(controller.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val task = setUpFullscreenTask()
- setUpLandscapeDisplay()
-
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
- setUpLandscapeDisplay()
-
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
+ @JvmField @Rule val setFlagsRule = SetFlagsRule()
+
+ @Mock lateinit var testExecutor: ShellExecutor
+ @Mock lateinit var shellCommandHandler: ShellCommandHandler
+ @Mock lateinit var shellController: ShellController
+ @Mock lateinit var displayController: DisplayController
+ @Mock lateinit var displayLayout: DisplayLayout
+ @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
+ @Mock lateinit var syncQueue: SyncTransactionQueue
+ @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+ @Mock lateinit var transitions: Transitions
+ @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler
+ @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler
+ @Mock
+ lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler
+ @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
+ @Mock lateinit var launchAdjacentController: LaunchAdjacentController
+ @Mock lateinit var splitScreenController: SplitScreenController
+ @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
+ @Mock lateinit var dragAndDropController: DragAndDropController
+ @Mock lateinit var multiInstanceHelper: MultiInstanceHelper
+ @Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver
+ @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator
+ @Mock lateinit var recentTasksController: RecentTasksController
+
+ private lateinit var mockitoSession: StaticMockitoSession
+ private lateinit var controller: DesktopTasksController
+ private lateinit var shellInit: ShellInit
+ private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository
+ private lateinit var desktopTasksLimiter: DesktopTasksLimiter
+ private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener
+
+ private val shellExecutor = TestShellExecutor()
+
+ // Mock running tasks are registered here so we can get the list from mock shell task organizer
+ private val runningTasks = mutableListOf<RunningTaskInfo>()
+
+ private val DISPLAY_DIMENSION_SHORT = 1600
+ private val DISPLAY_DIMENSION_LONG = 2560
+ private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 200, 2240, 1400)
+ private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 320, 1400, 2240)
+ private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 680, 1575, 1880)
+ private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 200, 1880, 1400)
+ private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 699, 1575, 1861)
+ private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 200, 1730, 1400)
+
+ @Before
+ fun setUp() {
+ mockitoSession =
+ mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .spyStatic(DesktopModeStatus::class.java)
+ .startMocking()
+ whenever(DesktopModeStatus.isEnabled()).thenReturn(true)
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+
+ shellInit = spy(ShellInit(testExecutor))
+ desktopModeTaskRepository = DesktopModeTaskRepository()
+ desktopTasksLimiter =
+ DesktopTasksLimiter(transitions, desktopModeTaskRepository, shellTaskOrganizer)
+
+ whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
+ whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
+ whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenAnswer { Binder() }
+ whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+ }
+
+ val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
+
+ controller = createController()
+ controller.setSplitScreenController(splitScreenController)
+
+ shellInit.init()
+
+ val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java)
+ verify(recentsTransitionHandler).addTransitionStateListener(captor.capture())
+ recentsTransitionStateListener = captor.value
+ }
+
+ private fun createController(): DesktopTasksController {
+ return DesktopTasksController(
+ context,
+ shellInit,
+ shellCommandHandler,
+ shellController,
+ displayController,
+ shellTaskOrganizer,
+ syncQueue,
+ rootTaskDisplayAreaOrganizer,
+ dragAndDropController,
+ transitions,
+ enterDesktopTransitionHandler,
+ exitDesktopTransitionHandler,
+ toggleResizeDesktopTaskTransitionHandler,
+ dragToDesktopTransitionHandler,
+ desktopModeTaskRepository,
+ desktopModeLoggerTransitionObserver,
+ launchAdjacentController,
+ recentsTransitionHandler,
+ multiInstanceHelper,
+ shellExecutor,
+ Optional.of(desktopTasksLimiter),
+ recentTasksController)
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
+
+ runningTasks.clear()
+ }
+
+ @Test
+ fun instantiate_addInitCallback() {
+ verify(shellInit).addInitCallback(any(), any<DesktopTasksController>())
+ }
+
+ @Test
+ fun instantiate_flagOff_doNotAddInitCallback() {
+ whenever(DesktopModeStatus.isEnabled()).thenReturn(false)
+ clearInvocations(shellInit)
+
+ createController()
+
+ verify(shellInit, never()).addInitCallback(any(), any<DesktopTasksController>())
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() {
+ val homeTask = setUpHomeTask()
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ markTaskHidden(task1)
+ markTaskHidden(task2)
+
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ assertThat(wct.hierarchyOps).hasSize(3)
+ // Expect order to be from bottom: home, task1, task2
+ wct.assertReorderAt(index = 0, homeTask)
+ wct.assertReorderAt(index = 1, task1)
+ wct.assertReorderAt(index = 2, task2)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() {
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ markTaskHidden(task1)
+ markTaskHidden(task2)
+
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ assertThat(wct.hierarchyOps).hasSize(3)
+ // Expect order to be from bottom: wallpaper intent, task1, task2
+ wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+ wct.assertReorderAt(index = 1, task1)
+ wct.assertReorderAt(index = 2, task2)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() {
+ val homeTask = setUpHomeTask()
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ markTaskVisible(task1)
+ markTaskVisible(task2)
+
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ assertThat(wct.hierarchyOps).hasSize(3)
+ // Expect order to be from bottom: home, task1, task2
+ wct.assertReorderAt(index = 0, homeTask)
+ wct.assertReorderAt(index = 1, task1)
+ wct.assertReorderAt(index = 2, task2)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() {
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ markTaskVisible(task1)
+ markTaskVisible(task2)
+
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ assertThat(wct.hierarchyOps).hasSize(3)
+ // Expect order to be from bottom: wallpaper intent, task1, task2
+ wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+ wct.assertReorderAt(index = 1, task1)
+ wct.assertReorderAt(index = 2, task2)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperDisabled() {
+ val homeTask = setUpHomeTask()
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ markTaskHidden(task1)
+ markTaskVisible(task2)
+
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ assertThat(wct.hierarchyOps).hasSize(3)
+ // Expect order to be from bottom: home, task1, task2
+ wct.assertReorderAt(index = 0, homeTask)
+ wct.assertReorderAt(index = 1, task1)
+ wct.assertReorderAt(index = 2, task2)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() {
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ markTaskHidden(task1)
+ markTaskVisible(task2)
+
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ assertThat(wct.hierarchyOps).hasSize(3)
+ // Expect order to be from bottom: wallpaper intent, task1, task2
+ wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+ wct.assertReorderAt(index = 1, task1)
+ wct.assertReorderAt(index = 2, task2)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() {
+ val homeTask = setUpHomeTask()
+
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ assertThat(wct.hierarchyOps).hasSize(1)
+ wct.assertReorderAt(index = 0, homeTask)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() {
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() {
+ val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY)
+ val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
+ setUpHomeTask(SECOND_DISPLAY)
+ val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY)
+ markTaskHidden(taskDefaultDisplay)
+ markTaskHidden(taskSecondDisplay)
+
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ assertThat(wct.hierarchyOps).hasSize(2)
+ // Expect order to be from bottom: home, task
+ wct.assertReorderAt(index = 0, homeTaskDefaultDisplay)
+ wct.assertReorderAt(index = 1, taskDefaultDisplay)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() {
+ val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY)
+ setUpHomeTask(SECOND_DISPLAY)
+ val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY)
+ markTaskHidden(taskDefaultDisplay)
+ markTaskHidden(taskSecondDisplay)
+
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ assertThat(wct.hierarchyOps).hasSize(2)
+ // Expect order to be from bottom: wallpaper intent, task
+ wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+ wct.assertReorderAt(index = 1, taskDefaultDisplay)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_desktopWallpaperDisabled_dontReorderMinimizedTask() {
+ val homeTask = setUpHomeTask()
+ val freeformTask = setUpFreeformTask()
+ val minimizedTask = setUpFreeformTask()
+
+ markTaskHidden(freeformTask)
+ markTaskHidden(minimizedTask)
+ desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId)
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ assertThat(wct.hierarchyOps).hasSize(2)
+ // Reorder home and freeform task to top, don't reorder the minimized task
+ wct.assertReorderAt(index = 0, homeTask, toTop = true)
+ wct.assertReorderAt(index = 1, freeformTask, toTop = true)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() {
+ setUpHomeTask()
+ val freeformTask = setUpFreeformTask()
+ val minimizedTask = setUpFreeformTask()
+
+ markTaskHidden(freeformTask)
+ markTaskHidden(minimizedTask)
+ desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId)
+ controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition()))
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java)
+ assertThat(wct.hierarchyOps).hasSize(2)
+ // Add desktop wallpaper activity
+ wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+ // Reorder freeform task to top, don't reorder the minimized task
+ wct.assertReorderAt(index = 1, freeformTask, toTop = true)
+ }
+
+ @Test
+ fun getVisibleTaskCount_noTasks_returnsZero() {
+ assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0)
+ }
+
+ @Test
+ fun getVisibleTaskCount_twoTasks_bothVisible_returnsTwo() {
+ setUpHomeTask()
+ setUpFreeformTask().also(::markTaskVisible)
+ setUpFreeformTask().also(::markTaskVisible)
+ assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2)
+ }
+
+ @Test
+ fun getVisibleTaskCount_twoTasks_oneVisible_returnsOne() {
+ setUpHomeTask()
+ setUpFreeformTask().also(::markTaskVisible)
+ setUpFreeformTask().also(::markTaskHidden)
+ assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1)
+ }
+
+ @Test
+ fun getVisibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() {
+ setUpHomeTask()
+ setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible)
+ setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible)
+ assertThat(controller.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
+ val task = setUpFullscreenTask()
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
+ val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
+ val task =
+ setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
+ val task =
+ setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
+ val task =
+ setUpFullscreenTask(
+ isResizable = false,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
shouldLetterbox = true)
- setUpLandscapeDisplay()
-
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val task = setUpFullscreenTask(isResizable = false,
- screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
- setUpLandscapeDisplay()
-
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val task = setUpFullscreenTask(isResizable = false,
- screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
- setUpLandscapeDisplay()
-
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
- setUpPortraitDisplay()
-
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
+ val task =
+ setUpFullscreenTask(
+ deviceOrientation = ORIENTATION_PORTRAIT,
screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
- setUpPortraitDisplay()
-
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
- screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
- setUpPortraitDisplay()
-
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val task = setUpFullscreenTask(isResizable = false,
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
+ val task =
+ setUpFullscreenTask(
+ deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
+ shouldLetterbox = true)
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
+ val task =
+ setUpFullscreenTask(
+ isResizable = false,
deviceOrientation = ORIENTATION_PORTRAIT,
screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
- setUpPortraitDisplay()
-
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val task = setUpFullscreenTask(isResizable = false,
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
+ val task =
+ setUpFullscreenTask(
+ isResizable = false,
deviceOrientation = ORIENTATION_PORTRAIT,
- screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
- setUpPortraitDisplay()
-
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
- }
-
- @Test
- fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
- val task = setUpFullscreenTask()
- val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
- tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- }
-
- @Test
- fun moveToDesktop_tdaFreeform_windowingModeSetToUndefined() {
- val task = setUpFullscreenTask()
- val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
- tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_UNDEFINED)
- }
-
- @Test
- fun moveToDesktop_nonExistentTask_doesNothing() {
- controller.moveToDesktop(999)
- verifyWCTNotExecuted()
- }
-
- @Test
- fun moveToDesktop_nonRunningTask_launchesInFreeform() {
- whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
-
- val task = createTaskInfo(1)
-
- whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
-
- controller.moveToDesktop(task.taskId)
- with(getLatestMoveToDesktopWct()){
- assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
- }
- }
-
- @Test
- fun moveToDesktop_topActivityTranslucent_doesNothing() {
- setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
- val task = setUpFullscreenTask().apply {
- isTopActivityTransparent = true
- numActivities = 1
- }
-
- controller.moveToDesktop(task)
- verifyWCTNotExecuted()
- }
-
- @Test
- fun moveToDesktop_deviceNotSupported_doesNothing() {
- val task = setUpFullscreenTask()
-
- // Simulate non compatible device
- doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
-
- controller.moveToDesktop(task)
- verifyWCTNotExecuted()
- }
-
- @Test
- fun moveToDesktop_deviceNotSupported_deviceRestrictionsOverridden_taskIsMovedToDesktop() {
- val task = setUpFullscreenTask()
-
- // Simulate non compatible device
- doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
-
- // Simulate enforce device restrictions system property overridden to false
- whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false)
-
- controller.moveToDesktop(task)
-
- val wct = getLatestMoveToDesktopWct()
- assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- }
-
- @Test
- fun moveToDesktop_deviceSupported_taskIsMovedToDesktop() {
- val task = setUpFullscreenTask()
-
- controller.moveToDesktop(task)
-
- val wct = getLatestMoveToDesktopWct()
- assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- }
-
- @Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() {
- val homeTask = setUpHomeTask()
- val freeformTask = setUpFreeformTask()
- val fullscreenTask = setUpFullscreenTask()
- markTaskHidden(freeformTask)
-
- controller.moveToDesktop(fullscreenTask)
-
- with(getLatestMoveToDesktopWct()) {
- // Operations should include home task, freeform task
- assertThat(hierarchyOps).hasSize(3)
- assertReorderSequence(homeTask, freeformTask, fullscreenTask)
- assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- }
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() {
- val freeformTask = setUpFreeformTask()
- val fullscreenTask = setUpFullscreenTask()
- markTaskHidden(freeformTask)
-
- controller.moveToDesktop(fullscreenTask)
-
- with(getLatestMoveToDesktopWct()) {
- // Operations should include wallpaper intent, freeform task, fullscreen task
- assertThat(hierarchyOps).hasSize(3)
- assertPendingIntentAt(index = 0, desktopWallpaperIntent)
- assertReorderAt(index = 1, freeformTask)
- assertReorderAt(index = 2, fullscreenTask)
- assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- }
- }
-
- @Test
- fun moveToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() {
- setUpHomeTask(displayId = DEFAULT_DISPLAY)
- val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
- markTaskHidden(freeformTaskDefault)
-
- val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY)
- val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY)
- markTaskHidden(freeformTaskSecond)
-
- controller.moveToDesktop(fullscreenTaskDefault)
-
- with(getLatestMoveToDesktopWct()) {
- // Check that hierarchy operations do not include tasks from second display
- assertThat(hierarchyOps.map { it.container })
- .doesNotContain(homeTaskSecond.token.asBinder())
- assertThat(hierarchyOps.map { it.container })
- .doesNotContain(freeformTaskSecond.token.asBinder())
- }
- }
-
- @Test
- fun moveToDesktop_splitTaskExitsSplit() {
- val task = setUpSplitScreenTask()
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- verify(splitScreenController).prepareExitSplitScreen(
- any(),
- anyInt(),
- eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)
- )
- }
-
- @Test
- fun moveToDesktop_fullscreenTaskDoesNotExitSplit() {
- val task = setUpFullscreenTask()
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- verify(splitScreenController, never()).prepareExitSplitScreen(
- any(),
- anyInt(),
- eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)
- )
- }
-
- @Test
- fun moveToDesktop_bringsTasksOverLimit_dontShowBackTask() {
- val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
- val homeTask = setUpHomeTask()
- val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
- val newTask = setUpFullscreenTask()
-
- controller.moveToDesktop(newTask)
-
- val wct = getLatestMoveToDesktopWct()
- assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + home
- wct.assertReorderAt(0, homeTask)
- for (i in 1..<taskLimit) { // Skipping freeformTasks[0]
- wct.assertReorderAt(index = i, task = freeformTasks[i])
- }
- wct.assertReorderAt(taskLimit, newTask)
- }
-
- @Test
- fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() {
- val task = setUpFreeformTask()
- val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
- tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
- controller.moveToFullscreen(task.taskId)
- val wct = getLatestExitDesktopWct()
- assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_UNDEFINED)
- }
-
- @Test
- fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() {
- val task = setUpFreeformTask()
- val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
- tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
- controller.moveToFullscreen(task.taskId)
- val wct = getLatestExitDesktopWct()
- assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FULLSCREEN)
- }
-
- @Test
- fun moveToFullscreen_nonExistentTask_doesNothing() {
- controller.moveToFullscreen(999)
- verifyWCTNotExecuted()
- }
-
- @Test
- fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() {
- val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY)
-
- controller.moveToFullscreen(taskDefaultDisplay.taskId)
-
- with(getLatestExitDesktopWct()) {
- assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder())
- assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder())
- }
- }
-
- @Test
- fun moveTaskToFront_postsWctWithReorderOp() {
- val task1 = setUpFreeformTask()
- setUpFreeformTask()
-
- controller.moveTaskToFront(task1)
-
- val wct = getLatestWct(type = TRANSIT_TO_FRONT)
- assertThat(wct.hierarchyOps).hasSize(1)
- wct.assertReorderAt(index = 0, task1)
- }
-
- @Test
- fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() {
- val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
- setUpHomeTask()
- val freeformTasks = (1..taskLimit + 1).map { _ -> setUpFreeformTask() }
-
- controller.moveTaskToFront(freeformTasks[0])
-
- val wct = getLatestWct(type = TRANSIT_TO_FRONT)
- assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize
- wct.assertReorderAt(0, freeformTasks[0], toTop = true)
- wct.assertReorderAt(1, freeformTasks[1], toTop = false)
- }
-
- @Test
- fun moveToNextDisplay_noOtherDisplays() {
- whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY))
- val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- controller.moveToNextDisplay(task.taskId)
- verifyWCTNotExecuted()
- }
-
- @Test
- fun moveToNextDisplay_moveFromFirstToSecondDisplay() {
- // 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)
-
- val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
- controller.moveToNextDisplay(task.taskId)
- with(getLatestWct(type = TRANSIT_CHANGE)) {
- assertThat(hierarchyOps).hasSize(1)
- assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder())
- assertThat(hierarchyOps[0].isReparent).isTrue()
- assertThat(hierarchyOps[0].newParent).isEqualTo(secondDisplayArea.token.asBinder())
- assertThat(hierarchyOps[0].toTop).isTrue()
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
+ shouldLetterbox = true)
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
+ val task = setUpFullscreenTask()
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
+ fun moveToDesktop_tdaFreeform_windowingModeSetToUndefined() {
+ val task = setUpFullscreenTask()
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED)
+ }
+
+ @Test
+ fun moveToDesktop_nonExistentTask_doesNothing() {
+ controller.moveToDesktop(999, transitionSource = UNKNOWN)
+ verifyEnterDesktopWCTNotExecuted()
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun moveToDesktop_desktopWallpaperDisabled_nonRunningTask_launchesInFreeform() {
+ val task = createTaskInfo(1)
+ whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
+ whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
+
+ controller.moveToDesktop(task.taskId, transitionSource = UNKNOWN)
+
+ with(getLatestEnterDesktopWct()) {
+ assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM)
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun moveToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() {
+ val task = createTaskInfo(1)
+ whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
+ whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
+
+ controller.moveToDesktop(task.taskId, transitionSource = UNKNOWN)
+
+ with(getLatestEnterDesktopWct()) {
+ // Add desktop wallpaper activity
+ assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+ // Launch task
+ assertLaunchTaskAt(index = 1, task.taskId, WINDOWING_MODE_FREEFORM)
+ }
+ }
+
+ @Test
+ fun moveToDesktop_topActivityTranslucent_doesNothing() {
+ setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+ val task =
+ setUpFullscreenTask().apply {
+ isTopActivityTransparent = true
+ numActivities = 1
}
- }
- @Test
- fun moveToNextDisplay_moveFromSecondToFirstDisplay() {
- // Set up two display ids
- 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)
-
- val task = setUpFreeformTask(displayId = SECOND_DISPLAY)
- controller.moveToNextDisplay(task.taskId)
-
- with(getLatestWct(type = TRANSIT_CHANGE)) {
- assertThat(hierarchyOps).hasSize(1)
- assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder())
- assertThat(hierarchyOps[0].isReparent).isTrue()
- assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder())
- assertThat(hierarchyOps[0].toTop).isTrue()
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ verifyEnterDesktopWCTNotExecuted()
+ }
+
+ @Test
+ fun moveToDesktop_systemUIActivity_doesNothing() {
+ val task = setUpFullscreenTask()
+
+ // Set task as systemUI package
+ val systemUIPackageName = context.resources.getString(
+ com.android.internal.R.string.config_systemUi)
+ val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+ task.baseActivity = baseComponent
+
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ verifyEnterDesktopWCTNotExecuted()
+ }
+
+ @Test
+ fun moveToDesktop_deviceSupported_taskIsMovedToDesktop() {
+ val task = setUpFullscreenTask()
+
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+
+ val wct = getLatestEnterDesktopWct()
+ assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() {
+ val homeTask = setUpHomeTask()
+ val freeformTask = setUpFreeformTask()
+ val fullscreenTask = setUpFullscreenTask()
+ markTaskHidden(freeformTask)
+
+ controller.moveToDesktop(fullscreenTask, transitionSource = UNKNOWN)
+
+ with(getLatestEnterDesktopWct()) {
+ // Operations should include home task, freeform task
+ assertThat(hierarchyOps).hasSize(3)
+ assertReorderSequence(homeTask, freeformTask, fullscreenTask)
+ assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() {
+ val freeformTask = setUpFreeformTask()
+ val fullscreenTask = setUpFullscreenTask()
+ markTaskHidden(freeformTask)
+
+ controller.moveToDesktop(fullscreenTask, transitionSource = UNKNOWN)
+
+ with(getLatestEnterDesktopWct()) {
+ // Operations should include wallpaper intent, freeform task, fullscreen task
+ assertThat(hierarchyOps).hasSize(3)
+ assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+ assertReorderAt(index = 1, freeformTask)
+ assertReorderAt(index = 2, fullscreenTask)
+ assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+ }
+
+ @Test
+ fun moveToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() {
+ setUpHomeTask(displayId = DEFAULT_DISPLAY)
+ val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY)
+ markTaskHidden(freeformTaskDefault)
+
+ val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY)
+ val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY)
+ markTaskHidden(freeformTaskSecond)
+
+ controller.moveToDesktop(fullscreenTaskDefault, transitionSource = UNKNOWN)
+
+ with(getLatestEnterDesktopWct()) {
+ // Check that hierarchy operations do not include tasks from second display
+ assertThat(hierarchyOps.map { it.container }).doesNotContain(homeTaskSecond.token.asBinder())
+ assertThat(hierarchyOps.map { it.container })
+ .doesNotContain(freeformTaskSecond.token.asBinder())
+ }
+ }
+
+ @Test
+ fun moveToDesktop_splitTaskExitsSplit() {
+ val task = setUpSplitScreenTask()
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
+ verify(splitScreenController)
+ .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE))
+ }
+
+ @Test
+ fun moveToDesktop_fullscreenTaskDoesNotExitSplit() {
+ val task = setUpFullscreenTask()
+ controller.moveToDesktop(task, transitionSource = UNKNOWN)
+ val wct = getLatestEnterDesktopWct()
+ assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM)
+ verify(splitScreenController, never())
+ .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE))
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun moveToDesktop_desktopWallpaperDisabled_bringsTasksOver_dontShowBackTask() {
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
+ val newTask = setUpFullscreenTask()
+ val homeTask = setUpHomeTask()
+
+ controller.moveToDesktop(newTask, transitionSource = UNKNOWN)
+
+ val wct = getLatestEnterDesktopWct()
+ assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + home
+ wct.assertReorderAt(0, homeTask)
+ wct.assertReorderSequenceInRange(
+ range = 1..<(taskLimit + 1),
+ *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0]
+ newTask
+ )
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun moveToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() {
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
+ val newTask = setUpFullscreenTask()
+ setUpHomeTask()
+
+ controller.moveToDesktop(newTask, transitionSource = UNKNOWN)
+
+ val wct = getLatestEnterDesktopWct()
+ assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + wallpaper
+ // Add desktop wallpaper activity
+ wct.assertPendingIntentAt(0, desktopWallpaperIntent)
+ wct.assertReorderSequenceInRange(
+ range = 1..<(taskLimit + 1),
+ *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0]
+ newTask
+ )
+ }
+
+ @Test
+ fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() {
+ val task = setUpFreeformTask()
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+ controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+ val wct = getLatestExitDesktopWct()
+ assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED)
+ }
+
+ @Test
+ fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() {
+ val task = setUpFreeformTask()
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
+ controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN)
+ val wct = getLatestExitDesktopWct()
+ assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ }
+
+ @Test
+ fun moveToFullscreen_nonExistentTask_doesNothing() {
+ controller.moveToFullscreen(999, transitionSource = UNKNOWN)
+ verifyExitDesktopWCTNotExecuted()
+ }
+
+ @Test
+ fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() {
+ val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY)
+
+ controller.moveToFullscreen(taskDefaultDisplay.taskId, transitionSource = UNKNOWN)
+
+ with(getLatestExitDesktopWct()) {
+ assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder())
+ assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder())
+ }
+ }
+
+ @Test
+ fun moveTaskToFront_postsWctWithReorderOp() {
+ val task1 = setUpFreeformTask()
+ setUpFreeformTask()
+
+ controller.moveTaskToFront(task1)
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT)
+ assertThat(wct.hierarchyOps).hasSize(1)
+ wct.assertReorderAt(index = 0, task1)
+ }
+
+ @Test
+ fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() {
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ setUpHomeTask()
+ val freeformTasks = (1..taskLimit + 1).map { _ -> setUpFreeformTask() }
+
+ controller.moveTaskToFront(freeformTasks[0])
+
+ val wct = getLatestWct(type = TRANSIT_TO_FRONT)
+ assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize
+ wct.assertReorderAt(0, freeformTasks[0], toTop = true)
+ wct.assertReorderAt(1, freeformTasks[1], toTop = false)
+ }
+
+ @Test
+ fun moveToNextDisplay_noOtherDisplays() {
+ whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY))
+ val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ controller.moveToNextDisplay(task.taskId)
+ verifyWCTNotExecuted()
+ }
+
+ @Test
+ fun moveToNextDisplay_moveFromFirstToSecondDisplay() {
+ // 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)
+
+ val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ controller.moveToNextDisplay(task.taskId)
+ with(getLatestWct(type = TRANSIT_CHANGE)) {
+ assertThat(hierarchyOps).hasSize(1)
+ assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder())
+ assertThat(hierarchyOps[0].isReparent).isTrue()
+ assertThat(hierarchyOps[0].newParent).isEqualTo(secondDisplayArea.token.asBinder())
+ assertThat(hierarchyOps[0].toTop).isTrue()
+ }
+ }
+
+ @Test
+ fun moveToNextDisplay_moveFromSecondToFirstDisplay() {
+ // Set up two display ids
+ 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)
+
+ val task = setUpFreeformTask(displayId = SECOND_DISPLAY)
+ controller.moveToNextDisplay(task.taskId)
+
+ with(getLatestWct(type = TRANSIT_CHANGE)) {
+ assertThat(hierarchyOps).hasSize(1)
+ assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder())
+ assertThat(hierarchyOps[0].isReparent).isTrue()
+ assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder())
+ assertThat(hierarchyOps[0].toTop).isTrue()
+ }
+ }
+
+ @Test
+ fun getTaskWindowingMode() {
+ val fullscreenTask = setUpFullscreenTask()
+ val freeformTask = setUpFreeformTask()
+
+ assertThat(controller.getTaskWindowingMode(fullscreenTask.taskId))
+ .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ assertThat(controller.getTaskWindowingMode(freeformTask.taskId))
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ assertThat(controller.getTaskWindowingMode(999)).isEqualTo(WINDOWING_MODE_UNDEFINED)
+ }
+
+ @Test
+ fun onDesktopWindowClose_noActiveTasks() {
+ val wct = WindowContainerTransaction()
+ controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = 1)
+ // Doesn't modify transaction
+ assertThat(wct.hierarchyOps).isEmpty()
+ }
+
+ @Test
+ fun onDesktopWindowClose_singleActiveTask_noWallpaperActivityToken() {
+ val task = setUpFreeformTask()
+ val wct = WindowContainerTransaction()
+ controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+ // Doesn't modify transaction
+ assertThat(wct.hierarchyOps).isEmpty()
+ }
+
+ @Test
+ fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() {
+ val task = setUpFreeformTask()
+ val wallpaperToken = MockToken().token()
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+
+ val wct = WindowContainerTransaction()
+ controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+ // Adds remove wallpaper operation
+ wct.assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ fun onDesktopWindowClose_singleActiveTask_isClosing() {
+ val task = setUpFreeformTask()
+ val wallpaperToken = MockToken().token()
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ desktopModeTaskRepository.addClosingTask(DEFAULT_DISPLAY, task.taskId)
+
+ val wct = WindowContainerTransaction()
+ controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+ // Doesn't modify transaction
+ assertThat(wct.hierarchyOps).isEmpty()
+ }
+
+ @Test
+ fun onDesktopWindowClose_singleActiveTask_isMinimized() {
+ val task = setUpFreeformTask()
+ val wallpaperToken = MockToken().token()
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId)
+
+ val wct = WindowContainerTransaction()
+ controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+ // Doesn't modify transaction
+ assertThat(wct.hierarchyOps).isEmpty()
+ }
+
+ @Test
+ fun onDesktopWindowClose_multipleActiveTasks() {
+ val task1 = setUpFreeformTask()
+ setUpFreeformTask()
+ val wallpaperToken = MockToken().token()
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+
+ val wct = WindowContainerTransaction()
+ controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task1.taskId)
+ // Doesn't modify transaction
+ assertThat(wct.hierarchyOps).isEmpty()
+ }
+
+ @Test
+ fun onDesktopWindowClose_multipleActiveTasks_isOnlyNonClosingTask() {
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ val wallpaperToken = MockToken().token()
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ desktopModeTaskRepository.addClosingTask(DEFAULT_DISPLAY, task2.taskId)
+
+ val wct = WindowContainerTransaction()
+ controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task1.taskId)
+ // Adds remove wallpaper operation
+ wct.assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ fun onDesktopWindowClose_multipleActiveTasks_hasMinimized() {
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ val wallpaperToken = MockToken().token()
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId)
+
+ val wct = WindowContainerTransaction()
+ controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, taskId = task1.taskId)
+ // Adds remove wallpaper operation
+ wct.assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val freeformTask = setUpFreeformTask()
+ markTaskVisible(freeformTask)
+ val fullscreenTask = createFullscreenTask()
+
+ val result = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+ assertThat(result?.changes?.get(fullscreenTask.token.asBinder())?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
+ fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val freeformTask = setUpFreeformTask()
+ markTaskVisible(freeformTask)
+ val fullscreenTask = createFullscreenTask()
+
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ // Make sure we only reorder the new task to top (we don't reorder the old task to bottom)
+ assertThat(wct?.hierarchyOps?.size).isEqualTo(1)
+ wct!!.assertReorderAt(0, fullscreenTask, toTop = true)
+ }
+
+ @Test
+ fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
+ freeformTasks.forEach { markTaskVisible(it) }
+ val fullscreenTask = createFullscreenTask()
+
+ val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
+
+ // Make sure we reorder the new task to top, and the back task to the bottom
+ assertThat(wct!!.hierarchyOps.size).isEqualTo(2)
+ wct.assertReorderAt(0, fullscreenTask, toTop = true)
+ wct.assertReorderAt(1, freeformTasks[0], toTop = false)
+ }
+
+ @Test
+ fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val freeformTask = setUpFreeformTask()
+ markTaskHidden(freeformTask)
+ val fullscreenTask = createFullscreenTask()
+ assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
+ }
+
+ @Test
+ fun handleRequest_fullscreenTask_noOtherTasks_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val fullscreenTask = createFullscreenTask()
+ assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
+ }
+
+ @Test
+ fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY)
+ createFreeformTask(displayId = SECOND_DISPLAY)
+
+ val result = controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay))
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
+ freeformTasks.forEach { markTaskVisible(it) }
+ val newFreeformTask = createFreeformTask()
+
+ val wct = controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN))
+
+ assertThat(wct?.hierarchyOps?.size).isEqualTo(1)
+ wct!!.assertReorderAt(0, freeformTasks[0], toTop = false) // Reorder to the bottom
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val freeformTask1 = setUpFreeformTask()
+ val freeformTask2 = createFreeformTask()
+
+ markTaskHidden(freeformTask1)
+ val result =
+ controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT))
+
+ assertNotNull(result, "Should handle request")
+ assertThat(result.hierarchyOps?.size).isEqualTo(2)
+ result.assertReorderAt(1, freeformTask2, toTop = true)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val freeformTask1 = setUpFreeformTask()
+ val freeformTask2 = createFreeformTask()
+
+ markTaskHidden(freeformTask1)
+ val result =
+ controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT))
+
+ assertNotNull(result, "Should handle request")
+ assertThat(result.hierarchyOps?.size).isEqualTo(3)
+ // Add desktop wallpaper activity
+ result.assertPendingIntentAt(0, desktopWallpaperIntent)
+ // Bring active desktop tasks to front
+ result.assertReorderAt(1, freeformTask1, toTop = true)
+ // Bring new task to front
+ result.assertReorderAt(2, freeformTask2, toTop = true)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val task = createFreeformTask()
+ val result = controller.handleRequest(Binder(), createTransition(task))
+
+ assertNotNull(result, "Should handle request")
+ assertThat(result.hierarchyOps?.size).isEqualTo(1)
+ result.assertReorderAt(0, task, toTop = true)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ 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)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
+ // Second display task
+ createFreeformTask(displayId = SECOND_DISPLAY)
+
+ val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay))
+
+ assertNotNull(result, "Should handle request")
+ assertThat(result.hierarchyOps?.size).isEqualTo(1)
+ result.assertReorderAt(0, taskDefaultDisplay, toTop = true)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
+ // Second display task
+ createFreeformTask(displayId = SECOND_DISPLAY)
+
+ val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay))
+
+ 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, taskDefaultDisplay, toTop = true)
+ }
+
+ @Test
+ fun handleRequest_freeformTask_alreadyInDesktop_noOverrideDensity_noConfigDensityChange() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+ whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(false)
+
+ val freeformTask1 = setUpFreeformTask()
+ markTaskVisible(freeformTask1)
+
+ val freeformTask2 = createFreeformTask()
+ val result =
+ controller.handleRequest(freeformTask2.token.asBinder(), createTransition(freeformTask2))
+ assertFalse(result.anyDensityConfigChange(freeformTask2.token))
+ }
+
+ @Test
+ fun handleRequest_freeformTask_alreadyInDesktop_overrideDensity_hasConfigDensityChange() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+ whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(true)
+
+ val freeformTask1 = setUpFreeformTask()
+ markTaskVisible(freeformTask1)
+
+ val freeformTask2 = createFreeformTask()
+ val result =
+ controller.handleRequest(freeformTask2.token.asBinder(), createTransition(freeformTask2))
+ assertTrue(result.anyDensityConfigChange(freeformTask2.token))
+ }
+
+ @Test
+ fun handleRequest_notOpenOrToFrontTransition_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val task =
+ TestRunningTaskInfoBuilder()
+ .setActivityType(ACTIVITY_TYPE_STANDARD)
+ .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
+ .build()
+ val transition = createTransition(task = task, type = WindowManager.TRANSIT_CLOSE)
+ val result = controller.handleRequest(Binder(), transition)
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun handleRequest_noTriggerTask_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+ assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull()
+ }
+
+ @Test
+ fun handleRequest_triggerTaskNotStandard_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+ val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
+ assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
+ }
+
+ @Test
+ fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() {
+ assumeTrue(ENABLE_SHELL_TRANSITIONS)
+
+ val task =
+ TestRunningTaskInfoBuilder()
+ .setActivityType(ACTIVITY_TYPE_STANDARD)
+ .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
+ .build()
+ assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
+ }
+
+ @Test
+ fun handleRequest_recentsAnimationRunning_returnNull() {
+ // Set up a visible freeform task so a fullscreen task should be converted to freeform
+ val freeformTask = setUpFreeformTask()
+ markTaskVisible(freeformTask)
+
+ // Mark recents animation running
+ recentsTransitionStateListener.onAnimationStateChanged(true)
+
+ // Open a fullscreen task, check that it does not result in a WCT with changes to it
+ val fullscreenTask = createFullscreenTask()
+ assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
+ }
+
+ @Test
+ fun handleRequest_shouldLaunchAsModal_returnSwitchToFullscreenWCT() {
+ setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+ val task =
+ setUpFreeformTask().apply {
+ isTopActivityTransparent = true
+ numActivities = 1
}
- }
-
- @Test
- fun getTaskWindowingMode() {
- val fullscreenTask = setUpFullscreenTask()
- val freeformTask = setUpFreeformTask()
-
- assertThat(controller.getTaskWindowingMode(fullscreenTask.taskId))
- .isEqualTo(WINDOWING_MODE_FULLSCREEN)
- assertThat(controller.getTaskWindowingMode(freeformTask.taskId))
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- assertThat(controller.getTaskWindowingMode(999)).isEqualTo(WINDOWING_MODE_UNDEFINED)
- }
-
- @Test
- fun onDesktopWindowClose_noActiveTasks() {
- val wct = WindowContainerTransaction()
- controller.onDesktopWindowClose(wct, 1 /* taskId */)
- // Doesn't modify transaction
- assertThat(wct.hierarchyOps).isEmpty()
- }
-
- @Test
- fun onDesktopWindowClose_singleActiveTask_noWallpaperActivityToken() {
- val task = setUpFreeformTask()
- val wct = WindowContainerTransaction()
- controller.onDesktopWindowClose(wct, task.taskId)
- // Doesn't modify transaction
- assertThat(wct.hierarchyOps).isEmpty()
- }
-
- @Test
- fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() {
- val task = setUpFreeformTask()
- val wallpaperToken = MockToken().token()
- desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-
- val wct = WindowContainerTransaction()
- controller.onDesktopWindowClose(wct, task.taskId)
- // Adds remove wallpaper operation
- wct.assertRemoveAt(index = 0, wallpaperToken)
- }
-
- @Test
- fun onDesktopWindowClose_multipleActiveTasks() {
- val task1 = setUpFreeformTask()
- setUpFreeformTask()
- val wallpaperToken = MockToken().token()
- desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-
- val wct = WindowContainerTransaction()
- controller.onDesktopWindowClose(wct, task1.taskId)
- // Doesn't modify transaction
- assertThat(wct.hierarchyOps).isEmpty()
- }
-
- @Test
- fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val freeformTask = setUpFreeformTask()
- markTaskVisible(freeformTask)
- val fullscreenTask = createFullscreenTask()
-
- val result = controller.handleRequest(Binder(), createTransition(fullscreenTask))
- assertThat(result?.changes?.get(fullscreenTask.token.asBinder())?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- }
-
- @Test
- fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val freeformTask = setUpFreeformTask()
- markTaskVisible(freeformTask)
- val fullscreenTask = createFullscreenTask()
-
- val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
-
- // Make sure we only reorder the new task to top (we don't reorder the old task to bottom)
- assertThat(wct?.hierarchyOps?.size).isEqualTo(1)
- wct!!.assertReorderAt(0, fullscreenTask, toTop = true)
- }
-
- @Test
- fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
- val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
- freeformTasks.forEach { markTaskVisible(it) }
- val fullscreenTask = createFullscreenTask()
-
- val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask))
-
- // Make sure we reorder the new task to top, and the back task to the bottom
- assertThat(wct!!.hierarchyOps.size).isEqualTo(2)
- wct!!.assertReorderAt(0, fullscreenTask, toTop = true)
- wct!!.assertReorderAt(1, freeformTasks[0], toTop = false)
- }
-
- @Test
- fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val freeformTask = setUpFreeformTask()
- markTaskHidden(freeformTask)
- val fullscreenTask = createFullscreenTask()
- assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
- }
- @Test
- fun handleRequest_fullscreenTask_noOtherTasks_returnNull() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
+ val result = controller.handleRequest(Binder(), createTransition(task))
+ assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+ }
- val fullscreenTask = createFullscreenTask()
- assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
- }
-
- @Test
- fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY)
- createFreeformTask(displayId = SECOND_DISPLAY)
-
- val result =
- controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay))
- assertThat(result).isNull()
- }
-
- @Test
- fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
- val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
- freeformTasks.forEach { markTaskVisible(it) }
- val newFreeformTask = createFreeformTask()
-
- val wct =
- controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN))
-
- assertThat(wct?.hierarchyOps?.size).isEqualTo(1)
- wct!!.assertReorderAt(0, freeformTasks[0], toTop = false) // Reorder to the bottom
- }
-
- @Test
- fun handleRequest_freeformTask_freeformNotVisible_reorderedToTop() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val freeformTask1 = setUpFreeformTask()
- markTaskHidden(freeformTask1)
-
- val freeformTask2 = createFreeformTask()
- val result =
- controller.handleRequest(
- Binder(),
- createTransition(freeformTask2, type = TRANSIT_TO_FRONT)
- )
-
- assertThat(result?.hierarchyOps?.size).isEqualTo(2)
- result!!.assertReorderAt(1, freeformTask2, toTop = true)
- }
-
- @Test
- fun handleRequest_freeformTask_noOtherTasks_reorderedToTop() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val task = createFreeformTask()
- val result = controller.handleRequest(Binder(), createTransition(task))
-
- assertThat(result?.hierarchyOps?.size).isEqualTo(1)
- result!!.assertReorderAt(0, task, toTop = true)
- }
-
- @Test
- fun handleRequest_freeformTask_freeformOnOtherDisplayOnly_reorderedToTop() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
- val taskSecondDisplay = createFreeformTask(displayId = SECOND_DISPLAY)
-
- val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay))
- assertThat(result?.hierarchyOps?.size).isEqualTo(1)
- result!!.assertReorderAt(0, taskDefaultDisplay, toTop = true)
- }
-
- @Test
- fun handleRequest_freeformTask_alreadyInDesktop_noOverrideDensity_noConfigDensityChange() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
- whenever(DesktopModeStatus.isDesktopDensityOverrideSet()).thenReturn(false)
-
- val freeformTask1 = setUpFreeformTask()
- markTaskVisible(freeformTask1)
-
- val freeformTask2 = createFreeformTask()
- val result = controller.handleRequest(freeformTask2.token.asBinder(),
- createTransition(freeformTask2))
- assertFalse(result.anyDensityConfigChange(freeformTask2.token))
- }
-
- @Test
- fun handleRequest_freeformTask_alreadyInDesktop_overrideDensity_hasConfigDensityChange() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
- whenever(DesktopModeStatus.isDesktopDensityOverrideSet()).thenReturn(true)
-
- val freeformTask1 = setUpFreeformTask()
- markTaskVisible(freeformTask1)
-
- val freeformTask2 = createFreeformTask()
- val result = controller.handleRequest(freeformTask2.token.asBinder(),
- createTransition(freeformTask2))
- assertTrue(result.anyDensityConfigChange(freeformTask2.token))
- }
-
- @Test
- fun handleRequest_notOpenOrToFrontTransition_returnNull() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val task =
- TestRunningTaskInfoBuilder()
- .setActivityType(ACTIVITY_TYPE_STANDARD)
- .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
- .build()
- val transition = createTransition(task = task, type = WindowManager.TRANSIT_CLOSE)
- val result = controller.handleRequest(Binder(), transition)
- assertThat(result).isNull()
- }
-
- @Test
- fun handleRequest_noTriggerTask_returnNull() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
- assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull()
- }
-
- @Test
- fun handleRequest_triggerTaskNotStandard_returnNull() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
- val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build()
- assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
- }
-
- @Test
- fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val task =
- TestRunningTaskInfoBuilder()
- .setActivityType(ACTIVITY_TYPE_STANDARD)
- .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
- .build()
- assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull()
- }
-
- @Test
- fun handleRequest_recentsAnimationRunning_returnNull() {
- // Set up a visible freeform task so a fullscreen task should be converted to freeform
- val freeformTask = setUpFreeformTask()
- markTaskVisible(freeformTask)
-
- // Mark recents animation running
- recentsTransitionStateListener.onAnimationStateChanged(true)
-
- // Open a fullscreen task, check that it does not result in a WCT with changes to it
- val fullscreenTask = createFullscreenTask()
- assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull()
- }
-
- @Test
- fun handleRequest_shouldLaunchAsModal_returnSwitchToFullscreenWCT() {
- setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
- val task = setUpFreeformTask().apply {
- isTopActivityTransparent = true
- numActivities = 1
- }
-
- val result = controller.handleRequest(Binder(), createTransition(task))
- assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
- .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_backTransition_singleActiveTask_noToken() {
- val task = setUpFreeformTask()
- val result =
- controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
- // Doesn't handle request
- assertThat(result).isNull()
- }
-
- @Test
- @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperDisabled() {
- desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
-
- val task = setUpFreeformTask()
- val result =
- controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
- // Doesn't handle request
- assertThat(result).isNull()
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperEnabled() {
- val wallpaperToken = MockToken().token()
- desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
-
- val task = setUpFreeformTask()
- val result =
- controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
- assertThat(result).isNotNull()
- // Creates remove wallpaper transaction
- result!!.assertRemoveAt(index = 0, wallpaperToken)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
- fun handleRequest_backTransition_multipleActiveTasks() {
- desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
-
- val task1 = setUpFreeformTask()
- setUpFreeformTask()
- val result =
- controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
- // Doesn't handle request
- assertThat(result).isNull()
- }
-
- @Test
- fun desktopTasksVisibilityChange_visible_setLaunchAdjacentDisabled() {
- val task = setUpFreeformTask()
- clearInvocations(launchAdjacentController)
-
- markTaskVisible(task)
- shellExecutor.flushAll()
- verify(launchAdjacentController).launchAdjacentEnabled = false
- }
-
- @Test
- fun desktopTasksVisibilityChange_invisible_setLaunchAdjacentEnabled() {
- val task = setUpFreeformTask()
- markTaskVisible(task)
- clearInvocations(launchAdjacentController)
-
- markTaskHidden(task)
- shellExecutor.flushAll()
- verify(launchAdjacentController).launchAdjacentEnabled = true
- }
- @Test
- fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() {
- val task1 = setUpFullscreenTask()
- val task2 = setUpFullscreenTask()
- val task3 = setUpFullscreenTask()
-
- task1.isFocused = true
- task2.isFocused = false
- task3.isFocused = false
-
- controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY)
-
- val wct = getLatestMoveToDesktopWct()
- assertThat(wct.changes[task1.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- }
-
- @Test
- fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() {
- val task1 = setUpSplitScreenTask()
- val task2 = setUpFullscreenTask()
- val task3 = setUpFullscreenTask()
- val task4 = setUpSplitScreenTask()
-
- task1.isFocused = true
- task2.isFocused = false
- task3.isFocused = false
- task4.isFocused = true
-
- task4.parentTaskId = task1.taskId
-
- controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY)
-
- val wct = getLatestMoveToDesktopWct()
- assertThat(wct.changes[task4.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- verify(splitScreenController).prepareExitSplitScreen(
- any(),
- anyInt(),
- eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)
- )
- }
-
- @Test
- fun moveFocusedTaskToFullscreen() {
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
- val task3 = setUpFreeformTask()
-
- task1.isFocused = false
- task2.isFocused = true
- task3.isFocused = false
-
- controller.enterFullscreen(DEFAULT_DISPLAY)
-
- val wct = getLatestExitDesktopWct()
- assertThat(wct.changes[task2.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val spyController = spy(controller)
- whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
- whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
- .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
- val task = setUpFullscreenTask()
- setUpLandscapeDisplay()
-
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
- val wct = getLatestDragToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun dragToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val spyController = spy(controller)
- whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
- whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
- .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
- val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
- setUpLandscapeDisplay()
-
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
- val wct = getLatestDragToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun dragToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val spyController = spy(controller)
- whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
- whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
- .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
- val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
+ @Test
+ fun handleRequest_systemUIActivity_returnSwitchToFullscreenWCT() {
+ val task = setUpFreeformTask()
+
+ // Set task as systemUI package
+ val systemUIPackageName = context.resources.getString(
+ com.android.internal.R.string.config_systemUi)
+ val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+ task.baseActivity = baseComponent
+
+ val result = controller.handleRequest(Binder(), createTransition(task))
+ assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_backTransition_singleActiveTaskNoTokenFlagDisabled_doesNotHandle() {
+ val task = setUpFreeformTask()
+
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_backTransition_singleActiveTaskNoTokenFlagEnabled_doesNotHandle() {
+ val task = setUpFreeformTask()
+
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_backTransition_singleActiveTaskWithTokenFlagDisabled_doesNotHandle() {
+ val task = setUpFreeformTask()
+
+ desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_backTransition_singleActiveTaskWithTokenFlagEnabled_handlesRequest() {
+ val task = setUpFreeformTask()
+ val wallpaperToken = MockToken().token()
+
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+
+ assertNotNull(result, "Should handle request")
+ // Should create remove wallpaper transaction
+ .assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_backTransition_multipleActiveTasksFlagDisabled_doesNotHandle() {
+ val task1 = setUpFreeformTask()
+ setUpFreeformTask()
+
+ desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_backTransition_multipleActiveTasksFlagEnabled_doesNotHandle() {
+ val task1 = setUpFreeformTask()
+ setUpFreeformTask()
+
+ desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_backTransition_multipleActiveTasksSingleNonClosing_handlesRequest() {
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val wallpaperToken = MockToken().token()
+
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+ assertNotNull(result, "Should handle request")
+ // Should create remove wallpaper transaction
+ .assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_backTransition_multipleActiveTasksSingleNonMinimized_handlesRequest() {
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val wallpaperToken = MockToken().token()
+
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK))
+
+ assertNotNull(result, "Should handle request")
+ // Should create remove wallpaper transaction
+ .assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_singleActiveTaskNoTokenFlagDisabled_doesNotHandle() {
+ val task = setUpFreeformTask()
+
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_singleActiveTaskNoTokenFlagEnabled_doesNotHandle() {
+ val task = setUpFreeformTask()
+
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_singleActiveTaskWithTokenFlagDisabled_doesNotHandle() {
+ val task = setUpFreeformTask()
+
+ desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_singleActiveTaskWithTokenFlagEnabled_handlesRequest() {
+ val task = setUpFreeformTask()
+ val wallpaperToken = MockToken().token()
+
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE))
+
+ assertNotNull(result, "Should handle request")
+ // Should create remove wallpaper transaction
+ .assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_multipleActiveTasksFlagDisabled_doesNotHandle() {
+ val task1 = setUpFreeformTask()
+ setUpFreeformTask()
+
+ desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_multipleActiveTasksFlagEnabled_doesNotHandle() {
+ val task1 = setUpFreeformTask()
+ setUpFreeformTask()
+
+ desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+ assertNull(result, "Should not handle request")
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_multipleActiveTasksSingleNonClosing_handlesRequest() {
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val wallpaperToken = MockToken().token()
+
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ desktopModeTaskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+ assertNotNull(result, "Should handle request")
+ // Should create remove wallpaper transaction
+ .assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_closeTransition_multipleActiveTasksSingleNonMinimized_handlesRequest() {
+ val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
+ val wallpaperToken = MockToken().token()
+
+ desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken
+ desktopModeTaskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
+ val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE))
+
+ assertNotNull(result, "Should handle request")
+ // Should create remove wallpaper transaction
+ .assertRemoveAt(index = 0, wallpaperToken)
+ }
+
+ @Test
+ fun desktopTasksVisibilityChange_visible_setLaunchAdjacentDisabled() {
+ val task = setUpFreeformTask()
+ clearInvocations(launchAdjacentController)
+
+ markTaskVisible(task)
+ shellExecutor.flushAll()
+ verify(launchAdjacentController).launchAdjacentEnabled = false
+ }
+
+ @Test
+ fun desktopTasksVisibilityChange_invisible_setLaunchAdjacentEnabled() {
+ val task = setUpFreeformTask()
+ markTaskVisible(task)
+ clearInvocations(launchAdjacentController)
+
+ markTaskHidden(task)
+ shellExecutor.flushAll()
+ verify(launchAdjacentController).launchAdjacentEnabled = true
+ }
+
+ @Test
+ fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() {
+ val task1 = setUpFullscreenTask()
+ val task2 = setUpFullscreenTask()
+ val task3 = setUpFullscreenTask()
+
+ task1.isFocused = true
+ task2.isFocused = false
+ task3.isFocused = false
+
+ controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+ val wct = getLatestEnterDesktopWct()
+ assertThat(wct.changes[task1.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
+ fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() {
+ val task1 = setUpSplitScreenTask()
+ val task2 = setUpFullscreenTask()
+ val task3 = setUpFullscreenTask()
+ val task4 = setUpSplitScreenTask()
+
+ task1.isFocused = true
+ task2.isFocused = false
+ task3.isFocused = false
+ task4.isFocused = true
+
+ task4.parentTaskId = task1.taskId
+
+ controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+ val wct = getLatestEnterDesktopWct()
+ assertThat(wct.changes[task4.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ verify(splitScreenController)
+ .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE))
+ }
+
+ @Test
+ fun moveFocusedTaskToFullscreen() {
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ val task3 = setUpFreeformTask()
+
+ task1.isFocused = false
+ task2.isFocused = true
+ task3.isFocused = false
+
+ controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN)
+
+ val wct = getLatestExitDesktopWct()
+ assertThat(wct.changes[task2.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask()
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task =
+ setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task =
+ setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task =
+ setUpFullscreenTask(
+ isResizable = false,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
shouldLetterbox = true)
- setUpLandscapeDisplay()
-
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
- val wct = getLatestDragToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun dragToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val spyController = spy(controller)
- whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
- whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
- .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
- val task = setUpFullscreenTask(isResizable = false,
- screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
- setUpLandscapeDisplay()
-
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
- val wct = getLatestDragToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun dragToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val spyController = spy(controller)
- whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
- whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
- .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
- val task = setUpFullscreenTask(isResizable = false,
- screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
- setUpLandscapeDisplay()
-
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
- val wct = getLatestDragToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun dragToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val spyController = spy(controller)
- whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
- whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
- .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
- val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
- setUpPortraitDisplay()
-
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
- val wct = getLatestDragToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun dragToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val spyController = spy(controller)
- whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
- whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
- .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
- val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task =
+ setUpFullscreenTask(
+ deviceOrientation = ORIENTATION_PORTRAIT,
screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
- setUpPortraitDisplay()
-
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
- val wct = getLatestDragToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun dragToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val spyController = spy(controller)
- whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
- whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
- .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
- val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
- screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
- setUpPortraitDisplay()
-
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
- val wct = getLatestDragToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun dragToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val spyController = spy(controller)
- whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
- whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
- .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
- val task = setUpFullscreenTask(isResizable = false,
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task =
+ setUpFullscreenTask(
+ deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
+ shouldLetterbox = true)
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task =
+ setUpFullscreenTask(
+ isResizable = false,
deviceOrientation = ORIENTATION_PORTRAIT,
screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
- setUpPortraitDisplay()
-
- spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
- val wct = getLatestDragToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
- }
-
- @Test
- @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
- fun dragToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
- doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- val spyController = spy(controller)
- whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
- whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
- .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
-
- val task = setUpFullscreenTask(isResizable = false,
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task =
+ setUpFullscreenTask(
+ isResizable = false,
deviceOrientation = ORIENTATION_PORTRAIT,
- screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
- setUpPortraitDisplay()
-
- spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task)
- val wct = getLatestDragToDesktopWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
- }
-
- @Test
- fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() {
- val task = setUpFreeformTask()
- val mockSurface = mock(SurfaceControl::class.java)
- val mockDisplayLayout = mock(DisplayLayout::class.java)
- whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
- whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
- controller.onDragPositioningMove(task, mockSurface, 200f,
- Rect(100, -100, 500, 1000))
-
- controller.onDragPositioningEnd(task,
- Point(100, -100), /* position */
- PointF(200f, -200f), /* inputCoordinate */
- Rect(100, -100, 500, 1000), /* taskBounds */
- Rect(0, 50, 2000, 2000) /* validDragArea */
- )
- val rectAfterEnd = Rect(100, 50, 500, 1150)
- verify(transitions).startTransition(
- eq(TRANSIT_CHANGE), Mockito.argThat { wct ->
- return@argThat wct.changes.any { (token, change) ->
- change.configuration.windowConfiguration.bounds == rectAfterEnd
- }
- }, eq(null))
- }
-
- fun enterSplit_freeformTaskIsMovedToSplit() {
- val task1 = setUpFreeformTask()
- val task2 = setUpFreeformTask()
- val task3 = setUpFreeformTask()
-
- task1.isFocused = false
- task2.isFocused = true
- task3.isFocused = false
-
- controller.enterSplit(DEFAULT_DISPLAY, false)
-
- verify(splitScreenController).requestEnterSplitSelect(
- task2,
- any(),
- SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT,
- task2.configuration.windowConfiguration.bounds
- )
- }
-
- @Test
- fun toggleBounds_togglesToStableBounds() {
- val bounds = Rect(0, 0, 100, 100)
- val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
-
- controller.toggleDesktopTaskSize(task)
- // Assert bounds set to stable bounds
- val wct = getLatestToggleResizeDesktopTaskWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
- }
-
- @Test
- fun toggleBounds_lastBoundsBeforeMaximizeSaved() {
- val bounds = Rect(0, 0, 100, 100)
- val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
-
- controller.toggleDesktopTaskSize(task)
- assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId))
- .isEqualTo(bounds)
- }
-
- @Test
- fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize() {
- val boundsBeforeMaximize = Rect(0, 0, 100, 100)
- val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
-
- // Maximize
- controller.toggleDesktopTaskSize(task)
- task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
-
- // Restore
- controller.toggleDesktopTaskSize(task)
-
- // Assert bounds set to last bounds before maximize
- val wct = getLatestToggleResizeDesktopTaskWct()
- assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
- }
-
- @Test
- fun toggleBounds_removesLastBoundsBeforeMaximizeAfterRestoringBounds() {
- val boundsBeforeMaximize = Rect(0, 0, 100, 100)
- val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
-
- // Maximize
- controller.toggleDesktopTaskSize(task)
- task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
-
- // Restore
- controller.toggleDesktopTaskSize(task)
-
- // Assert last bounds before maximize removed after use
- assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
- }
-
- private val desktopWallpaperIntent: Intent
- get() = Intent(context, DesktopWallpaperActivity::class.java)
-
- private fun setUpFreeformTask(
- displayId: Int = DEFAULT_DISPLAY,
- bounds: Rect? = null
- ): RunningTaskInfo {
- val task = createFreeformTask(displayId, bounds)
- whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
- desktopModeTaskRepository.addActiveTask(displayId, task.taskId)
- desktopModeTaskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId)
- runningTasks.add(task)
- return task
- }
-
- private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
- val task = createHomeTask(displayId)
- whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
- runningTasks.add(task)
- return task
- }
-
- private fun setUpFullscreenTask(
- displayId: Int = DEFAULT_DISPLAY,
- isResizable: Boolean = true,
- windowingMode: Int = WINDOWING_MODE_FULLSCREEN,
- deviceOrientation: Int = ORIENTATION_LANDSCAPE,
- screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED,
- shouldLetterbox: Boolean = false
- ): RunningTaskInfo {
- val task = createFullscreenTask(displayId)
- val activityInfo = ActivityInfo()
- activityInfo.screenOrientation = screenOrientation
- with(task) {
- topActivityInfo = activityInfo
- isResizeable = isResizable
- configuration.orientation = deviceOrientation
- configuration.windowConfiguration.windowingMode = windowingMode
-
- if (shouldLetterbox) {
- if (deviceOrientation == ORIENTATION_LANDSCAPE &&
- screenOrientation == SCREEN_ORIENTATION_PORTRAIT) {
- // Letterbox to portrait size
- appCompatTaskInfo.topActivityBoundsLetterboxed = true
- appCompatTaskInfo.topActivityLetterboxWidth = 1200
- appCompatTaskInfo.topActivityLetterboxHeight = 1600
- } else if (deviceOrientation == ORIENTATION_PORTRAIT &&
- screenOrientation == SCREEN_ORIENTATION_LANDSCAPE) {
- // Letterbox to landscape size
- appCompatTaskInfo.topActivityBoundsLetterboxed = true
- appCompatTaskInfo.topActivityLetterboxWidth = 1600
- appCompatTaskInfo.topActivityLetterboxHeight = 1200
- }
- } else {
- appCompatTaskInfo.topActivityBoundsLetterboxed = false
- }
-
- if (deviceOrientation == ORIENTATION_LANDSCAPE) {
- configuration.windowConfiguration.appBounds = Rect(0, 0,
- DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT)
- } else {
- configuration.windowConfiguration.appBounds = Rect(0, 0,
- DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG)
- }
- }
- whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
- whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
- runningTasks.add(task)
- return task
- }
-
- private fun setUpLandscapeDisplay() {
- whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG)
- whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT)
- }
-
- private fun setUpPortraitDisplay() {
- whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT)
- whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG)
- }
-
- private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
- val task = createSplitScreenTask(displayId)
- whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
- whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true)
- whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
- runningTasks.add(task)
- return task
- }
-
- private fun markTaskVisible(task: RunningTaskInfo) {
- desktopModeTaskRepository.updateVisibleFreeformTasks(
- task.displayId,
- task.taskId,
- visible = true
- )
- }
-
- private fun markTaskHidden(task: RunningTaskInfo) {
- desktopModeTaskRepository.updateVisibleFreeformTasks(
- task.displayId,
- task.taskId,
- visible = false
- )
- }
-
- private fun getLatestWct(
- @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
- handlerClass: Class<out TransitionHandler>? = null
- ): WindowContainerTransaction {
- val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
- if (ENABLE_SHELL_TRANSITIONS) {
- if (handlerClass == null) {
- verify(transitions).startTransition(eq(type), arg.capture(), isNull())
- } else {
- verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass))
- }
- } else {
- verify(shellTaskOrganizer).applyTransaction(arg.capture())
- }
- return arg.value
- }
-
- private fun getLatestToggleResizeDesktopTaskWct(): WindowContainerTransaction {
- val arg: ArgumentCaptor<WindowContainerTransaction> =
- ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
- if (ENABLE_SHELL_TRANSITIONS) {
- verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce())
- .startTransition(capture(arg))
- } else {
- verify(shellTaskOrganizer).applyTransaction(capture(arg))
- }
- return arg.value
- }
-
- private fun getLatestMoveToDesktopWct(): WindowContainerTransaction {
- val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
- if (ENABLE_SHELL_TRANSITIONS) {
- verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture())
- } else {
- verify(shellTaskOrganizer).applyTransaction(arg.capture())
- }
- return arg.value
- }
-
- private fun getLatestDragToDesktopWct(): WindowContainerTransaction {
- val arg: ArgumentCaptor<WindowContainerTransaction> =
- ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
- if (ENABLE_SHELL_TRANSITIONS) {
- verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg))
- } else {
- verify(shellTaskOrganizer).applyTransaction(capture(arg))
- }
- return arg.value
- }
-
- private fun getLatestExitDesktopWct(): WindowContainerTransaction {
- val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
- if (ENABLE_SHELL_TRANSITIONS) {
- verify(exitDesktopTransitionHandler)
- .startTransition(eq(TRANSIT_EXIT_DESKTOP_MODE), arg.capture(), any(), any())
- } else {
- verify(shellTaskOrganizer).applyTransaction(arg.capture())
- }
- return arg.value
- }
-
- private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? =
- wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds
-
-
- private fun verifyWCTNotExecuted() {
- if (ENABLE_SHELL_TRANSITIONS) {
- verify(transitions, never()).startTransition(anyInt(), any(), isNull())
- } else {
- verify(shellTaskOrganizer, never()).applyTransaction(any())
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE,
+ shouldLetterbox = true)
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() {
+ val task = setUpFreeformTask()
+ val mockSurface = mock(SurfaceControl::class.java)
+ val mockDisplayLayout = mock(DisplayLayout::class.java)
+ whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
+ whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
+ controller.onDragPositioningMove(task, mockSurface, 200f, Rect(100, -100, 500, 1000))
+
+ controller.onDragPositioningEnd(
+ task,
+ Point(100, -100), /* position */
+ PointF(200f, -200f), /* inputCoordinate */
+ Rect(100, -100, 500, 1000), /* taskBounds */
+ Rect(0, 50, 2000, 2000) /* validDragArea */)
+ val rectAfterEnd = Rect(100, 50, 500, 1150)
+ verify(transitions)
+ .startTransition(
+ eq(TRANSIT_CHANGE),
+ Mockito.argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ change.configuration.windowConfiguration.bounds == rectAfterEnd
+ }
+ },
+ eq(null))
+ }
+
+ fun enterSplit_freeformTaskIsMovedToSplit() {
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ val task3 = setUpFreeformTask()
+
+ task1.isFocused = false
+ task2.isFocused = true
+ task3.isFocused = false
+
+ controller.enterSplit(DEFAULT_DISPLAY, false)
+
+ verify(splitScreenController)
+ .requestEnterSplitSelect(
+ task2,
+ any(),
+ SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT,
+ task2.configuration.windowConfiguration.bounds)
+ }
+
+ @Test
+ fun toggleBounds_togglesToStableBounds() {
+ val bounds = Rect(0, 0, 100, 100)
+ val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
+
+ controller.toggleDesktopTaskSize(task)
+ // Assert bounds set to stable bounds
+ val wct = getLatestToggleResizeDesktopTaskWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
+ }
+
+ @Test
+ fun toggleBounds_lastBoundsBeforeMaximizeSaved() {
+ val bounds = Rect(0, 0, 100, 100)
+ val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
+
+ controller.toggleDesktopTaskSize(task)
+ assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isEqualTo(bounds)
+ }
+
+ @Test
+ fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize() {
+ val boundsBeforeMaximize = Rect(0, 0, 100, 100)
+ val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
+
+ // Maximize
+ controller.toggleDesktopTaskSize(task)
+ task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
+
+ // Restore
+ controller.toggleDesktopTaskSize(task)
+
+ // Assert bounds set to last bounds before maximize
+ val wct = getLatestToggleResizeDesktopTaskWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
+ }
+
+ @Test
+ fun toggleBounds_removesLastBoundsBeforeMaximizeAfterRestoringBounds() {
+ val boundsBeforeMaximize = Rect(0, 0, 100, 100)
+ val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
+
+ // Maximize
+ controller.toggleDesktopTaskSize(task)
+ task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
+
+ // Restore
+ controller.toggleDesktopTaskSize(task)
+
+ // Assert last bounds before maximize removed after use
+ assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
+ }
+
+ private val desktopWallpaperIntent: Intent
+ get() = Intent(context, DesktopWallpaperActivity::class.java)
+
+ private fun setUpFreeformTask(
+ displayId: Int = DEFAULT_DISPLAY,
+ bounds: Rect? = null
+ ): RunningTaskInfo {
+ val task = createFreeformTask(displayId, bounds)
+ val activityInfo = ActivityInfo()
+ task.topActivityInfo = activityInfo
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ desktopModeTaskRepository.addActiveTask(displayId, task.taskId)
+ desktopModeTaskRepository.updateVisibleFreeformTasks(displayId, task.taskId, visible = true)
+ desktopModeTaskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId)
+ runningTasks.add(task)
+ return task
+ }
+
+ private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
+ val task = createHomeTask(displayId)
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ runningTasks.add(task)
+ return task
+ }
+
+ private fun setUpFullscreenTask(
+ displayId: Int = DEFAULT_DISPLAY,
+ isResizable: Boolean = true,
+ windowingMode: Int = WINDOWING_MODE_FULLSCREEN,
+ deviceOrientation: Int = ORIENTATION_LANDSCAPE,
+ screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED,
+ shouldLetterbox: Boolean = false
+ ): RunningTaskInfo {
+ val task = createFullscreenTask(displayId)
+ val activityInfo = ActivityInfo()
+ activityInfo.screenOrientation = screenOrientation
+ with(task) {
+ topActivityInfo = activityInfo
+ isResizeable = isResizable
+ configuration.orientation = deviceOrientation
+ configuration.windowConfiguration.windowingMode = windowingMode
+
+ if (shouldLetterbox) {
+ if (deviceOrientation == ORIENTATION_LANDSCAPE &&
+ screenOrientation == SCREEN_ORIENTATION_PORTRAIT) {
+ // Letterbox to portrait size
+ appCompatTaskInfo.topActivityBoundsLetterboxed = true
+ appCompatTaskInfo.topActivityLetterboxWidth = 1200
+ appCompatTaskInfo.topActivityLetterboxHeight = 1600
+ } else if (deviceOrientation == ORIENTATION_PORTRAIT &&
+ screenOrientation == SCREEN_ORIENTATION_LANDSCAPE) {
+ // Letterbox to landscape size
+ appCompatTaskInfo.topActivityBoundsLetterboxed = true
+ appCompatTaskInfo.topActivityLetterboxWidth = 1600
+ appCompatTaskInfo.topActivityLetterboxHeight = 1200
}
- }
-
- private fun createTransition(
- task: RunningTaskInfo?,
- @WindowManager.TransitionType type: Int = TRANSIT_OPEN
- ): TransitionRequestInfo {
- return TransitionRequestInfo(type, task, null /* remoteTransition */)
- }
-
- companion object {
- const val SECOND_DISPLAY = 2
- private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
- }
+ } else {
+ appCompatTaskInfo.topActivityBoundsLetterboxed = false
+ }
+
+ if (deviceOrientation == ORIENTATION_LANDSCAPE) {
+ configuration.windowConfiguration.appBounds =
+ Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT)
+ } else {
+ configuration.windowConfiguration.appBounds =
+ Rect(0, 0, DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG)
+ }
+ }
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ runningTasks.add(task)
+ return task
+ }
+
+ private fun setUpLandscapeDisplay() {
+ whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG)
+ whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT)
+ }
+
+ private fun setUpPortraitDisplay() {
+ whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT)
+ whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG)
+ }
+
+ private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
+ val task = createSplitScreenTask(displayId)
+ whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true)
+ whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ runningTasks.add(task)
+ return task
+ }
+
+ private fun markTaskVisible(task: RunningTaskInfo) {
+ desktopModeTaskRepository.updateVisibleFreeformTasks(
+ task.displayId, task.taskId, visible = true)
+ }
+
+ private fun markTaskHidden(task: RunningTaskInfo) {
+ desktopModeTaskRepository.updateVisibleFreeformTasks(
+ task.displayId, task.taskId, visible = false)
+ }
+
+ private fun getLatestWct(
+ @WindowManager.TransitionType type: Int = TRANSIT_OPEN,
+ handlerClass: Class<out TransitionHandler>? = null
+ ): WindowContainerTransaction {
+ val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ if (ENABLE_SHELL_TRANSITIONS) {
+ if (handlerClass == null) {
+ verify(transitions).startTransition(eq(type), arg.capture(), isNull())
+ } else {
+ verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass))
+ }
+ } else {
+ verify(shellTaskOrganizer).applyTransaction(arg.capture())
+ }
+ return arg.value
+ }
+
+ private fun getLatestToggleResizeDesktopTaskWct(): WindowContainerTransaction {
+ val arg: ArgumentCaptor<WindowContainerTransaction> =
+ ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ if (ENABLE_SHELL_TRANSITIONS) {
+ verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()).startTransition(capture(arg))
+ } else {
+ verify(shellTaskOrganizer).applyTransaction(capture(arg))
+ }
+ return arg.value
+ }
+
+ private fun getLatestEnterDesktopWct(): WindowContainerTransaction {
+ val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ if (ENABLE_SHELL_TRANSITIONS) {
+ verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any())
+ } else {
+ verify(shellTaskOrganizer).applyTransaction(arg.capture())
+ }
+ return arg.value
+ }
+
+ private fun getLatestDragToDesktopWct(): WindowContainerTransaction {
+ val arg: ArgumentCaptor<WindowContainerTransaction> =
+ ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ if (ENABLE_SHELL_TRANSITIONS) {
+ verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg))
+ } else {
+ verify(shellTaskOrganizer).applyTransaction(capture(arg))
+ }
+ return arg.value
+ }
+
+ private fun getLatestExitDesktopWct(): WindowContainerTransaction {
+ val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ if (ENABLE_SHELL_TRANSITIONS) {
+ verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any())
+ } else {
+ verify(shellTaskOrganizer).applyTransaction(arg.capture())
+ }
+ return arg.value
+ }
+
+ private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? =
+ wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds
+
+ private fun verifyWCTNotExecuted() {
+ if (ENABLE_SHELL_TRANSITIONS) {
+ verify(transitions, never()).startTransition(anyInt(), any(), isNull())
+ } else {
+ verify(shellTaskOrganizer, never()).applyTransaction(any())
+ }
+ }
+
+ private fun verifyExitDesktopWCTNotExecuted() {
+ if (ENABLE_SHELL_TRANSITIONS) {
+ verify(exitDesktopTransitionHandler, never()).startTransition(any(), any(), any(), any())
+ } else {
+ verify(shellTaskOrganizer, never()).applyTransaction(any())
+ }
+ }
+
+ private fun verifyEnterDesktopWCTNotExecuted() {
+ if (ENABLE_SHELL_TRANSITIONS) {
+ verify(enterDesktopTransitionHandler, never()).moveToDesktop(any(), any())
+ } else {
+ verify(shellTaskOrganizer, never()).applyTransaction(any())
+ }
+ }
+
+ private fun createTransition(
+ task: RunningTaskInfo?,
+ @WindowManager.TransitionType type: Int = TRANSIT_OPEN
+ ): TransitionRequestInfo {
+ return TransitionRequestInfo(type, task, null /* remoteTransition */)
+ }
+
+ companion object {
+ const val SECOND_DISPLAY = 2
+ private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
+ }
}
private fun WindowContainerTransaction.assertIndexInBounds(index: Int) {
- assertWithMessage("WCT does not have a hierarchy operation at index $index")
- .that(hierarchyOps.size)
- .isGreaterThan(index)
+ assertWithMessage("WCT does not have a hierarchy operation at index $index")
+ .that(hierarchyOps.size)
+ .isGreaterThan(index)
}
private fun WindowContainerTransaction.assertReorderAt(
- index: Int,
- task: RunningTaskInfo,
- toTop: Boolean? = null
+ index: Int,
+ task: RunningTaskInfo,
+ toTop: Boolean? = null
) {
- assertIndexInBounds(index)
- val op = hierarchyOps[index]
- assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER)
- assertThat(op.container).isEqualTo(task.token.asBinder())
- toTop?.let { assertThat(op.toTop).isEqualTo(it) }
+ assertIndexInBounds(index)
+ val op = hierarchyOps[index]
+ assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER)
+ assertThat(op.container).isEqualTo(task.token.asBinder())
+ toTop?.let { assertThat(op.toTop).isEqualTo(it) }
}
private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) {
- for (i in tasks.indices) {
- assertReorderAt(i, tasks[i])
- }
+ for (i in tasks.indices) {
+ assertReorderAt(i, tasks[i])
+ }
+}
+
+/** Checks if the reorder hierarchy operations in [range] correspond to [tasks] list */
+private fun WindowContainerTransaction.assertReorderSequenceInRange(
+ range: IntRange,
+ vararg tasks: RunningTaskInfo
+) {
+ assertThat(hierarchyOps.slice(range).map { it.type to it.container })
+ .containsExactlyElementsIn(tasks.map { HIERARCHY_OP_TYPE_REORDER to it.token.asBinder() })
+ .inOrder()
}
private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowContainerToken) {
- assertIndexInBounds(index)
- val op = hierarchyOps[index]
- assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
- assertThat(op.container).isEqualTo(token.asBinder())
+ assertIndexInBounds(index)
+ val op = hierarchyOps[index]
+ assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK)
+ assertThat(op.container).isEqualTo(token.asBinder())
}
private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) {
- assertIndexInBounds(index)
- val op = hierarchyOps[index]
- assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT)
- assertThat(op.pendingIntent?.intent?.component).isEqualTo(intent.component)
+ assertIndexInBounds(index)
+ val op = hierarchyOps[index]
+ assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT)
+ assertThat(op.pendingIntent?.intent?.component).isEqualTo(intent.component)
}
private fun WindowContainerTransaction.assertLaunchTaskAt(
@@ -1860,23 +2196,26 @@ private fun WindowContainerTransaction.assertLaunchTaskAt(
taskId: Int,
windowingMode: Int
) {
- val keyLaunchWindowingMode = "android.activity.windowingMode"
-
- assertIndexInBounds(index)
- val op = hierarchyOps[index]
- assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_LAUNCH_TASK)
- assertThat(op.launchOptions?.getInt(LAUNCH_KEY_TASK_ID)).isEqualTo(taskId)
- assertThat(op.launchOptions?.getInt(keyLaunchWindowingMode, WINDOWING_MODE_UNDEFINED))
- .isEqualTo(windowingMode)
+ val keyLaunchWindowingMode = "android.activity.windowingMode"
+
+ assertIndexInBounds(index)
+ val op = hierarchyOps[index]
+ assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_LAUNCH_TASK)
+ assertThat(op.launchOptions?.getInt(LAUNCH_KEY_TASK_ID)).isEqualTo(taskId)
+ assertThat(op.launchOptions?.getInt(keyLaunchWindowingMode, WINDOWING_MODE_UNDEFINED))
+ .isEqualTo(windowingMode)
}
+
private fun WindowContainerTransaction?.anyDensityConfigChange(
token: WindowContainerToken
): Boolean {
- return this?.changes?.any { change ->
- change.key == token.asBinder() && ((change.value.configSetMask and CONFIG_DENSITY) != 0)
- } ?: false
-}
-private fun createTaskInfo(id: Int) = RecentTaskInfo().apply {
- taskId = id
- token = WindowContainerToken(mock(IWindowContainerToken::class.java))
+ return this?.changes?.any { change ->
+ change.key == token.asBinder() && ((change.value.configSetMask and CONFIG_DENSITY) != 0)
+ } ?: false
}
+
+private fun createTaskInfo(id: Int) =
+ RecentTaskInfo().apply {
+ taskId = id
+ token = WindowContainerToken(mock(IWindowContainerToken::class.java))
+ }
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 2ade3fba9b08..bbf523bc40d2 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
@@ -18,6 +18,8 @@ import androidx.test.filters.SmallTest
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
+import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
@@ -48,6 +50,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
@Mock private lateinit var transitions: Transitions
@Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
@Mock private lateinit var splitScreenController: SplitScreenController
+ @Mock private lateinit var dragAnimator: MoveToDesktopAnimator
private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() }
@@ -68,7 +71,6 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
@Test
fun startDragToDesktop_animateDragWhenReady() {
val task = createTask()
- val dragAnimator = mock<MoveToDesktopAnimator>()
// Simulate transition is started.
val transition = startDragToDesktopTransition(task, dragAnimator)
@@ -90,36 +92,36 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
@Test
fun startDragToDesktop_cancelledBeforeReady_startCancelTransition() {
- val task = createTask()
- val dragAnimator = mock<MoveToDesktopAnimator>()
- // Simulate transition is started and is ready to animate.
- val transition = startDragToDesktopTransition(task, dragAnimator)
-
- handler.cancelDragToDesktopTransition()
+ performEarlyCancel(DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL)
+ verify(transitions)
+ .startTransition(eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), eq(handler))
+ }
- handler.startAnimation(
- transition = transition,
- info =
- createTransitionInfo(
- type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
- draggedTask = task
- ),
- startTransaction = mock(),
- finishTransaction = mock(),
- finishCallback = {}
+ @Test
+ fun startDragToDesktop_cancelledBeforeReady_verifySplitLeftCancel() {
+ performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT)
+ verify(splitScreenController).requestEnterSplitSelect(
+ any(),
+ any(),
+ eq(SPLIT_POSITION_TOP_OR_LEFT),
+ any()
)
+ }
- // Don't even animate the "drag" since it was already cancelled.
- verify(dragAnimator, never()).startAnimation()
- // Instead, start the cancel transition.
- verify(transitions)
- .startTransition(eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), eq(handler))
+ @Test
+ fun startDragToDesktop_cancelledBeforeReady_verifySplitRightCancel() {
+ performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT)
+ verify(splitScreenController).requestEnterSplitSelect(
+ any(),
+ any(),
+ eq(SPLIT_POSITION_BOTTOM_OR_RIGHT),
+ any()
+ )
}
@Test
fun startDragToDesktop_aborted_finishDropped() {
val task = createTask()
- val dragAnimator = mock<MoveToDesktopAnimator>()
// Simulate transition is started.
val transition = startDragToDesktopTransition(task, dragAnimator)
// But the transition was aborted.
@@ -137,14 +139,15 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
@Test
fun startDragToDesktop_aborted_cancelDropped() {
val task = createTask()
- val dragAnimator = mock<MoveToDesktopAnimator>()
// Simulate transition is started.
val transition = startDragToDesktopTransition(task, dragAnimator)
// But the transition was aborted.
handler.onTransitionConsumed(transition, aborted = true, mock())
// Attempt to finish the failed drag start.
- handler.cancelDragToDesktopTransition()
+ handler.cancelDragToDesktopTransition(
+ DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL
+ )
// Should not be attempted and state should be reset.
assertFalse(handler.inProgress)
@@ -153,7 +156,6 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
@Test
fun startDragToDesktop_anotherTransitionInProgress_startDropped() {
val task = createTask()
- val dragAnimator = mock<MoveToDesktopAnimator>()
// Simulate attempt to start two drag to desktop transitions.
startDragToDesktopTransition(task, dragAnimator)
@@ -169,39 +171,63 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
@Test
fun cancelDragToDesktop_startWasReady_cancel() {
- val task = createTask()
- val dragAnimator = mock<MoveToDesktopAnimator>()
- whenever(dragAnimator.position).thenReturn(PointF())
- // Simulate transition is started and is ready to animate.
- val transition = startDragToDesktopTransition(task, dragAnimator)
- handler.startAnimation(
- transition = transition,
- info =
- createTransitionInfo(
- type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
- draggedTask = task
- ),
- startTransaction = mock(),
- finishTransaction = mock(),
- finishCallback = {}
- )
+ startDrag()
// Then user cancelled after it had already started.
- handler.cancelDragToDesktopTransition()
+ handler.cancelDragToDesktopTransition(
+ DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL
+ )
// Cancel animation should run since it had already started.
verify(dragAnimator).cancelAnimator()
}
@Test
+ fun cancelDragToDesktop_splitLeftCancelType_splitRequested() {
+ startDrag()
+
+ // Then user cancelled it, requesting split.
+ handler.cancelDragToDesktopTransition(
+ DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT
+ )
+
+ // Verify the request went through split controller.
+ verify(splitScreenController).requestEnterSplitSelect(
+ any(),
+ any(),
+ eq(SPLIT_POSITION_TOP_OR_LEFT),
+ any()
+ )
+ }
+
+ @Test
+ fun cancelDragToDesktop_splitRightCancelType_splitRequested() {
+ startDrag()
+
+ // Then user cancelled it, requesting split.
+ handler.cancelDragToDesktopTransition(
+ DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT
+ )
+
+ // Verify the request went through split controller.
+ verify(splitScreenController).requestEnterSplitSelect(
+ any(),
+ any(),
+ eq(SPLIT_POSITION_BOTTOM_OR_RIGHT),
+ any()
+ )
+ }
+
+ @Test
fun cancelDragToDesktop_startWasNotReady_animateCancel() {
val task = createTask()
- val dragAnimator = mock<MoveToDesktopAnimator>()
// Simulate transition is started and is ready to animate.
startDragToDesktopTransition(task, dragAnimator)
// Then user cancelled before the transition was ready and animated.
- handler.cancelDragToDesktopTransition()
+ handler.cancelDragToDesktopTransition(
+ DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL
+ )
// No need to animate the cancel since the start animation couldn't even start.
verifyZeroInteractions(dragAnimator)
@@ -210,7 +236,9 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
@Test
fun cancelDragToDesktop_transitionNotInProgress_dropCancel() {
// Then cancel is called before the transition was started.
- handler.cancelDragToDesktopTransition()
+ handler.cancelDragToDesktopTransition(
+ DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL
+ )
// Verify cancel is dropped.
verify(transitions, never()).startTransition(
@@ -233,6 +261,24 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
)
}
+ private fun startDrag() {
+ val task = createTask()
+ whenever(dragAnimator.position).thenReturn(PointF())
+ // Simulate transition is started and is ready to animate.
+ val transition = startDragToDesktopTransition(task, dragAnimator)
+ handler.startAnimation(
+ transition = transition,
+ info =
+ createTransitionInfo(
+ type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
+ draggedTask = task
+ ),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+ }
+
private fun startDragToDesktopTransition(
task: RunningTaskInfo,
dragAnimator: MoveToDesktopAnimator
@@ -250,6 +296,29 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
return token
}
+ private fun performEarlyCancel(cancelState: DragToDesktopTransitionHandler.CancelState) {
+ val task = createTask()
+ // Simulate transition is started and is ready to animate.
+ val transition = startDragToDesktopTransition(task, dragAnimator)
+
+ handler.cancelDragToDesktopTransition(cancelState)
+
+ handler.startAnimation(
+ transition = transition,
+ info =
+ createTransitionInfo(
+ type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
+ draggedTask = task
+ ),
+ startTransaction = mock(),
+ finishTransaction = mock(),
+ finishCallback = {}
+ )
+
+ // Don't even animate the "drag" since it was already cancelled.
+ verify(dragAnimator, never()).startAnimation()
+ }
+
private fun createTask(
@WindowingMode windowingMode: Int = WINDOWING_MODE_FULLSCREEN,
isHome: Boolean = false,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
index 0d0a08cb0ffb..b2467e9a62cf 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java
@@ -21,6 +21,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread;
+import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN;
+
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -45,6 +47,7 @@ import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
import com.android.wm.shell.transition.Transitions;
import org.junit.Before;
@@ -97,18 +100,18 @@ public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase {
@Test
public void testTransitExitDesktopModeAnimation() throws Throwable {
- final int transitionType = Transitions.TRANSIT_EXIT_DESKTOP_MODE;
+ final int transitionType = TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN;
final int taskId = 1;
WindowContainerTransaction wct = new WindowContainerTransaction();
doReturn(mToken).when(mTransitions)
.startTransition(transitionType, wct, mExitDesktopTaskTransitionHandler);
- mExitDesktopTaskTransitionHandler.startTransition(transitionType, wct, mPoint,
- null);
+ mExitDesktopTaskTransitionHandler.startTransition(DesktopModeTransitionSource.UNKNOWN,
+ wct, mPoint, null);
TransitionInfo.Change change =
createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FULLSCREEN);
- TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_EXIT_DESKTOP_MODE, change);
+ TransitionInfo info = createTransitionInfo(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN, change);
ArrayList<Exception> exceptions = new ArrayList<>();
runOnUiThread(() -> {
try {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
index 5880ffb0dce2..72950a8dc139 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipAnimationControllerTest.java
@@ -88,8 +88,11 @@ public class PipAnimationControllerTest extends ShellTestCase {
@Test
public void getAnimator_withBounds_returnBoundsAnimator() {
+ final Rect baseValue = new Rect(0, 0, 100, 100);
+ final Rect startValue = new Rect(0, 0, 100, 100);
+ final Rect endValue1 = new Rect(100, 100, 200, 200);
final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
- .getAnimator(mTaskInfo, mLeash, new Rect(), new Rect(), new Rect(), null,
+ .getAnimator(mTaskInfo, mLeash, baseValue, startValue, endValue1, null,
TRANSITION_DIRECTION_TO_PIP, 0, ROTATION_0);
assertEquals("Expect ANIM_TYPE_BOUNDS animation",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java
index 974539f23b80..aa2d6f09508f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipGravityTest.java
@@ -241,16 +241,16 @@ public class TvPipGravityTest extends ShellTestCase {
@Test
public void updateGravity_move_expanded_valid() {
- mTvPipBoundsState.setTvPipExpanded(true);
-
// Vertical expanded PiP.
mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+ mTvPipBoundsState.setTvPipExpanded(true);
mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.CENTER_VERTICAL | Gravity.LEFT, true);
moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, true);
// Horizontal expanded PiP.
mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+ mTvPipBoundsState.setTvPipExpanded(true);
mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.TOP | Gravity.CENTER_HORIZONTAL, true);
moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, true);
@@ -281,10 +281,9 @@ public class TvPipGravityTest extends ShellTestCase {
@Test
public void updateGravity_move_expanded_invalid() {
- mTvPipBoundsState.setTvPipExpanded(true);
-
// Vertical expanded PiP.
mTvPipBoundsState.setDesiredTvExpandedAspectRatio(VERTICAL_EXPANDED_ASPECT_RATIO, true);
+ mTvPipBoundsState.setTvPipExpanded(true);
mTvPipBoundsState.setTvPipGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
moveAndCheckGravity(KEYCODE_DPAD_RIGHT, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false);
moveAndCheckGravity(KEYCODE_DPAD_UP, Gravity.CENTER_VERTICAL | Gravity.RIGHT, false);
@@ -297,6 +296,7 @@ public class TvPipGravityTest extends ShellTestCase {
// Horizontal expanded PiP.
mTvPipBoundsState.setDesiredTvExpandedAspectRatio(HORIZONTAL_EXPANDED_ASPECT_RATIO, true);
+ mTvPipBoundsState.setTvPipExpanded(true);
mTvPipBoundsState.setTvPipGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
moveAndCheckGravity(KEYCODE_DPAD_DOWN, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false);
moveAndCheckGravity(KEYCODE_DPAD_LEFT, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, false);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 56c4ceacc8ab..5c5a1a26f5d2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -113,6 +113,8 @@ public class RecentTasksControllerTest extends ShellTestCase {
private DisplayInsetsController mDisplayInsetsController;
@Mock
private IRecentTasksListener mRecentTasksListener;
+ @Mock
+ private TaskStackTransitionObserver mTaskStackTransitionObserver;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -139,7 +141,8 @@ public class RecentTasksControllerTest extends ShellTestCase {
mDisplayInsetsController, mMainExecutor));
mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit,
mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager,
- Optional.of(mDesktopModeTaskRepository), mMainExecutor);
+ Optional.of(mDesktopModeTaskRepository), mTaskStackTransitionObserver,
+ mMainExecutor);
mRecentTasksController = spy(mRecentTasksControllerReal);
mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController),
@@ -396,7 +399,7 @@ public class RecentTasksControllerTest extends ShellTestCase {
}
@Test
- public void testGetRecentTasks_proto2Enabled_ignoresMinimizedFreeformTasks() {
+ public void testGetRecentTasks_proto2Enabled_includesMinimizedFreeformTasks() {
ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
@@ -412,8 +415,7 @@ public class RecentTasksControllerTest extends ShellTestCase {
ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
- // 2 freeform tasks should be grouped into one, 1 task should be skipped, 3 total recents
- // entries
+ // 3 freeform tasks should be grouped into one, 2 single tasks, 3 total recents entries
assertEquals(3, recentTasks.size());
GroupedRecentTaskInfo freeformGroup = recentTasks.get(0);
GroupedRecentTaskInfo singleGroup1 = recentTasks.get(1);
@@ -425,9 +427,10 @@ public class RecentTasksControllerTest extends ShellTestCase {
assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup2.getType());
// Check freeform group entries
- assertEquals(2, freeformGroup.getTaskInfoList().size());
+ assertEquals(3, freeformGroup.getTaskInfoList().size());
assertEquals(t1, freeformGroup.getTaskInfoList().get(0));
- assertEquals(t5, freeformGroup.getTaskInfoList().get(1));
+ assertEquals(t3, freeformGroup.getTaskInfoList().get(1));
+ assertEquals(t5, freeformGroup.getTaskInfoList().get(2));
// Check single entries
assertEquals(t2, singleGroup1.getTaskInfo1());
@@ -557,6 +560,30 @@ public class RecentTasksControllerTest extends ShellTestCase {
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+ public void onTaskMovedToFront_TaskStackObserverEnabled_triggersOnTaskMovedToFront()
+ throws Exception {
+ mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+ ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+
+ mRecentTasksControllerReal.onTaskMovedToFrontThroughTransition(taskInfo);
+
+ verify(mRecentTasksListener).onTaskMovedToFront(taskInfo);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+ public void onTaskMovedToFront_TaskStackObserverEnabled_doesNotTriggersOnTaskMovedToFront()
+ throws Exception {
+ mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+ ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+
+ mRecentTasksControllerReal.onTaskMovedToFront(taskInfo);
+
+ verify(mRecentTasksListener, never()).onTaskMovedToFront(any());
+ }
+
+ @Test
public void getNullSplitBoundsNonSplitTask() {
SplitBounds sb = mRecentTasksController.getSplitBoundsForTaskId(3);
assertNull("splitBounds should be null for non-split task", sb);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
new file mode 100644
index 000000000000..f9599702e763
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
@@ -0,0 +1,217 @@
+/*
+ * 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.wm.shell.recents
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration
+import android.os.IBinder
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.IWindowContainerToken
+import android.window.TransitionInfo
+import android.window.WindowContainerToken
+import androidx.test.filters.SmallTest
+import com.android.window.flags.Flags
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.TransitionInfoBuilder
+import com.android.wm.shell.transition.Transitions
+import com.google.common.truth.Truth.assertThat
+import dagger.Lazy
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.same
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+
+/**
+ * Test class for {@link TaskStackTransitionObserver}
+ *
+ * Usage: atest WMShellUnitTests:TaskStackTransitionObserverTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class TaskStackTransitionObserverTest {
+
+ @JvmField @Rule val setFlagsRule = SetFlagsRule()
+
+ @Mock private lateinit var shellInit: ShellInit
+ @Mock lateinit var testExecutor: ShellExecutor
+ @Mock private lateinit var transitionsLazy: Lazy<Transitions>
+ @Mock private lateinit var transitions: Transitions
+ @Mock private lateinit var mockTransitionBinder: IBinder
+
+ private lateinit var transitionObserver: TaskStackTransitionObserver
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ shellInit = Mockito.spy(ShellInit(testExecutor))
+ whenever(transitionsLazy.get()).thenReturn(transitions)
+ transitionObserver = TaskStackTransitionObserver(transitionsLazy, shellInit)
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+ verify(shellInit)
+ .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
+ initRunnableCaptor.value.run()
+ } else {
+ transitionObserver.onInit()
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+ fun testRegistersObserverAtInit() {
+ verify(transitions).registerObserver(same(transitionObserver))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+ fun taskCreated_freeformWindow_listenerNotified() {
+ val listener = TestListener()
+ val executor = TestShellExecutor()
+ transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+ val change =
+ createChange(
+ WindowManager.TRANSIT_OPEN,
+ createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+ )
+ val transitionInfo =
+ TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ callOnTransitionFinished()
+ executor.flushAll()
+
+ assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId)
+ assertThat(listener.taskInfoToBeNotified.windowingMode)
+ .isEqualTo(change.taskInfo?.windowingMode)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+ fun taskCreated_fullscreenWindow_listenerNotNotified() {
+ val listener = TestListener()
+ val executor = TestShellExecutor()
+ transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+ val change =
+ createChange(
+ WindowManager.TRANSIT_OPEN,
+ createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+ )
+ val transitionInfo =
+ TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ callOnTransitionFinished()
+ executor.flushAll()
+
+ assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(0)
+ assertThat(listener.taskInfoToBeNotified.windowingMode)
+ .isEqualTo(WindowConfiguration.WINDOWING_MODE_UNDEFINED)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+ fun taskCreated_freeformWindowOnTopOfFreeform_listenerNotified() {
+ val listener = TestListener()
+ val executor = TestShellExecutor()
+ transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+ val freeformOpenChange =
+ createChange(
+ WindowManager.TRANSIT_OPEN,
+ createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+ )
+ val freeformReorderChange =
+ createChange(
+ WindowManager.TRANSIT_TO_BACK,
+ createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+ )
+ val transitionInfo =
+ TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0)
+ .addChange(freeformOpenChange)
+ .addChange(freeformReorderChange)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+ callOnTransitionFinished()
+ executor.flushAll()
+
+ assertThat(listener.taskInfoToBeNotified.taskId)
+ .isEqualTo(freeformOpenChange.taskInfo?.taskId)
+ assertThat(listener.taskInfoToBeNotified.windowingMode)
+ .isEqualTo(freeformOpenChange.taskInfo?.windowingMode)
+ }
+
+ class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener {
+ var taskInfoToBeNotified = ActivityManager.RunningTaskInfo()
+
+ override fun onTaskMovedToFrontThroughTransition(
+ taskInfo: ActivityManager.RunningTaskInfo
+ ) {
+ taskInfoToBeNotified = taskInfo
+ }
+ }
+
+ /** Simulate calling the onTransitionReady() method */
+ private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
+ val startT = Mockito.mock(SurfaceControl.Transaction::class.java)
+ val finishT = Mockito.mock(SurfaceControl.Transaction::class.java)
+
+ transitionObserver.onTransitionReady(mockTransitionBinder, transitionInfo, startT, finishT)
+ }
+
+ /** Simulate calling the onTransitionFinished() method */
+ private fun callOnTransitionFinished() {
+ transitionObserver.onTransitionFinished(mockTransitionBinder, false)
+ }
+
+ companion object {
+ fun createTaskInfo(taskId: Int, windowingMode: Int): ActivityManager.RunningTaskInfo {
+ val taskInfo = ActivityManager.RunningTaskInfo()
+ taskInfo.taskId = taskId
+ taskInfo.configuration.windowConfiguration.windowingMode = windowingMode
+
+ return taskInfo
+ }
+
+ fun createChange(
+ mode: Int,
+ taskInfo: ActivityManager.RunningTaskInfo
+ ): TransitionInfo.Change {
+ val change =
+ TransitionInfo.Change(
+ WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)),
+ Mockito.mock(SurfaceControl::class.java)
+ )
+ change.mode = mode
+ change.taskInfo = taskInfo
+ return change
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index 964d86e8bd35..69a61eadf61d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -1192,7 +1192,8 @@ public class ShellTransitionTests extends ShellTestCase {
mMainHandler, mAnimExecutor, mock(HomeTransitionObserver.class));
final RecentsTransitionHandler recentsHandler =
new RecentsTransitionHandler(shellInit, transitions,
- mock(RecentTasksController.class), mock(HomeTransitionObserver.class));
+ mock(RecentTasksController.class), mock(HomeTransitionObserver.class),
+ () -> mock(SurfaceControl.Transaction.class));
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
shellInit.init();
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 9c1dc22bcef2..ca1e3f173e24 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
@@ -22,7 +22,9 @@ import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
+import android.content.ComponentName
import android.content.Context
+import android.content.pm.ActivityInfo
import android.graphics.Rect
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
@@ -341,7 +343,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
}
@Test
- fun testDescorationIsNotCreatedForTopTranslucentActivities() {
+ fun testDecorationIsNotCreatedForTopTranslucentActivities() {
setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply {
isTopActivityTransparent = true
@@ -354,6 +356,22 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
}
@Test
+ fun testDecorationIsNotCreatedForSystemUIActivities() {
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+
+ // Set task as systemUI package
+ val systemUIPackageName = context.resources.getString(
+ com.android.internal.R.string.config_systemUi)
+ val baseComponent = ComponentName(systemUIPackageName, /* class */ "")
+ task.baseActivity = baseComponent
+
+ onTaskOpening(task)
+
+ verify(mockDesktopModeWindowDecorFactory, never())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ }
+
+ @Test
@RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING)
fun testRelayoutRunsWhenStatusBarsInsetsSourceVisibilityChanges() {
val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true)
@@ -522,7 +540,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
displayId: Int = DEFAULT_DISPLAY,
@WindowConfiguration.WindowingMode windowingMode: Int,
activityType: Int = ACTIVITY_TYPE_STANDARD,
- focused: Boolean = true
+ focused: Boolean = true,
+ activityInfo: ActivityInfo = ActivityInfo()
): RunningTaskInfo {
return TestRunningTaskInfoBuilder()
.setDisplayId(displayId)
@@ -530,6 +549,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
.setVisible(true)
.setActivityType(activityType)
.build().apply {
+ topActivityInfo = activityInfo
isFocused = focused
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index a731e5394bdf..46c158908226 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -22,11 +22,16 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.wm.shell.MockSurfaceControlHelper.createMockSurfaceControlTransaction;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -35,7 +40,7 @@ import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
-import android.content.res.Configuration;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.os.Handler;
@@ -45,15 +50,22 @@ import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
+import android.view.AttachedSurfaceControl;
import android.view.Choreographer;
import android.view.Display;
+import android.view.GestureDetector;
+import android.view.InsetsState;
+import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
+import android.view.View;
import android.view.WindowManager;
import android.window.WindowContainerTransaction;
+import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.internal.R;
import com.android.window.flags.Flags;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
@@ -62,14 +74,18 @@ import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.shared.DesktopModeStatus;
import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams;
+import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import org.mockito.quality.Strictness;
import java.util.function.Supplier;
@@ -106,18 +122,26 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
@Mock
private Supplier<SurfaceControl.Transaction> mMockTransactionSupplier;
@Mock
- private SurfaceControl.Transaction mMockTransaction;
- @Mock
private SurfaceControl mMockSurfaceControl;
@Mock
private SurfaceControlViewHost mMockSurfaceControlViewHost;
@Mock
+ private AttachedSurfaceControl mMockRootSurfaceControl;
+ @Mock
private WindowDecoration.SurfaceControlViewHostFactory mMockSurfaceControlViewHostFactory;
@Mock
private TypedArray mMockRoundedCornersRadiusArray;
- private final Configuration mConfiguration = new Configuration();
+ @Mock
+ private TestTouchEventListener mMockTouchEventListener;
+ @Mock
+ private DesktopModeWindowDecoration.ExclusionRegionListener mMockExclusionRegionListener;
+ @Mock
+ private PackageManager mMockPackageManager;
+ private final InsetsState mInsetsState = new InsetsState();
+ private SurfaceControl.Transaction mMockTransaction;
+ private StaticMockitoSession mMockitoSession;
private TestableContext mTestableContext;
/** Set up run before test class. */
@@ -131,11 +155,29 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
@Before
public void setUp() {
+ mMockitoSession = mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .spyStatic(DesktopModeStatus.class)
+ .startMocking();
+ when(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(false);
doReturn(mMockSurfaceControlViewHost).when(mMockSurfaceControlViewHostFactory).create(
any(), any(), any());
+ when(mMockSurfaceControlViewHost.getRootSurfaceControl())
+ .thenReturn(mMockRootSurfaceControl);
+ mMockTransaction = createMockSurfaceControlTransaction();
doReturn(mMockTransaction).when(mMockTransactionSupplier).get();
mTestableContext = new TestableContext(mContext);
mTestableContext.ensureTestableResources();
+ mContext.setMockPackageManager(mMockPackageManager);
+ when(mMockPackageManager.getApplicationLabel(any())).thenReturn("applicationLabel");
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY);
+ doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt());
+ }
+
+ @After
+ public void tearDown() {
+ mMockitoSession.finishMocking();
}
@Test
@@ -206,6 +248,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
@Test
@DisableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY)
public void updateRelayoutParams_appHeader_usesSystemDensity() {
+ when(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(true);
final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources()
.getConfiguration().densityDpi;
final int customTaskDensity = systemDensity + 300;
@@ -323,6 +366,99 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
}
+ @Test
+ public void relayout_fullscreenTask_appliesTransactionImmediately() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+ spyWindowDecor.relayout(taskInfo);
+
+ verify(mMockTransaction).apply();
+ verify(mMockRootSurfaceControl, never()).applyTransactionOnDraw(any());
+ }
+
+ @Test
+ public void relayout_freeformTask_appliesTransactionOnDraw() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT)
+ taskInfo.isResizeable = false;
+
+ spyWindowDecor.relayout(taskInfo);
+
+ verify(mMockTransaction, never()).apply();
+ verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockTransaction);
+ }
+
+ @Test
+ public void relayout_fullscreenTask_doesNotCreateViewHostImmediately() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+ spyWindowDecor.relayout(taskInfo);
+
+ verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any());
+ }
+
+ @Test
+ public void relayout_fullscreenTask_postsViewHostCreation() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+
+ ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
+ spyWindowDecor.relayout(taskInfo);
+
+ verify(mMockHandler).post(runnableArgument.capture());
+ runnableArgument.getValue().run();
+ verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any());
+ }
+
+ @Test
+ public void relayout_freeformTask_createsViewHostImmediately() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ // Make non-resizable to avoid dealing with input-permissions (MONITOR_INPUT)
+ taskInfo.isResizeable = false;
+
+ spyWindowDecor.relayout(taskInfo);
+
+ verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any());
+ verify(mMockHandler, never()).post(any());
+ }
+
+ @Test
+ public void relayout_removesExistingHandlerCallback() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
+ spyWindowDecor.relayout(taskInfo);
+ verify(mMockHandler).post(runnableArgument.capture());
+
+ spyWindowDecor.relayout(taskInfo);
+
+ verify(mMockHandler).removeCallbacks(runnableArgument.getValue());
+ }
+
+ @Test
+ public void close_removesExistingHandlerCallback() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ final DesktopModeWindowDecoration spyWindowDecor = spy(createWindowDecoration(taskInfo));
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class);
+ spyWindowDecor.relayout(taskInfo);
+ verify(mMockHandler).post(runnableArgument.capture());
+
+ spyWindowDecor.close();
+
+ verify(mMockHandler).removeCallbacks(runnableArgument.getValue());
+ }
+
private void fillRoundedCornersResources(int fillValue) {
when(mMockRoundedCornersRadiusArray.getDimensionPixelSize(anyInt(), anyInt()))
.thenReturn(fillValue);
@@ -343,12 +479,16 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
private DesktopModeWindowDecoration createWindowDecoration(
ActivityManager.RunningTaskInfo taskInfo) {
- return new DesktopModeWindowDecoration(mContext, mMockDisplayController,
- mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl,
+ DesktopModeWindowDecoration windowDecor = new DesktopModeWindowDecoration(mContext,
+ mMockDisplayController, mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl,
mMockHandler, mMockChoreographer, mMockSyncQueue, mMockRootTaskDisplayAreaOrganizer,
SurfaceControl.Builder::new, mMockTransactionSupplier,
WindowContainerTransaction::new, SurfaceControl::new,
mMockSurfaceControlViewHostFactory);
+ windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener,
+ mMockTouchEventListener, mMockTouchEventListener);
+ windowDecor.setExclusionRegionListener(mMockExclusionRegionListener);
+ return windowDecor;
}
private ActivityManager.RunningTaskInfo createTaskInfo(boolean visible) {
@@ -373,4 +513,32 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
return (params.mInputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL)
!= 0;
}
+
+ private static class TestTouchEventListener extends GestureDetector.SimpleOnGestureListener
+ implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
+ View.OnGenericMotionListener, DragDetector.MotionEventHandler {
+
+ @Override
+ public void onClick(View v) {}
+
+ @Override
+ public boolean onGenericMotion(View v, MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onLongClick(View v) {
+ return false;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean handleMotionEvent(@Nullable View v, MotionEvent ev) {
+ return false;
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
index e6fabcfec58a..86aded76c0f3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
@@ -16,25 +16,34 @@
package com.android.wm.shell.windowdecor
import android.app.ActivityManager
+import android.content.Context
+import android.content.res.Resources
import android.graphics.PointF
import android.graphics.Rect
import android.os.IBinder
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
import android.view.Display
import android.window.WindowContainerToken
+import com.android.window.flags.Flags
+import com.android.wm.shell.R
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.shared.DesktopModeStatus
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
-import org.mockito.Mockito.`when` as whenever
import org.mockito.Mockito.any
+import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
/**
@@ -47,17 +56,32 @@ import org.mockito.MockitoAnnotations
class DragPositioningCallbackUtilityTest {
@Mock
private lateinit var mockWindowDecoration: WindowDecoration<*>
+
@Mock
private lateinit var taskToken: WindowContainerToken
+
@Mock
private lateinit var taskBinder: IBinder
+
@Mock
private lateinit var mockDisplayController: DisplayController
+
@Mock
private lateinit var mockDisplayLayout: DisplayLayout
+
@Mock
private lateinit var mockDisplay: Display
+ @Mock
+ private lateinit var mockContext: Context
+
+ @Mock
+ private lateinit var mockResources: Resources
+
+ @JvmField
+ @Rule
+ val setFlagsRule = SetFlagsRule()
+
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
@@ -69,16 +93,15 @@ class DragPositioningCallbackUtilityTest {
(i.arguments.first() as Rect).set(STABLE_BOUNDS)
}
- mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
- taskId = TASK_ID
- token = taskToken
- minWidth = MIN_WIDTH
- minHeight = MIN_HEIGHT
- defaultMinSize = DEFAULT_MIN
- displayId = DISPLAY_ID
- configuration.windowConfiguration.setBounds(STARTING_BOUNDS)
- }
+ initializeTaskInfo()
mockWindowDecoration.mDisplay = mockDisplay
+ mockWindowDecoration.mDecorWindowContext = mockContext
+ whenever(mockContext.getResources()).thenReturn(mockResources)
+ whenever(mockWindowDecoration.mDecorWindowContext.resources).thenReturn(mockResources)
+ whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_width))
+ .thenReturn(DESKTOP_MODE_MIN_WIDTH)
+ whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_height))
+ .thenReturn(DESKTOP_MODE_MIN_HEIGHT)
whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID }
}
@@ -93,8 +116,8 @@ class DragPositioningCallbackUtilityTest {
val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
- repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
- mockDisplayController, mockWindowDecoration)
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration)
assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
@@ -113,8 +136,8 @@ class DragPositioningCallbackUtilityTest {
val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
- repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
- mockDisplayController, mockWindowDecoration)
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration)
assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top + 5)
@@ -127,14 +150,14 @@ class DragPositioningCallbackUtilityTest {
val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat())
val repositionTaskBounds = Rect(STARTING_BOUNDS)
- // Resize to width of 95px and width of -5px with minimum of 10px
+ // Resize to width of 95px and height of -5px with minimum of 10px
val newX = STARTING_BOUNDS.right.toFloat() - 5
val newY = STARTING_BOUNDS.top.toFloat() + 105
val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
- repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
- mockDisplayController, mockWindowDecoration)
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration)
assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
@@ -153,8 +176,8 @@ class DragPositioningCallbackUtilityTest {
val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
- repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
- mockDisplayController, mockWindowDecoration)
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration)
assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top + 80)
assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 80)
@@ -172,8 +195,8 @@ class DragPositioningCallbackUtilityTest {
val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
- repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
- mockDisplayController, mockWindowDecoration)
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration)
assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right)
@@ -196,14 +219,13 @@ class DragPositioningCallbackUtilityTest {
assertThat(repositionTaskBounds.left).isEqualTo(validDragArea.left)
assertThat(repositionTaskBounds.top).isEqualTo(validDragArea.bottom)
assertThat(repositionTaskBounds.right)
- .isEqualTo(validDragArea.left + STARTING_BOUNDS.width())
+ .isEqualTo(validDragArea.left + STARTING_BOUNDS.width())
assertThat(repositionTaskBounds.bottom)
- .isEqualTo(validDragArea.bottom + STARTING_BOUNDS.height())
+ .isEqualTo(validDragArea.bottom + STARTING_BOUNDS.height())
}
@Test
fun testChangeBounds_toDisallowedBounds_freezesAtLimit() {
- var hasMoved = false
val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(),
STARTING_BOUNDS.bottom.toFloat())
val repositionTaskBounds = Rect(STARTING_BOUNDS)
@@ -212,32 +234,177 @@ class DragPositioningCallbackUtilityTest {
var newY = STARTING_BOUNDS.bottom.toFloat() + 10
var delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
assertTrue(DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
- repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
- mockDisplayController, mockWindowDecoration))
- hasMoved = true
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration))
// Resize width to 120px, height to disallowed area which should not result in a change.
newX += 10
newY = DISALLOWED_RESIZE_AREA.top.toFloat()
delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
assertTrue(DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
- repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta,
- mockDisplayController, mockWindowDecoration))
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration))
assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right + 20)
assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom + 10)
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+ fun taskMinWidthHeightUndefined_changeBoundsInDesktopModeLessThanMin_shouldNotChangeBounds() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(mockContext)).thenReturn(true)
+ initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1)
+ val startingPoint =
+ PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat())
+ val repositionTaskBounds = Rect(STARTING_BOUNDS)
+ // Shrink height and width to 1px. The default allowed width and height are defined in
+ // R.dimen.desktop_mode_minimum_window_width and R.dimen.desktop_mode_minimum_window_height
+ val newX = STARTING_BOUNDS.right.toFloat() - 99
+ val newY = STARTING_BOUNDS.bottom.toFloat() - 99
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration)
+ assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
+ assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
+ assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right)
+ assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+ fun taskMinWidthHeightUndefined_changeBoundsInDesktopModeAllowedSize_shouldChangeBounds() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(mockContext)).thenReturn(true)
+ initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1)
+ val startingPoint =
+ PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat())
+ val repositionTaskBounds = Rect(STARTING_BOUNDS)
+ // Shrink height and width to 20px. The default allowed width and height are defined in
+ // R.dimen.desktop_mode_minimum_window_width and R.dimen.desktop_mode_minimum_window_height
+ val newX = STARTING_BOUNDS.right.toFloat() - 80
+ val newY = STARTING_BOUNDS.bottom.toFloat() - 80
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration)
+ assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
+ assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
+ assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 80)
+ assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom - 80)
+ }
+
+ @Test
+ fun taskMinWidthHeightUndefined_changeBoundsLessThanDefaultMinSize_shouldNotChangeBounds() {
+ initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1)
+ val startingPoint =
+ PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat())
+ val repositionTaskBounds = Rect(STARTING_BOUNDS)
+ // Shrink height and width to 1px. The default allowed width and height are defined in the
+ // defaultMinSize of the TaskInfo.
+ val newX = STARTING_BOUNDS.right.toFloat() - 99
+ val newY = STARTING_BOUNDS.bottom.toFloat() - 99
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration)
+ assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
+ assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
+ assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right)
+ assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom)
+ }
+
+ @Test
+ fun taskMinWidthHeightUndefined_changeBoundsToAnAllowedSize_shouldChangeBounds() {
+ initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1)
+ val startingPoint =
+ PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat())
+ val repositionTaskBounds = Rect(STARTING_BOUNDS)
+ // Shrink height and width to 50px. The default allowed width and height are defined in the
+ // defaultMinSize of the TaskInfo.
+ val newX = STARTING_BOUNDS.right.toFloat() - 50
+ val newY = STARTING_BOUNDS.bottom.toFloat() - 50
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+ repositionTaskBounds, STARTING_BOUNDS, STABLE_BOUNDS, delta, mockDisplayController,
+ mockWindowDecoration)
+ assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
+ assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
+ assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right - 50)
+ assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom - 50)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+ fun testChangeBounds_windowSizeExceedsStableBounds_shouldBeAllowedToChangeBounds() {
+ val startingPoint =
+ PointF(OFF_CENTER_STARTING_BOUNDS.right.toFloat(),
+ OFF_CENTER_STARTING_BOUNDS.bottom.toFloat())
+ val repositionTaskBounds = Rect(OFF_CENTER_STARTING_BOUNDS)
+ // Increase height and width by STABLE_BOUNDS. Subtract by 5px so that it doesn't reach
+ // the disallowed drag area.
+ val offset = 5
+ val newX = STABLE_BOUNDS.right.toFloat() - offset
+ val newY = STABLE_BOUNDS.bottom.toFloat() - offset
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+ repositionTaskBounds, OFF_CENTER_STARTING_BOUNDS, STABLE_BOUNDS, delta,
+ mockDisplayController, mockWindowDecoration)
+ assertThat(repositionTaskBounds.width()).isGreaterThan(STABLE_BOUNDS.right)
+ assertThat(repositionTaskBounds.height()).isGreaterThan(STABLE_BOUNDS.bottom)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+ fun testChangeBoundsInDesktopMode_windowSizeExceedsStableBounds_shouldBeLimitedToDisplaySize() {
+ whenever(DesktopModeStatus.canEnterDesktopMode(mockContext)).thenReturn(true)
+ val startingPoint =
+ PointF(OFF_CENTER_STARTING_BOUNDS.right.toFloat(),
+ OFF_CENTER_STARTING_BOUNDS.bottom.toFloat())
+ val repositionTaskBounds = Rect(OFF_CENTER_STARTING_BOUNDS)
+ // Increase height and width by STABLE_BOUNDS. Subtract by 5px so that it doesn't reach
+ // the disallowed drag area.
+ val offset = 5
+ val newX = STABLE_BOUNDS.right.toFloat() - offset
+ val newY = STABLE_BOUNDS.bottom.toFloat() - offset
+ val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+ DragPositioningCallbackUtility.changeBounds(CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+ repositionTaskBounds, OFF_CENTER_STARTING_BOUNDS, STABLE_BOUNDS, delta,
+ mockDisplayController, mockWindowDecoration)
+ assertThat(repositionTaskBounds.width()).isLessThan(STABLE_BOUNDS.right)
+ assertThat(repositionTaskBounds.height()).isLessThan(STABLE_BOUNDS.bottom)
+ }
+
+ private fun initializeTaskInfo(taskMinWidth: Int = MIN_WIDTH, taskMinHeight: Int = MIN_HEIGHT) {
+ mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
+ taskId = TASK_ID
+ token = taskToken
+ minWidth = taskMinWidth
+ minHeight = taskMinHeight
+ defaultMinSize = DEFAULT_MIN
+ displayId = DISPLAY_ID
+ configuration.windowConfiguration.setBounds(STARTING_BOUNDS)
+ }
+ }
+
companion object {
private const val TASK_ID = 5
private const val MIN_WIDTH = 10
private const val MIN_HEIGHT = 10
+ private const val DESKTOP_MODE_MIN_WIDTH = 20
+ private const val DESKTOP_MODE_MIN_HEIGHT = 20
private const val DENSITY_DPI = 20
private const val DEFAULT_MIN = 40
private const val DISPLAY_ID = 1
private const val NAVBAR_HEIGHT = 50
private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600)
private val STARTING_BOUNDS = Rect(0, 0, 100, 100)
+ private val OFF_CENTER_STARTING_BOUNDS = Rect(-100, -100, 10, 10)
private val DISALLOWED_RESIZE_AREA = Rect(
DISPLAY_BOUNDS.left,
DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
index 54645083eca8..4dea5a75a0e8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
@@ -27,10 +27,9 @@ import static com.google.common.truth.Truth.assertThat;
import android.annotation.NonNull;
import android.graphics.Point;
import android.graphics.Region;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.testing.AndroidTestingRunner;
import android.util.Size;
@@ -74,7 +73,7 @@ public class DragResizeWindowGeometryTests {
TASK_SIZE.getHeight() + EDGE_RESIZE_THICKNESS / 2);
@Rule
- public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
/**
* Check that both groups of objects satisfy equals/hashcode within each group, and that each
@@ -144,10 +143,11 @@ public class DragResizeWindowGeometryTests {
/**
* Validate that with the flag enabled, the corner resize regions are the largest size, to
- * capture all eligible input regardless of source (touch or cursor).
+ * capture all eligible input regardless of source (touchscreen or cursor).
+ * <p>Note that capturing input does not necessarily mean that the event will be handled.
*/
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
public void testRegionUnion_edgeDragResizeEnabled_containsLargeCorners() {
Region region = new Region();
GEOMETRY.union(region);
@@ -164,7 +164,7 @@ public class DragResizeWindowGeometryTests {
* size.
*/
@Test
- @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
public void testRegionUnion_edgeDragResizeDisabled_containsFineCorners() {
Region region = new Region();
GEOMETRY.union(region);
@@ -176,74 +176,114 @@ public class DragResizeWindowGeometryTests {
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
public void testCalculateControlType_edgeDragResizeEnabled_edges() {
- // The input source (touch or cursor) shouldn't impact the edge resize size.
- validateCtrlTypeForEdges(/* isTouch= */ false);
- validateCtrlTypeForEdges(/* isTouch= */ true);
+ // The input source (touchscreen or cursor) shouldn't impact the edge resize size.
+ validateCtrlTypeForEdges(/* isTouchscreen= */ false, /* isEdgeResizePermitted= */ false);
+ validateCtrlTypeForEdges(/* isTouchscreen= */ true, /* isEdgeResizePermitted= */ false);
+ validateCtrlTypeForEdges(/* isTouchscreen= */ false, /* isEdgeResizePermitted= */ true);
+ validateCtrlTypeForEdges(/* isTouchscreen= */ true, /* isEdgeResizePermitted= */ true);
}
@Test
- @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
public void testCalculateControlType_edgeDragResizeDisabled_edges() {
- // Edge resizing is not supported when the flag is disabled.
- validateCtrlTypeForEdges(/* isTouch= */ false);
- validateCtrlTypeForEdges(/* isTouch= */ false);
+ // Edge resizing is not supported for touchscreen input when the flag is disabled.
+ validateCtrlTypeForEdges(/* isTouchscreen= */ false, /* isEdgeResizePermitted= */ true);
+ validateCtrlTypeForEdges(/* isTouchscreen= */ true, /* isEdgeResizePermitted= */ false);
}
- private void validateCtrlTypeForEdges(boolean isTouch) {
- assertThat(GEOMETRY.calculateCtrlType(isTouch, LEFT_EDGE_POINT.x,
- LEFT_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_LEFT);
- assertThat(GEOMETRY.calculateCtrlType(isTouch, TOP_EDGE_POINT.x,
- TOP_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_TOP);
- assertThat(GEOMETRY.calculateCtrlType(isTouch, RIGHT_EDGE_POINT.x,
- RIGHT_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_RIGHT);
- assertThat(GEOMETRY.calculateCtrlType(isTouch, BOTTOM_EDGE_POINT.x,
- BOTTOM_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_BOTTOM);
+ private void validateCtrlTypeForEdges(boolean isTouchscreen, boolean isEdgeResizePermitted) {
+ assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ LEFT_EDGE_POINT.x, LEFT_EDGE_POINT.y)).isEqualTo(
+ isEdgeResizePermitted ? CTRL_TYPE_LEFT : CTRL_TYPE_UNDEFINED);
+ assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ TOP_EDGE_POINT.x, TOP_EDGE_POINT.y)).isEqualTo(
+ isEdgeResizePermitted ? CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED);
+ assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ RIGHT_EDGE_POINT.x, RIGHT_EDGE_POINT.y)).isEqualTo(
+ isEdgeResizePermitted ? CTRL_TYPE_RIGHT : CTRL_TYPE_UNDEFINED);
+ assertThat(GEOMETRY.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ BOTTOM_EDGE_POINT.x, BOTTOM_EDGE_POINT.y)).isEqualTo(
+ isEdgeResizePermitted ? CTRL_TYPE_BOTTOM : CTRL_TYPE_UNDEFINED);
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
public void testCalculateControlType_edgeDragResizeEnabled_corners() {
final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2);
final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2);
// When the flag is enabled, points within fine corners should pass regardless of touch or
// not. Points outside fine corners should not pass when using a course input (non-touch).
- fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true);
- fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, true);
- fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, true);
- fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, false);
+ // Edge resizing permitted (events from stylus/cursor) should have no impact on corners.
+ fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */
+ true, /* isEdgeResizePermitted= */ true, true);
+ fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */
+ true, /* isEdgeResizePermitted= */ true, true);
+ fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */
+ true, /* isEdgeResizePermitted= */ false, true);
+ fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */
+ true, /* isEdgeResizePermitted= */ false, true);
+ fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ true, true);
+ fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ true, false);
+ fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ false, true);
+ fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ false, false);
// When the flag is enabled, points near the large corners should only pass when the point
// is within the corner for large touch inputs.
- largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true);
- largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true,
- false);
- largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false);
- largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false,
- false);
+ largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */
+ true, /* isEdgeResizePermitted= */ true, true);
+ largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */
+ true, /* isEdgeResizePermitted= */ true, false);
+ largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ true, false);
+ largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ true, false);
}
@Test
- @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
public void testCalculateControlType_edgeDragResizeDisabled_corners() {
final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2);
final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2);
- // When the flag is disabled, points within fine corners should pass only when touch.
- fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true);
- fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, false);
- fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false);
- fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, false);
+ // When the flag is disabled, points within fine corners should pass only from touchscreen.
+ // Edge resize permitted (indicating the event is from a cursor/stylus) should have no
+ // impact.
+ fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */
+ true, /* isEdgeResizePermitted= */ true, true);
+ fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */
+ true, /* isEdgeResizePermitted= */ true, false);
+ fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */
+ true, /* isEdgeResizePermitted= */ false, true);
+ fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */
+ true, /* isEdgeResizePermitted= */ false, false);
+
+ // Points within fine corners should never pass when not from touchscreen; expect edge
+ // resizing only.
+ fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ true, false);
+ fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ true, false);
+ fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ false, false);
+ fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ false, false);
// When the flag is disabled, points near the large corners should never pass.
- largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, false);
- largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true,
- false);
- largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false);
- largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false,
- false);
+ largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */
+ true, /* isEdgeResizePermitted= */ true, false);
+ largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */
+ true, /* isEdgeResizePermitted= */ true, false);
+ largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ true, false);
+ largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ true, false);
}
/**
@@ -306,19 +346,20 @@ public class DragResizeWindowGeometryTests {
* {@code @DragPositioningCallback.CtrlType}.
*/
public void validateCtrlTypeForInnerPoints(@NonNull DragResizeWindowGeometry geometry,
- boolean isTouch, boolean expectedWithinGeometry) {
- assertThat(geometry.calculateCtrlType(isTouch, mTopLeftPoint.x,
- mTopLeftPoint.y)).isEqualTo(
+ boolean isTouchscreen, boolean isEdgeResizePermitted,
+ boolean expectedWithinGeometry) {
+ assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ mTopLeftPoint.x, mTopLeftPoint.y)).isEqualTo(
expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED);
- assertThat(geometry.calculateCtrlType(isTouch, mTopRightPoint.x,
- mTopRightPoint.y)).isEqualTo(
+ assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ mTopRightPoint.x, mTopRightPoint.y)).isEqualTo(
expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED);
- assertThat(geometry.calculateCtrlType(isTouch, mBottomLeftPoint.x,
- mBottomLeftPoint.y)).isEqualTo(
+ assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ mBottomLeftPoint.x, mBottomLeftPoint.y)).isEqualTo(
expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM
: CTRL_TYPE_UNDEFINED);
- assertThat(geometry.calculateCtrlType(isTouch, mBottomRightPoint.x,
- mBottomRightPoint.y)).isEqualTo(
+ assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ mBottomRightPoint.x, mBottomRightPoint.y)).isEqualTo(
expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM
: CTRL_TYPE_UNDEFINED);
}
@@ -328,19 +369,20 @@ public class DragResizeWindowGeometryTests {
* {@code @DragPositioningCallback.CtrlType}.
*/
public void validateCtrlTypeForOutsidePoints(@NonNull DragResizeWindowGeometry geometry,
- boolean isTouch, boolean expectedWithinGeometry) {
- assertThat(geometry.calculateCtrlType(isTouch, mTopLeftPointOutside.x,
- mTopLeftPointOutside.y)).isEqualTo(
+ boolean isTouchscreen, boolean isEdgeResizePermitted,
+ boolean expectedWithinGeometry) {
+ assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ mTopLeftPointOutside.x, mTopLeftPointOutside.y)).isEqualTo(
expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED);
- assertThat(geometry.calculateCtrlType(isTouch, mTopRightPointOutside.x,
- mTopRightPointOutside.y)).isEqualTo(
+ assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ mTopRightPointOutside.x, mTopRightPointOutside.y)).isEqualTo(
expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED);
- assertThat(geometry.calculateCtrlType(isTouch, mBottomLeftPointOutside.x,
- mBottomLeftPointOutside.y)).isEqualTo(
+ assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ mBottomLeftPointOutside.x, mBottomLeftPointOutside.y)).isEqualTo(
expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM
: CTRL_TYPE_UNDEFINED);
- assertThat(geometry.calculateCtrlType(isTouch, mBottomRightPointOutside.x,
- mBottomRightPointOutside.y)).isEqualTo(
+ assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ mBottomRightPointOutside.x, mBottomRightPointOutside.y)).isEqualTo(
expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM
: CTRL_TYPE_UNDEFINED);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
index 9174556d091b..666750485ef2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt
@@ -2,6 +2,8 @@ package com.android.wm.shell.windowdecor
import android.app.ActivityManager
import android.app.WindowConfiguration
+import android.content.Context
+import android.content.res.Resources
import android.graphics.Point
import android.graphics.Rect
import android.os.IBinder
@@ -17,6 +19,7 @@ import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING
import androidx.test.filters.SmallTest
+import com.android.wm.shell.R
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.DisplayController
@@ -83,7 +86,10 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
private lateinit var mockTransaction: SurfaceControl.Transaction
@Mock
private lateinit var mockTransitionBinder: IBinder
-
+ @Mock
+ private lateinit var mockContext: Context
+ @Mock
+ private lateinit var mockResources: Resources
private lateinit var taskPositioner: FluidResizeTaskPositioner
@Before
@@ -119,6 +125,12 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
}
`when`(mockWindowDecoration.calculateValidDragArea()).thenReturn(VALID_DRAG_AREA)
mockWindowDecoration.mDisplay = mockDisplay
+ mockWindowDecoration.mDecorWindowContext = mockContext
+ whenever(mockWindowDecoration.mDecorWindowContext.resources).thenReturn(mockResources)
+ whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_width))
+ .thenReturn(DESKTOP_MODE_MIN_WIDTH)
+ whenever(mockResources.getDimensionPixelSize(R.dimen.desktop_mode_minimum_window_height))
+ .thenReturn(DESKTOP_MODE_MIN_HEIGHT)
whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID }
whenever(mockTransitions.startTransition(anyInt(), any(), any()))
.doReturn(mockTransitionBinder)
@@ -788,6 +800,8 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
private const val TASK_ID = 5
private const val MIN_WIDTH = 10
private const val MIN_HEIGHT = 10
+ private const val DESKTOP_MODE_MIN_WIDTH = 20
+ private const val DESKTOP_MODE_MIN_HEIGHT = 20
private const val DENSITY_DPI = 20
private const val DEFAULT_MIN = 40
private const val DISPLAY_ID = 1
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
new file mode 100644
index 000000000000..5582e0f46321
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt
@@ -0,0 +1,212 @@
+/*
+ * 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.wm.shell.windowdecor
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Rect
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.Display
+import android.view.LayoutInflater
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.window.flags.Flags
+import com.android.wm.shell.R
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
+import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
+import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
+import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED
+import com.android.wm.shell.splitscreen.SplitScreenController
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.whenever
+
+/**
+ * Tests for [HandleMenu].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:HandleMenuTest
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class HandleMenuTest : ShellTestCase() {
+ @JvmField
+ @Rule
+ val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ @Mock
+ private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration
+ @Mock
+ private lateinit var onClickListener: View.OnClickListener
+ @Mock
+ private lateinit var onTouchListener: View.OnTouchListener
+ @Mock
+ private lateinit var appIcon: Bitmap
+ @Mock
+ private lateinit var appName: CharSequence
+ @Mock
+ private lateinit var displayController: DisplayController
+ @Mock
+ private lateinit var splitScreenController: SplitScreenController
+ @Mock
+ private lateinit var displayLayout: DisplayLayout
+ @Mock
+ private lateinit var mockSurfaceControlViewHost: SurfaceControlViewHost
+
+ private lateinit var handleMenu: HandleMenu
+
+ @Before
+ fun setUp() {
+ val mockAdditionalViewHostViewContainer = AdditionalViewHostViewContainer(
+ mock(SurfaceControl::class.java),
+ mockSurfaceControlViewHost,
+ ) {
+ SurfaceControl.Transaction()
+ }
+ val menuView = LayoutInflater.from(context).inflate(
+ R.layout.desktop_mode_window_decor_handle_menu, null)
+ whenever(mockDesktopWindowDecoration.addWindow(
+ anyInt(), any(), any(), any(), anyInt(), anyInt(), anyInt(), anyInt())
+ ).thenReturn(mockAdditionalViewHostViewContainer)
+ whenever(mockAdditionalViewHostViewContainer.view).thenReturn(menuView)
+ whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
+ whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width())
+ whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height())
+ whenever(displayLayout.isLandscape).thenReturn(true)
+ mockDesktopWindowDecoration.mDecorWindowContext = context
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
+ fun testFullscreenMenuUsesSystemViewContainer() {
+ createTaskInfo(WINDOWING_MODE_FULLSCREEN, SPLIT_POSITION_UNDEFINED)
+ val handleMenu = createAndShowHandleMenu()
+ assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer)
+ // Verify menu is created at coordinates that, when added to WindowManager,
+ // show at the top-center of display.
+ assertTrue(handleMenu.mHandleMenuPosition.equals(16f, -512f))
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
+ fun testFreeformMenu_usesViewHostViewContainer() {
+ createTaskInfo(WINDOWING_MODE_FREEFORM, SPLIT_POSITION_UNDEFINED)
+ handleMenu = createAndShowHandleMenu()
+ assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalViewHostViewContainer)
+ // Verify menu is created near top-left of task.
+ assertTrue(handleMenu.mHandleMenuPosition.equals(12f, 8f))
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
+ fun testSplitLeftMenu_usesSystemViewContainer() {
+ createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_TOP_OR_LEFT)
+ handleMenu = createAndShowHandleMenu()
+ assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer)
+ // Verify menu is created at coordinates that, when added to WindowManager,
+ // show at the top of split left task.
+ assertTrue(handleMenu.mHandleMenuPosition.equals(-624f, -512f))
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_ADDITIONAL_WINDOWS_ABOVE_STATUS_BAR)
+ fun testSplitRightMenu_usesSystemViewContainer() {
+ createTaskInfo(WINDOWING_MODE_MULTI_WINDOW, SPLIT_POSITION_BOTTOM_OR_RIGHT)
+ handleMenu = createAndShowHandleMenu()
+ assertTrue(handleMenu.mHandleMenuViewContainer is AdditionalSystemViewContainer)
+ // Verify menu is created at coordinates that, when added to WindowManager,
+ // show at the top of split right task.
+ assertTrue(handleMenu.mHandleMenuPosition.equals(656f, -512f))
+ }
+
+ private fun createTaskInfo(windowingMode: Int, splitPosition: Int) {
+ val taskDescriptionBuilder = ActivityManager.TaskDescription.Builder()
+ .setBackgroundColor(Color.YELLOW)
+ val bounds = when (windowingMode) {
+ WINDOWING_MODE_FULLSCREEN -> DISPLAY_BOUNDS
+ WINDOWING_MODE_FREEFORM -> FREEFORM_BOUNDS
+ WINDOWING_MODE_MULTI_WINDOW -> {
+ if (splitPosition == SPLIT_POSITION_TOP_OR_LEFT) {
+ SPLIT_LEFT_BOUNDS
+ } else {
+ SPLIT_RIGHT_BOUNDS
+ }
+ }
+ else -> error("Unsupported windowing mode")
+ }
+ mockDesktopWindowDecoration.mTaskInfo = TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setTaskDescriptionBuilder(taskDescriptionBuilder)
+ .setWindowingMode(windowingMode)
+ .setBounds(bounds)
+ .setVisible(true)
+ .build()
+ // Calculate captionX similar to how WindowDecoration calculates it.
+ whenever(mockDesktopWindowDecoration.captionX).thenReturn(
+ (mockDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration
+ .bounds.width() - context.resources.getDimensionPixelSize(
+ R.dimen.desktop_mode_fullscreen_decor_caption_width)) / 2)
+ whenever(splitScreenController.getSplitPosition(any())).thenReturn(splitPosition)
+ whenever(splitScreenController.getStageBounds(any(), any())).thenAnswer {
+ (it.arguments.first() as Rect).set(SPLIT_LEFT_BOUNDS)
+ }
+ }
+
+ private fun createAndShowHandleMenu(): HandleMenu {
+ val layoutId = if (mockDesktopWindowDecoration.mTaskInfo.isFreeform) {
+ R.layout.desktop_mode_app_header
+ } else {
+ R.layout.desktop_mode_app_header
+ }
+ val handleMenu = HandleMenu(mockDesktopWindowDecoration, layoutId,
+ onClickListener, onTouchListener, appIcon, appName, displayController,
+ splitScreenController, true /* shouldShowWindowingPill */,
+ 50 /* captionHeight */ )
+ handleMenu.show()
+ return handleMenu
+ }
+
+ companion object {
+ private val DISPLAY_BOUNDS = Rect(0, 0, 2560, 1600)
+ private val FREEFORM_BOUNDS = Rect(500, 500, 2000, 1200)
+ private val SPLIT_LEFT_BOUNDS = Rect(0, 0, 1280, 1600)
+ private val SPLIT_RIGHT_BOUNDS = Rect(1280, 0, 2560, 1600)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
index 5da57c50e6c1..a07be79579eb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
@@ -150,7 +150,7 @@ class ResizeVeilTest : ShellTestCase() {
fun showVeil() {
val veil = createResizeVeil()
- veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), false /* fadeIn */)
+ veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */)
verify(mockTransaction).show(mockResizeVeilSurface)
verify(mockTransaction).show(mockBackgroundSurface)
@@ -162,7 +162,7 @@ class ResizeVeilTest : ShellTestCase() {
fun showVeil_displayUnavailable_doesNotShow() {
val veil = createResizeVeil(withDisplayAvailable = false)
- veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), false /* fadeIn */)
+ veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */)
verify(mockTransaction, never()).show(mockResizeVeilSurface)
verify(mockTransaction, never()).show(mockBackgroundSurface)
@@ -174,8 +174,8 @@ class ResizeVeilTest : ShellTestCase() {
fun showVeil_alreadyVisible_doesNotShowAgain() {
val veil = createResizeVeil()
- veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), false /* fadeIn */)
- veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), false /* fadeIn */)
+ veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */)
+ veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */)
verify(mockTransaction, times(1)).show(mockResizeVeilSurface)
verify(mockTransaction, times(1)).show(mockBackgroundSurface)
@@ -188,7 +188,13 @@ class ResizeVeilTest : ShellTestCase() {
val veil = createResizeVeil(parent = mock())
val newParent = mock<SurfaceControl>()
- veil.showVeil(mockTransaction, newParent, Rect(0, 0, 100, 100), false /* fadeIn */)
+ veil.showVeil(
+ mockTransaction,
+ newParent,
+ Rect(0, 0, 100, 100),
+ taskInfo,
+ false /* fadeIn */
+ )
verify(mockTransaction).reparent(mockResizeVeilSurface, newParent)
}
@@ -212,11 +218,11 @@ class ResizeVeilTest : ShellTestCase() {
context,
mockDisplayController,
mockAppIcon,
- taskInfo,
parent,
{ mockTransaction },
mockSurfaceControlBuilderFactory,
- mockSurfaceControlViewHostFactory
+ mockSurfaceControlViewHostFactory,
+ taskInfo
)
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
index 48ac1e5717aa..901ca90b573e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt
@@ -17,6 +17,8 @@ package com.android.wm.shell.windowdecor
import android.app.ActivityManager
import android.app.WindowConfiguration
+import android.content.Context
+import android.content.res.Resources
import android.graphics.Point
import android.graphics.Rect
import android.os.IBinder
@@ -98,6 +100,10 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
private lateinit var mockFinishCallback: TransitionFinishCallback
@Mock
private lateinit var mockTransitions: Transitions
+ @Mock
+ private lateinit var mockContext: Context
+ @Mock
+ private lateinit var mockResources: Resources
private lateinit var taskPositioner: VeiledResizeTaskPositioner
@@ -105,6 +111,9 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
fun setUp() {
MockitoAnnotations.initMocks(this)
+ mockDesktopWindowDecoration.mDisplay = mockDisplay
+ mockDesktopWindowDecoration.mDecorWindowContext = mockContext
+ whenever(mockContext.getResources()).thenReturn(mockResources)
whenever(taskToken.asBinder()).thenReturn(taskBinder)
whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout)
whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI)
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 48310810e8c9..f3603e1d9b46 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
@@ -32,6 +32,7 @@ import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.any;
@@ -76,6 +77,7 @@ import com.android.wm.shell.TestRunningTaskInfoBuilder;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.shared.DesktopModeStatus;
import com.android.wm.shell.tests.R;
+import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer;
import org.junit.Before;
import org.junit.Test;
@@ -371,7 +373,7 @@ public class WindowDecorationTests extends ShellTestCase {
}
@Test
- public void testAddWindow() {
+ public void testAddViewHostViewContainer() {
final Display defaultDisplay = mock(Display.class);
doReturn(defaultDisplay).when(mMockDisplayController)
.getDisplay(Display.DEFAULT_DISPLAY);
@@ -393,6 +395,7 @@ public class WindowDecorationTests extends ShellTestCase {
final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
.setDisplayId(Display.DEFAULT_DISPLAY)
.setTaskDescriptionBuilder(taskDescriptionBuilder)
+ .setWindowingMode(WINDOWING_MODE_FREEFORM)
.setBounds(TASK_BOUNDS)
.setPositionInParent(TASK_POSITION_IN_PARENT.x, TASK_POSITION_IN_PARENT.y)
.setVisible(true)
@@ -407,7 +410,7 @@ public class WindowDecorationTests extends ShellTestCase {
createMockSurfaceControlBuilder(additionalWindowSurface);
mMockSurfaceControlBuilders.add(additionalWindowSurfaceBuilder);
- WindowDecoration.AdditionalWindow additionalWindow = windowDecor.addTestWindow();
+ windowDecor.addTestViewContainer();
verify(additionalWindowSurfaceBuilder).setContainerLayer();
verify(additionalWindowSurfaceBuilder).setParent(decorContainerSurface);
@@ -421,12 +424,6 @@ public class WindowDecorationTests extends ShellTestCase {
verify(mMockSurfaceControlAddWindowT).show(additionalWindowSurface);
verify(mMockSurfaceControlViewHostFactory, Mockito.times(2))
.create(any(), eq(defaultDisplay), any());
- assertThat(additionalWindow.mWindowViewHost).isNotNull();
-
- additionalWindow.releaseView();
-
- assertThat(additionalWindow.mWindowViewHost).isNull();
- assertThat(additionalWindow.mWindowSurface).isNull();
}
@Test
@@ -832,6 +829,36 @@ public class WindowDecorationTests extends ShellTestCase {
eq(mMockTaskSurface), anyInt(), anyInt());
}
+ @Test
+ public void updateViewHost_applyTransactionOnDrawIsTrue_surfaceControlIsUpdated() {
+ final TestWindowDecoration windowDecor = createWindowDecoration(
+ new TestRunningTaskInfoBuilder().build());
+ mRelayoutParams.mApplyStartTransactionOnDraw = true;
+
+ windowDecor.updateViewHost(mRelayoutParams, mMockSurfaceControlStartT, mRelayoutResult);
+
+ verify(mMockRootSurfaceControl).applyTransactionOnDraw(mMockSurfaceControlStartT);
+ }
+
+ @Test
+ public void updateViewHost_nullDrawTransaction_applyTransactionOnDrawIsTrue_throwsException() {
+ final TestWindowDecoration windowDecor = createWindowDecoration(
+ new TestRunningTaskInfoBuilder().build());
+ mRelayoutParams.mApplyStartTransactionOnDraw = true;
+
+ assertThrows(IllegalArgumentException.class,
+ () -> windowDecor.updateViewHost(
+ mRelayoutParams, null /* onDrawTransaction */, mRelayoutResult));
+ }
+
+ @Test
+ public void updateViewHost_nullDrawTransaction_applyTransactionOnDrawIsFalse_doesNotThrow() {
+ final TestWindowDecoration windowDecor = createWindowDecoration(
+ new TestRunningTaskInfoBuilder().build());
+ mRelayoutParams.mApplyStartTransactionOnDraw = false;
+
+ windowDecor.updateViewHost(mRelayoutParams, null /* onDrawTransaction */, mRelayoutResult);
+ }
private TestWindowDecoration createWindowDecoration(ActivityManager.RunningTaskInfo taskInfo) {
return new TestWindowDecoration(mContext, mMockDisplayController, mMockShellTaskOrganizer,
@@ -905,16 +932,16 @@ public class WindowDecorationTests extends ShellTestCase {
mMockWindowContainerTransaction, mMockView, mRelayoutResult);
}
- private WindowDecoration.AdditionalWindow addTestWindow() {
+ private AdditionalViewContainer addTestViewContainer() {
final Resources resources = mDecorWindowContext.getResources();
- int width = loadDimensionPixelSize(resources, mCaptionMenuWidthId);
- int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
- String name = "Test Window";
- WindowDecoration.AdditionalWindow additionalWindow =
+ final int width = loadDimensionPixelSize(resources, mCaptionMenuWidthId);
+ final int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId);
+ final String name = "Test Window";
+ final AdditionalViewContainer additionalViewContainer =
addWindow(R.layout.desktop_mode_window_decor_handle_menu, name,
mMockSurfaceControlAddWindowT, mMockSurfaceSyncGroup, 0 /* x */,
0 /* y */, width, height);
- return additionalWindow;
+ return additionalViewContainer;
}
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt
new file mode 100644
index 000000000000..d3e996b12e1f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainerTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.wm.shell.windowdecor.additionalviewcontainer
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.LayoutInflater
+import android.view.View
+import android.view.WindowManager
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.R
+import com.android.wm.shell.ShellTestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/**
+ * Tests for [AdditionalSystemViewContainer].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:AdditionalSystemViewContainerTest
+ */
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class AdditionalSystemViewContainerTest : ShellTestCase() {
+ @Mock
+ private lateinit var mockView: View
+ @Mock
+ private lateinit var mockLayoutInflater: LayoutInflater
+ @Mock
+ private lateinit var mockContext: Context
+ @Mock
+ private lateinit var mockWindowManager: WindowManager
+ private lateinit var viewContainer: AdditionalSystemViewContainer
+
+ @Before
+ fun setUp() {
+ whenever(mockContext.getSystemService(WindowManager::class.java))
+ .thenReturn(mockWindowManager)
+ whenever(mockContext.getSystemService(Context
+ .LAYOUT_INFLATER_SERVICE)).thenReturn(mockLayoutInflater)
+ whenever(mockLayoutInflater.inflate(
+ R.layout.desktop_mode_window_decor_handle_menu, null)).thenReturn(mockView)
+ }
+
+ @Test
+ fun testReleaseView_ViewRemoved() {
+ viewContainer = AdditionalSystemViewContainer(
+ mockContext,
+ R.layout.desktop_mode_window_decor_handle_menu,
+ TASK_ID,
+ X,
+ Y,
+ WIDTH,
+ HEIGHT
+ )
+ verify(mockWindowManager).addView(eq(mockView), any())
+ viewContainer.releaseView()
+ verify(mockWindowManager).removeViewImmediate(mockView)
+ }
+
+ companion object {
+ private const val X = 500
+ private const val Y = 50
+ private const val WIDTH = 400
+ private const val HEIGHT = 600
+ private const val TASK_ID = 5
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainerTest.kt
new file mode 100644
index 000000000000..82d557a28f52
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalViewHostViewContainerTest.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.wm.shell.windowdecor.additionalviewcontainer
+
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import java.util.function.Supplier
+
+/**
+ * Tests for [AdditionalViewHostViewContainer].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:AdditionalViewHostViewContainerTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class AdditionalViewHostViewContainerTest : ShellTestCase() {
+ @Mock
+ private lateinit var mockTransactionSupplier: Supplier<SurfaceControl.Transaction>
+ @Mock
+ private lateinit var mockTransaction: SurfaceControl.Transaction
+ @Mock
+ private lateinit var mockSurface: SurfaceControl
+ @Mock
+ private lateinit var mockViewHost: SurfaceControlViewHost
+ private lateinit var viewContainer: AdditionalViewHostViewContainer
+
+ @Before
+ fun setUp() {
+ whenever(mockTransactionSupplier.get()).thenReturn(mockTransaction)
+ }
+
+ @Test
+ fun testReleaseView_ViewRemoved() {
+ viewContainer = AdditionalViewHostViewContainer(
+ mockSurface,
+ mockViewHost,
+ mockTransactionSupplier
+ )
+ viewContainer.releaseView()
+ verify(mockViewHost).release()
+ verify(mockTransaction).remove(mockSurface)
+ verify(mockTransaction).apply()
+ }
+}
diff --git a/libs/androidfw/AssetManager.cpp b/libs/androidfw/AssetManager.cpp
index 68befffecf2f..e6182454ad8a 100644
--- a/libs/androidfw/AssetManager.cpp
+++ b/libs/androidfw/AssetManager.cpp
@@ -926,8 +926,8 @@ Asset* AssetManager::openAssetFromZipLocked(const ZipFileRO* pZipFile,
//printf("USING Zip '%s'\n", pEntry->getFileName());
- if (!pZipFile->getEntryInfo(entry, &method, &uncompressedLen, NULL, NULL,
- NULL, NULL))
+ if (!pZipFile->getEntryInfo(entry, &method, &uncompressedLen, nullptr, nullptr,
+ nullptr, nullptr, nullptr))
{
ALOGW("getEntryInfo failed\n");
return NULL;
diff --git a/libs/androidfw/LocaleDataTables.cpp b/libs/androidfw/LocaleDataTables.cpp
index b68143d82090..94351182871a 100644
--- a/libs/androidfw/LocaleDataTables.cpp
+++ b/libs/androidfw/LocaleDataTables.cpp
@@ -2451,10 +2451,10 @@ const struct {
const char script[4];
const std::unordered_map<uint32_t, uint32_t>* map;
} SCRIPT_PARENTS[] = {
+ {{'L', 'a', 't', 'n'}, &LATN_PARENTS},
{{'A', 'r', 'a', 'b'}, &ARAB_PARENTS},
{{'D', 'e', 'v', 'a'}, &DEVA_PARENTS},
{{'H', 'a', 'n', 't'}, &HANT_PARENTS},
- {{'L', 'a', 't', 'n'}, &LATN_PARENTS},
{{'~', '~', '~', 'B'}, &___B_PARENTS},
};
diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp
index a3dd9833219e..de9991a8be5e 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -2650,8 +2650,9 @@ bool ResTable_config::isBetterThan(const ResTable_config& o,
return (mnc);
}
}
-
- if (isLocaleBetterThan(o, requested)) {
+ // Cheaper to check for the empty locales here before calling the function
+ // as we often can skip it completely.
+ if (requested->locale && (locale || o.locale) && isLocaleBetterThan(o, requested)) {
return true;
}
@@ -7237,27 +7238,11 @@ void DynamicRefTable::addMapping(uint8_t buildPackageId, uint8_t runtimePackageI
status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const {
uint32_t res = *resId;
- size_t packageId = Res_GETPACKAGE(res) + 1;
-
if (!Res_VALIDID(res)) {
// Cannot look up a null or invalid id, so no lookup needs to be done.
return NO_ERROR;
}
-
- const auto alias_it = std::lower_bound(mAliasId.begin(), mAliasId.end(), res,
- [](const AliasMap::value_type& pair, uint32_t val) { return pair.first < val; });
- if (alias_it != mAliasId.end() && alias_it->first == res) {
- // Rewrite the resource id to its alias resource id. Since the alias resource id is a
- // compile-time id, it still needs to be resolved further.
- res = alias_it->second;
- }
-
- if (packageId == SYS_PACKAGE_ID || (packageId == APP_PACKAGE_ID && !mAppAsLib)) {
- // No lookup needs to be done, app and framework package IDs are absolute.
- *resId = res;
- return NO_ERROR;
- }
-
+ const size_t packageId = Res_GETPACKAGE(res) + 1;
if (packageId == 0 || (packageId == APP_PACKAGE_ID && mAppAsLib)) {
// The package ID is 0x00. That means that a shared library is accessing
// its own local resource.
@@ -7267,6 +7252,24 @@ status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const {
*resId = (0xFFFFFF & (*resId)) | (((uint32_t) mAssignedPackageId) << 24);
return NO_ERROR;
}
+ // All aliases are coming from the framework, and usually have their own separate ID range,
+ // skipping the whole binary search is much more efficient than not finding anything.
+ if (packageId == SYS_PACKAGE_ID && !mAliasId.empty() &&
+ res >= mAliasId.front().first && res <= mAliasId.back().first) {
+ const auto alias_it = std::lower_bound(mAliasId.begin(), mAliasId.end(), res,
+ [](const AliasMap::value_type& pair,
+ uint32_t val) { return pair.first < val; });
+ if (alias_it != mAliasId.end() && alias_it->first == res) {
+ // Rewrite the resource id to its alias resource id. Since the alias resource id is a
+ // compile-time id, it still needs to be resolved further.
+ res = alias_it->second;
+ }
+ }
+ if (packageId == SYS_PACKAGE_ID || (packageId == APP_PACKAGE_ID && !mAppAsLib)) {
+ // No lookup needs to be done, app and framework package IDs are absolute.
+ *resId = res;
+ return NO_ERROR;
+ }
// Do a proper lookup.
uint8_t translatedId = mLookupTable[packageId];
diff --git a/libs/androidfw/ZipFileRO.cpp b/libs/androidfw/ZipFileRO.cpp
index 839c7b6fef37..10651cdaff33 100644
--- a/libs/androidfw/ZipFileRO.cpp
+++ b/libs/androidfw/ZipFileRO.cpp
@@ -119,14 +119,6 @@ ZipEntryRO ZipFileRO::findEntryByName(const char* entryName) const
* appear to be bogus.
*/
bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod,
- uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset,
- uint32_t* pModWhen, uint32_t* pCrc32) const
-{
- return getEntryInfo(entry, pMethod, pUncompLen, pCompLen, pOffset, pModWhen,
- pCrc32, nullptr);
-}
-
-bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod,
uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset,
uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const
{
diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp b/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp
index 829a39617012..a218a1ff1eb6 100644
--- a/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp
+++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp
@@ -52,10 +52,11 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
// Populate the DynamicRefTable with fuzzed data
populateDynamicRefTableWithFuzzedData(*dynamic_ref_table, fuzzedDataProvider);
+ std::vector<uint8_t> xmlData = fuzzedDataProvider.ConsumeRemainingBytes<uint8_t>();
+ // Make sure the object here outlives the vector it's set to, otherwise it will try
+ // accessing an already freed buffer and crash.
auto tree = android::ResXMLTree(std::move(dynamic_ref_table));
-
- std::vector<uint8_t> xmlData = fuzzedDataProvider.ConsumeRemainingBytes<uint8_t>();
if (tree.setTo(xmlData.data(), xmlData.size()) != android::NO_ERROR) {
return 0; // Exit early if unable to parse XML data
}
diff --git a/libs/androidfw/include/androidfw/ZipFileRO.h b/libs/androidfw/include/androidfw/ZipFileRO.h
index f7c5007c80d2..0f3f19c91ed1 100644
--- a/libs/androidfw/include/androidfw/ZipFileRO.h
+++ b/libs/androidfw/include/androidfw/ZipFileRO.h
@@ -147,10 +147,6 @@ public:
* Returns "false" if "entry" is bogus or if the data in the Zip file
* appears to be bad.
*/
- bool getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, uint32_t* pUncompLen,
- uint32_t* pCompLen, off64_t* pOffset, uint32_t* pModWhen,
- uint32_t* pCrc32) const;
-
bool getEntryInfo(ZipEntryRO entry, uint16_t* pMethod,
uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset,
uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const;
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 7c1c5b4e7e5f..341599e79662 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -115,6 +115,7 @@ cc_defaults {
"libharfbuzz_ng",
"libminikin",
"server_configurable_flags",
+ "libaconfig_storage_read_api_cc"
],
static_libs: [
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 325bdd63ab22..5d3bc89b40dd 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -39,6 +39,9 @@ constexpr bool clip_surfaceviews() {
constexpr bool hdr_10bit_plus() {
return false;
}
+constexpr bool initialize_gl_always() {
+ return false;
+}
} // namespace hwui_flags
#endif
@@ -257,5 +260,9 @@ bool Properties::isDrawingEnabled() {
return drawingEnabled == DrawingEnabled::On;
}
+bool Properties::initializeGlAlways() {
+ return base::GetBoolProperty(PROPERTY_INITIALIZE_GL_ALWAYS, hwui_flags::initialize_gl_always());
+}
+
} // namespace uirenderer
} // namespace android
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index c1510d96461f..d3176f6879d2 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -229,6 +229,11 @@ enum DebugLevel {
#define PROPERTY_8BIT_HDR_HEADROOM "debug.hwui.8bit_hdr_headroom"
+/**
+ * Whether to initialize GL even when HWUI is running Vulkan.
+ */
+#define PROPERTY_INITIALIZE_GL_ALWAYS "debug.hwui.initialize_gl_always"
+
///////////////////////////////////////////////////////////////////////////////
// Misc
///////////////////////////////////////////////////////////////////////////////
@@ -368,6 +373,8 @@ public:
static bool isDrawingEnabled();
static void setDrawingEnabled(bool enable);
+ static bool initializeGlAlways();
+
private:
static StretchEffectBehavior stretchEffectBehavior;
static ProfileType sProfileType;
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index 50f8b3929e1e..cd3ae5342f4e 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -90,3 +90,10 @@ flag {
description: "Add canvas#drawRegion API"
bug: "318612129"
}
+
+flag {
+ name: "initialize_gl_always"
+ namespace: "core_graphics"
+ description: "Initialize GL even when HWUI is set to use Vulkan. This improves app startup time for apps using GL."
+ bug: "335172671"
+}
diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp
index 6ace3967ecf3..15b2bac50c79 100644
--- a/libs/hwui/apex/jni_runtime.cpp
+++ b/libs/hwui/apex/jni_runtime.cpp
@@ -192,5 +192,14 @@ void zygote_preload_graphics() {
// Preload Vulkan driver if HWUI renders with Vulkan backend.
uint32_t apiVersion;
vkEnumerateInstanceVersion(&apiVersion);
+
+ if (Properties::initializeGlAlways()) {
+ // Even though HWUI is rendering with Vulkan, some apps still use
+ // GL. Preload GL driver just in case. Since this happens prior to
+ // forking from the zygote, apps that do not use GL are unaffected.
+ // Any memory that (E)GL uses for this call is in shared memory,
+ // and this call only happens once.
+ eglGetDisplay(EGL_DEFAULT_DISPLAY);
+ }
}
}
diff --git a/libs/hwui/effects/GainmapRenderer.cpp b/libs/hwui/effects/GainmapRenderer.cpp
index 0a30c6c14c4c..eac03609d72f 100644
--- a/libs/hwui/effects/GainmapRenderer.cpp
+++ b/libs/hwui/effects/GainmapRenderer.cpp
@@ -96,6 +96,7 @@ void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkR
#ifdef __ANDROID__
static constexpr char gGainmapSKSL[] = R"SKSL(
+ uniform shader linearBase;
uniform shader base;
uniform shader gainmap;
uniform colorFilter workingSpaceToLinearSrgb;
@@ -117,7 +118,11 @@ static constexpr char gGainmapSKSL[] = R"SKSL(
}
half4 main(float2 coord) {
- half4 S = base.eval(coord);
+ if (W == 0.0) {
+ return base.eval(coord);
+ }
+
+ half4 S = linearBase.eval(coord);
half4 G = gainmap.eval(coord);
if (gainmapIsAlpha == 1) {
G = half4(G.a, G.a, G.a, 1.0);
@@ -186,8 +191,10 @@ private:
SkColorFilterPriv::MakeColorSpaceXform(baseColorSpace, gainmapMathColorSpace);
// The base image shader will convert into the color space in which the gainmap is applied.
- auto baseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions)
- ->makeWithColorFilter(colorXformSdrToGainmap);
+ auto linearBaseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions)
+ ->makeWithColorFilter(colorXformSdrToGainmap);
+
+ auto baseImageShader = baseImage->makeShader(tileModeX, tileModeY, samplingOptions);
// The gainmap image shader will ignore any color space that the gainmap has.
const SkMatrix gainmapRectToDstRect =
@@ -201,6 +208,7 @@ private:
auto colorXformGainmapToDst = SkColorFilterPriv::MakeColorSpaceXform(
gainmapMathColorSpace, SkColorSpace::MakeSRGBLinear());
+ mBuilder.child("linearBase") = std::move(linearBaseImageShader);
mBuilder.child("base") = std::move(baseImageShader);
mBuilder.child("gainmap") = std::move(gainmapImageShader);
mBuilder.child("workingSpaceToLinearSrgb") = std::move(colorXformGainmapToDst);
diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index 07e97f85d588..a88139d6b5d6 100644
--- a/libs/hwui/jni/Graphics.cpp
+++ b/libs/hwui/jni/Graphics.cpp
@@ -583,6 +583,16 @@ jobject GraphicsJNI::getColorSpace(JNIEnv* env, SkColorSpace* decodeColorSpace,
transferParams.a, transferParams.b, transferParams.c, transferParams.d,
transferParams.e, transferParams.f, transferParams.g);
+ // Some transfer functions that are considered valid by Skia are not
+ // accepted by android.graphics.
+ if (hasException(env)) {
+ // Callers (e.g. Bitmap#getColorSpace) are not expected to throw an
+ // Exception, so clear it and return null, which is a documented
+ // possibility.
+ env->ExceptionClear();
+ return nullptr;
+ }
+
jfloatArray xyzArray = env->NewFloatArray(9);
jfloat xyz[9] = {
xyzMatrix.vals[0][0],
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index 0d0af1110ca4..4d185c69c172 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -28,8 +28,8 @@
#include <include/gpu/ganesh/vk/GrVkBackendSemaphore.h>
#include <include/gpu/ganesh/vk/GrVkBackendSurface.h>
#include <include/gpu/ganesh/vk/GrVkDirectContext.h>
+#include <include/gpu/vk/VulkanBackendContext.h>
#include <ui/FatVector.h>
-#include <vk/GrVkExtensions.h>
#include <vk/GrVkTypes.h>
#include <sstream>
@@ -141,7 +141,8 @@ VulkanManager::~VulkanManager() {
mPhysicalDeviceFeatures2 = {};
}
-void VulkanManager::setupDevice(GrVkExtensions& grExtensions, VkPhysicalDeviceFeatures2& features) {
+void VulkanManager::setupDevice(skgpu::VulkanExtensions& grExtensions,
+ VkPhysicalDeviceFeatures2& features) {
VkResult err;
constexpr VkApplicationInfo app_info = {
@@ -506,7 +507,7 @@ sk_sp<GrDirectContext> VulkanManager::createContext(GrContextOptions& options,
return vkGetInstanceProcAddr(instance, proc_name);
};
- GrVkBackendContext backendContext;
+ skgpu::VulkanBackendContext backendContext;
backendContext.fInstance = mInstance;
backendContext.fPhysicalDevice = mPhysicalDevice;
backendContext.fDevice = mDevice;
diff --git a/libs/hwui/renderthread/VulkanManager.h b/libs/hwui/renderthread/VulkanManager.h
index b92ebb3cdf71..08f9d4253d7e 100644
--- a/libs/hwui/renderthread/VulkanManager.h
+++ b/libs/hwui/renderthread/VulkanManager.h
@@ -24,8 +24,7 @@
#include <SkSurface.h>
#include <android-base/unique_fd.h>
#include <utils/StrongPointer.h>
-#include <vk/GrVkBackendContext.h>
-#include <vk/GrVkExtensions.h>
+#include <vk/VulkanExtensions.h>
#include <vulkan/vulkan.h>
// VK_ANDROID_frame_boundary is a bespoke extension defined by AGI
@@ -127,7 +126,7 @@ private:
// Sets up the VkInstance and VkDevice objects. Also fills out the passed in
// VkPhysicalDeviceFeatures struct.
- void setupDevice(GrVkExtensions&, VkPhysicalDeviceFeatures2&);
+ void setupDevice(skgpu::VulkanExtensions&, VkPhysicalDeviceFeatures2&);
// simple wrapper class that exists only to initialize a pointer to NULL
template <typename FNPTR_TYPE>
@@ -206,7 +205,7 @@ private:
BufferAge,
};
SwapBehavior mSwapBehavior = SwapBehavior::Discard;
- GrVkExtensions mExtensions;
+ skgpu::VulkanExtensions mExtensions;
uint32_t mDriverVersion = 0;
std::once_flag mInitFlag;
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
index 7b7ccf51aa1a..7a82938435af 100644
--- a/libs/input/Android.bp
+++ b/libs/input/Android.bp
@@ -46,7 +46,6 @@ cc_library_shared {
"liblog",
"libutils",
"libgui",
- "libui",
"libinput",
],
diff --git a/libs/nativehelper_jvm/Android.bp b/libs/nativehelper_jvm/Android.bp
new file mode 100644
index 000000000000..b5b70283551a
--- /dev/null
+++ b/libs/nativehelper_jvm/Android.bp
@@ -0,0 +1,19 @@
+package {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+cc_library_host_static {
+ name: "libnativehelper_jvm",
+ srcs: [
+ "JNIPlatformHelp.c",
+ "JniConstants.c",
+ "file_descriptor_jni.c",
+ ],
+ whole_static_libs: ["libnativehelper_any_vm"],
+ export_static_lib_headers: ["libnativehelper_any_vm"],
+ target: {
+ windows: {
+ enabled: true,
+ },
+ },
+}
diff --git a/libs/nativehelper_jvm/JNIPlatformHelp.c b/libs/nativehelper_jvm/JNIPlatformHelp.c
new file mode 100644
index 000000000000..9df31a8caa7f
--- /dev/null
+++ b/libs/nativehelper_jvm/JNIPlatformHelp.c
@@ -0,0 +1,104 @@
+/*
+ * 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.
+ */
+
+#include <nativehelper/JNIPlatformHelp.h>
+
+#include <stddef.h>
+
+#include "JniConstants.h"
+
+static int GetBufferPosition(JNIEnv* env, jobject nioBuffer) {
+ return(*env)->GetIntField(env, nioBuffer, JniConstants_NioBuffer_position(env));
+}
+
+static int GetBufferLimit(JNIEnv* env, jobject nioBuffer) {
+ return(*env)->GetIntField(env, nioBuffer, JniConstants_NioBuffer_limit(env));
+}
+
+static int GetBufferElementSizeShift(JNIEnv* env, jobject nioBuffer) {
+ jclass byteBufferClass = JniConstants_NioByteBufferClass(env);
+ jclass shortBufferClass = JniConstants_NioShortBufferClass(env);
+ jclass charBufferClass = JniConstants_NioCharBufferClass(env);
+ jclass intBufferClass = JniConstants_NioIntBufferClass(env);
+ jclass floatBufferClass = JniConstants_NioFloatBufferClass(env);
+ jclass longBufferClass = JniConstants_NioLongBufferClass(env);
+ jclass doubleBufferClass = JniConstants_NioDoubleBufferClass(env);
+
+ // Check the type of the Buffer
+ if ((*env)->IsInstanceOf(env, nioBuffer, byteBufferClass)) {
+ return 0;
+ } else if ((*env)->IsInstanceOf(env, nioBuffer, shortBufferClass) ||
+ (*env)->IsInstanceOf(env, nioBuffer, charBufferClass)) {
+ return 1;
+ } else if ((*env)->IsInstanceOf(env, nioBuffer, intBufferClass) ||
+ (*env)->IsInstanceOf(env, nioBuffer, floatBufferClass)) {
+ return 2;
+ } else if ((*env)->IsInstanceOf(env, nioBuffer, longBufferClass) ||
+ (*env)->IsInstanceOf(env, nioBuffer, doubleBufferClass)) {
+ return 3;
+ }
+ return 0;
+}
+
+jarray jniGetNioBufferBaseArray(JNIEnv* env, jobject nioBuffer) {
+ jmethodID hasArrayMethod = JniConstants_NioBuffer_hasArray(env);
+ jboolean hasArray = (*env)->CallBooleanMethod(env, nioBuffer, hasArrayMethod);
+ if (hasArray) {
+ jmethodID arrayMethod = JniConstants_NioBuffer_array(env);
+ return (*env)->CallObjectMethod(env, nioBuffer, arrayMethod);
+ } else {
+ return NULL;
+ }
+}
+
+int jniGetNioBufferBaseArrayOffset(JNIEnv* env, jobject nioBuffer) {
+ jmethodID hasArrayMethod = JniConstants_NioBuffer_hasArray(env);
+ jboolean hasArray = (*env)->CallBooleanMethod(env, nioBuffer, hasArrayMethod);
+ if (hasArray) {
+ jmethodID arrayOffsetMethod = JniConstants_NioBuffer_arrayOffset(env);
+ jint arrayOffset = (*env)->CallIntMethod(env, nioBuffer, arrayOffsetMethod);
+ const int position = GetBufferPosition(env, nioBuffer);
+ jint elementSizeShift = GetBufferElementSizeShift(env, nioBuffer);
+ return (arrayOffset + position) << elementSizeShift;
+ } else {
+ return 0;
+ }
+}
+
+jlong jniGetNioBufferPointer(JNIEnv* env, jobject nioBuffer) {
+ // in Java 11, the address field of a HeapByteBuffer contains a non-zero value despite
+ // HeapByteBuffer being a non-direct buffer. In that case, this should still return 0.
+ jmethodID isDirectMethod = JniConstants_NioBuffer_isDirect(env);
+ jboolean isDirect = (*env)->CallBooleanMethod(env, nioBuffer, isDirectMethod);
+ if (isDirect == JNI_FALSE) {
+ return 0L;
+ }
+ jlong baseAddress = (*env)->GetLongField(env, nioBuffer, JniConstants_NioBuffer_address(env));
+ if (baseAddress != 0) {
+ const int position = GetBufferPosition(env, nioBuffer);
+ const int shift = GetBufferElementSizeShift(env, nioBuffer);
+ baseAddress += position << shift;
+ }
+ return baseAddress;
+}
+
+jlong jniGetNioBufferFields(JNIEnv* env, jobject nioBuffer,
+ jint* position, jint* limit, jint* elementSizeShift) {
+ *position = GetBufferPosition(env, nioBuffer);
+ *limit = GetBufferLimit(env, nioBuffer);
+ *elementSizeShift = GetBufferElementSizeShift(env, nioBuffer);
+ return (*env)->GetLongField(env, nioBuffer, JniConstants_NioBuffer_address(env));
+}
diff --git a/libs/nativehelper_jvm/JniConstants.c b/libs/nativehelper_jvm/JniConstants.c
new file mode 100644
index 000000000000..ca58f61070ba
--- /dev/null
+++ b/libs/nativehelper_jvm/JniConstants.c
@@ -0,0 +1,199 @@
+/*
+ * 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.
+ */
+
+#include "JniConstants.h"
+
+#include <pthread.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+#define LOG_TAG "JniConstants"
+#include <log/log.h>
+
+// jclass constants list:
+// <class, signature, androidOnly>
+#define JCLASS_CONSTANTS_LIST(V) \
+ V(FileDescriptor, "java/io/FileDescriptor", false) \
+ V(NioBuffer, "java/nio/Buffer", false) \
+ V(NioByteBuffer, "java/nio/ByteBuffer", false) \
+ V(NioShortBuffer, "java/nio/ShortBuffer", false) \
+ V(NioCharBuffer, "java/nio/CharBuffer", false) \
+ V(NioIntBuffer, "java/nio/IntBuffer", false) \
+ V(NioFloatBuffer, "java/nio/FloatBuffer", false) \
+ V(NioLongBuffer, "java/nio/LongBuffer", false) \
+ V(NioDoubleBuffer, "java/nio/DoubleBuffer", false)
+
+// jmethodID's of public methods constants list:
+// <Class, method, method-string, signature, is_static>
+#define JMETHODID_CONSTANTS_LIST(V) \
+ V(FileDescriptor, init, "<init>", "()V", false) \
+ V(NioBuffer, array, "array", "()Ljava/lang/Object;", false) \
+ V(NioBuffer, hasArray, "hasArray", "()Z", false) \
+ V(NioBuffer, isDirect, "isDirect", "()Z", false) \
+ V(NioBuffer, arrayOffset, "arrayOffset", "()I", false)
+
+// jfieldID constants list:
+// <Class, field, signature, is_static>
+#define JFIELDID_CONSTANTS_LIST(V) \
+ V(FileDescriptor, fd, "I", false) \
+ V(NioBuffer, address, "J", false) \
+ V(NioBuffer, limit, "I", false) \
+ V(NioBuffer, position, "I", false)
+
+#define CLASS_NAME(cls) g_ ## cls
+#define METHOD_NAME(cls, method) g_ ## cls ## _ ## method
+#define FIELD_NAME(cls, field) g_ ## cls ## _ ## field
+
+//
+// Declare storage for cached classes, methods and fields.
+//
+
+#define JCLASS_DECLARE_STORAGE(cls, ...) \
+ static jclass CLASS_NAME(cls) = NULL;
+JCLASS_CONSTANTS_LIST(JCLASS_DECLARE_STORAGE)
+#undef JCLASS_DECLARE_STORAGE
+
+#define JMETHODID_DECLARE_STORAGE(cls, method, ...) \
+ static jmethodID METHOD_NAME(cls, method) = NULL;
+JMETHODID_CONSTANTS_LIST(JMETHODID_DECLARE_STORAGE)
+#undef JMETHODID_DECLARE_STORAGE
+
+#define JFIELDID_DECLARE_STORAGE(cls, field, ...) \
+ static jfieldID FIELD_NAME(cls, field) = NULL;
+JFIELDID_CONSTANTS_LIST(JFIELDID_DECLARE_STORAGE)
+#undef JFIELDID_DECLARE_STORAGE
+
+//
+// Helper methods
+//
+
+static jclass FindClass(JNIEnv* env, const char* signature, bool androidOnly) {
+ jclass cls = (*env)->FindClass(env, signature);
+ if (cls == NULL) {
+ LOG_ALWAYS_FATAL_IF(!androidOnly, "Class not found: %s", signature);
+ return NULL;
+ }
+ return (*env)->NewGlobalRef(env, cls);
+}
+
+static jmethodID FindMethod(JNIEnv* env, jclass cls,
+ const char* name, const char* signature, bool isStatic) {
+ jmethodID method;
+ if (isStatic) {
+ method = (*env)->GetStaticMethodID(env, cls, name, signature);
+ } else {
+ method = (*env)->GetMethodID(env, cls, name, signature);
+ }
+ LOG_ALWAYS_FATAL_IF(method == NULL, "Method not found: %s:%s", name, signature);
+ return method;
+}
+
+static jfieldID FindField(JNIEnv* env, jclass cls,
+ const char* name, const char* signature, bool isStatic) {
+ jfieldID field;
+ if (isStatic) {
+ field = (*env)->GetStaticFieldID(env, cls, name, signature);
+ } else {
+ field = (*env)->GetFieldID(env, cls, name, signature);
+ }
+ LOG_ALWAYS_FATAL_IF(field == NULL, "Field not found: %s:%s", name, signature);
+ return field;
+}
+
+static pthread_once_t g_initialized = PTHREAD_ONCE_INIT;
+static JNIEnv* g_init_env;
+
+static void InitializeConstants() {
+ // Initialize cached classes.
+#define JCLASS_INITIALIZE(cls, signature, androidOnly) \
+ CLASS_NAME(cls) = FindClass(g_init_env, signature, androidOnly);
+ JCLASS_CONSTANTS_LIST(JCLASS_INITIALIZE)
+#undef JCLASS_INITIALIZE
+
+ // Initialize cached methods.
+#define JMETHODID_INITIALIZE(cls, method, name, signature, isStatic) \
+ METHOD_NAME(cls, method) = \
+ FindMethod(g_init_env, CLASS_NAME(cls), name, signature, isStatic);
+ JMETHODID_CONSTANTS_LIST(JMETHODID_INITIALIZE)
+#undef JMETHODID_INITIALIZE
+
+ // Initialize cached fields.
+#define JFIELDID_INITIALIZE(cls, field, signature, isStatic) \
+ FIELD_NAME(cls, field) = \
+ FindField(g_init_env, CLASS_NAME(cls), #field, signature, isStatic);
+ JFIELDID_CONSTANTS_LIST(JFIELDID_INITIALIZE)
+#undef JFIELDID_INITIALIZE
+}
+
+void EnsureInitialized(JNIEnv* env) {
+ // This method has to be called in every cache accesses because library can be built
+ // 2 different ways and existing usage for compat version doesn't have a good hook for
+ // initialization and is widely used.
+ g_init_env = env;
+ pthread_once(&g_initialized, InitializeConstants);
+}
+
+// API exported by libnativehelper_api.h.
+
+void jniUninitializeConstants() {
+ // Uninitialize cached classes, methods and fields.
+ //
+ // NB we assume the runtime is stopped at this point and do not delete global
+ // references.
+#define JCLASS_INVALIDATE(cls, ...) CLASS_NAME(cls) = NULL;
+ JCLASS_CONSTANTS_LIST(JCLASS_INVALIDATE);
+#undef JCLASS_INVALIDATE
+
+#define JMETHODID_INVALIDATE(cls, method, ...) METHOD_NAME(cls, method) = NULL;
+ JMETHODID_CONSTANTS_LIST(JMETHODID_INVALIDATE);
+#undef JMETHODID_INVALIDATE
+
+#define JFIELDID_INVALIDATE(cls, field, ...) FIELD_NAME(cls, field) = NULL;
+ JFIELDID_CONSTANTS_LIST(JFIELDID_INVALIDATE);
+#undef JFIELDID_INVALIDATE
+
+ // If jniConstantsUninitialize is called, runtime has shutdown. Reset
+ // state as some tests re-start the runtime.
+ pthread_once_t o = PTHREAD_ONCE_INIT;
+ memcpy(&g_initialized, &o, sizeof(o));
+}
+
+//
+// Accessors
+//
+
+#define JCLASS_ACCESSOR_IMPL(cls, ...) \
+jclass JniConstants_ ## cls ## Class(JNIEnv* env) { \
+ EnsureInitialized(env); \
+ return CLASS_NAME(cls); \
+}
+JCLASS_CONSTANTS_LIST(JCLASS_ACCESSOR_IMPL)
+#undef JCLASS_ACCESSOR_IMPL
+
+#define JMETHODID_ACCESSOR_IMPL(cls, method, ...) \
+jmethodID JniConstants_ ## cls ## _ ## method(JNIEnv* env) { \
+ EnsureInitialized(env); \
+ return METHOD_NAME(cls, method); \
+}
+JMETHODID_CONSTANTS_LIST(JMETHODID_ACCESSOR_IMPL)
+
+#define JFIELDID_ACCESSOR_IMPL(cls, field, ...) \
+jfieldID JniConstants_ ## cls ## _ ## field(JNIEnv* env) { \
+ EnsureInitialized(env); \
+ return FIELD_NAME(cls, field); \
+}
+JFIELDID_CONSTANTS_LIST(JFIELDID_ACCESSOR_IMPL)
diff --git a/libs/nativehelper_jvm/JniConstants.h b/libs/nativehelper_jvm/JniConstants.h
new file mode 100644
index 000000000000..e7a266d72509
--- /dev/null
+++ b/libs/nativehelper_jvm/JniConstants.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright 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.
+ */
+
+#pragma once
+
+#include <sys/cdefs.h>
+
+#include <jni.h>
+
+__BEGIN_DECLS
+
+//
+// Classes in constants cache.
+//
+// NB The implementations of these methods are generated by the JCLASS_ACCESSOR_IMPL macro in
+// JniConstants.c.
+//
+jclass JniConstants_FileDescriptorClass(JNIEnv* env);
+jclass JniConstants_NioByteBufferClass(JNIEnv* env);
+jclass JniConstants_NioShortBufferClass(JNIEnv* env);
+jclass JniConstants_NioCharBufferClass(JNIEnv* env);
+jclass JniConstants_NioIntBufferClass(JNIEnv* env);
+jclass JniConstants_NioFloatBufferClass(JNIEnv* env);
+jclass JniConstants_NioLongBufferClass(JNIEnv* env);
+jclass JniConstants_NioDoubleBufferClass(JNIEnv* env);
+
+//
+// Methods in the constants cache.
+//
+// NB The implementations of these methods are generated by the JMETHODID_ACCESSOR_IMPL macro in
+// JniConstants.c.
+//
+jmethodID JniConstants_FileDescriptor_init(JNIEnv* env);
+jmethodID JniConstants_NioBuffer_array(JNIEnv* env);
+jmethodID JniConstants_NioBuffer_arrayOffset(JNIEnv* env);
+jmethodID JniConstants_NioBuffer_hasArray(JNIEnv* env);
+jmethodID JniConstants_NioBuffer_isDirect(JNIEnv* env);
+
+//
+// Fields in the constants cache.
+//
+// NB The implementations of these methods are generated by the JFIELDID_ACCESSOR_IMPL macro in
+// JniConstants.c.
+//
+jfieldID JniConstants_FileDescriptor_fd(JNIEnv* env);
+jfieldID JniConstants_NioBuffer_address(JNIEnv* env);
+jfieldID JniConstants_NioBuffer_limit(JNIEnv* env);
+jfieldID JniConstants_NioBuffer_position(JNIEnv* env);
+
+__END_DECLS
diff --git a/libs/nativehelper_jvm/OWNERS b/libs/nativehelper_jvm/OWNERS
new file mode 100644
index 000000000000..5d55f6e4319b
--- /dev/null
+++ b/libs/nativehelper_jvm/OWNERS
@@ -0,0 +1,7 @@
+# Bug component: 326772
+
+include /libs/hwui/OWNERS
+include platform/libnativehelper:/OWNERS
+
+diegoperez@google.com
+jgaillard@google.com
diff --git a/libs/nativehelper_jvm/README b/libs/nativehelper_jvm/README
new file mode 100644
index 000000000000..755c42261f43
--- /dev/null
+++ b/libs/nativehelper_jvm/README
@@ -0,0 +1,2 @@
+libnativehelper_jvm is a JVM-compatible version of libnativehelper.
+It should be used instead of libnativehelper whenever a host library is meant to run on a JVM. \ No newline at end of file
diff --git a/libs/nativehelper_jvm/file_descriptor_jni.c b/libs/nativehelper_jvm/file_descriptor_jni.c
new file mode 100644
index 000000000000..36880cd586ca
--- /dev/null
+++ b/libs/nativehelper_jvm/file_descriptor_jni.c
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+#include <android/file_descriptor_jni.h>
+
+#include <stddef.h>
+
+#define LOG_TAG "file_descriptor_jni"
+#include <log/log.h>
+
+#include "JniConstants.h"
+
+static void EnsureArgumentIsFileDescriptor(JNIEnv* env, jobject instance) {
+ LOG_ALWAYS_FATAL_IF(instance == NULL, "FileDescriptor is NULL");
+ jclass jifd = JniConstants_FileDescriptorClass(env);
+ LOG_ALWAYS_FATAL_IF(!(*env)->IsInstanceOf(env, instance, jifd),
+ "Argument is not a FileDescriptor");
+}
+
+JNIEXPORT _Nullable jobject AFileDescriptor_create(JNIEnv* env) {
+ return (*env)->NewObject(env,
+ JniConstants_FileDescriptorClass(env),
+ JniConstants_FileDescriptor_init(env));
+}
+
+JNIEXPORT int AFileDescriptor_getFd(JNIEnv* env, jobject fileDescriptor) {
+ EnsureArgumentIsFileDescriptor(env, fileDescriptor);
+ return (*env)->GetIntField(env, fileDescriptor, JniConstants_FileDescriptor_fd(env));
+}
+
+JNIEXPORT void AFileDescriptor_setFd(JNIEnv* env, jobject fileDescriptor, int fd) {
+ EnsureArgumentIsFileDescriptor(env, fileDescriptor);
+ (*env)->SetIntField(env, fileDescriptor, JniConstants_FileDescriptor_fd(env), fd);
+}