summaryrefslogtreecommitdiff
path: root/libs
diff options
context:
space:
mode:
Diffstat (limited to 'libs')
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java62
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java16
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java111
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java48
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java41
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java1413
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java17
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java60
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java694
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java187
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java286
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java270
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java27
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/util/DeduplicateConsumer.java63
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java69
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java129
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java27
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java15
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/Android.bp2
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java57
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java941
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java13
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java18
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java478
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java275
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java71
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java37
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java8
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java165
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java7
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/util/DeduplicateConsumerTest.java97
-rw-r--r--libs/WindowManager/Shell/Android.bp26
-rw-r--r--libs/WindowManager/Shell/AndroidManifest.xml32
-rw-r--r--libs/WindowManager/Shell/aconfig/Android.bp2
-rw-r--r--libs/WindowManager/Shell/aconfig/multitasking.aconfig57
-rw-r--r--libs/WindowManager/Shell/multivalentTests/Android.bp2
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt178
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt283
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt459
-rw-r--r--libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml (renamed from libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml)14
-rw-r--r--libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_color_selector.xml14
-rw-r--r--libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_outline_color_selector.xml28
-rw-r--r--libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml26
-rw-r--r--libs/WindowManager/Shell/res/drawable/circular_progress.xml4
-rw-r--r--libs/WindowManager/Shell/res/drawable/decor_desktop_mode_maximize_button_dark.xml8
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml25
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_background.xml5
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_button_background.xml (renamed from libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_maximize_button_background.xml)3
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml (renamed from libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_snap_right_button_background.xml)11
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background_on_hover.xml (renamed from libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_snap_left_button_background.xml)10
-rw-r--r--libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml22
-rw-r--r--libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget.xml19
-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_drop_target.xml22
-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)5
-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)15
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml7
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml11
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml102
-rw-r--r--libs/WindowManager/Shell/res/layout/maximize_menu_button.xml30
-rw-r--r--libs/WindowManager/Shell/res/values-af/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-am/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ar/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-as/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-az/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-be/strings.xml20
-rw-r--r--libs/WindowManager/Shell/res/values-bg/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-bn/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-bs/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ca/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-cs/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-da/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-de/strings.xml8
-rw-r--r--libs/WindowManager/Shell/res/values-el/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-en-rAU/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-en-rCA/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-en-rGB/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-en-rIN/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-en-rXC/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-es-rUS/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-es/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-et/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-eu/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-fa/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-fi/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-fr-rCA/strings.xml10
-rw-r--r--libs/WindowManager/Shell/res/values-fr/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-gl/strings.xml10
-rw-r--r--libs/WindowManager/Shell/res/values-gu/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-hi/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-hr/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-hu/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-hy/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-in/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-is/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-it/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-iw/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ja/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ka/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-kk/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-km/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-kn/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ko/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ky/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-lo/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-lt/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-lv/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-mk/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ml/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-mn/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-mr/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ms/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-my/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-nb/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-ne/strings.xml6
-rw-r--r--libs/WindowManager/Shell/res/values-nl/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-or/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-pa/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-pl/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-pt-rBR/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-pt-rPT/strings.xml6
-rw-r--r--libs/WindowManager/Shell/res/values-pt/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ro/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ru/strings.xml10
-rw-r--r--libs/WindowManager/Shell/res/values-si/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-sk/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-sl/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-sq/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-sr/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-sv/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-sw/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ta/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-te/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-th/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values-tl/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-tr/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-uk/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ur/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-uz/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-vi/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rCN/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rHK/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-zh-rTW/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-zu/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values/colors.xml4
-rw-r--r--libs/WindowManager/Shell/res/values/config.xml42
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml101
-rw-r--r--libs/WindowManager/Shell/res/values/strings.xml10
-rw-r--r--libs/WindowManager/Shell/res/values/styles.xml8
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java184
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IHomeTransitionListener.aidl (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl)4
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl)15
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java)17
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java15
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimator.kt (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt)22
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt)15
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ChoreographerSfVsync.java34
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalMainThread.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalMainThread.java)4
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalThread.java31
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellAnimationThread.java31
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellBackgroundThread.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java)4
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellMainThread.java31
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellSplashscreenThread.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellSplashscreenThread.java)4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunner.java39
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java48
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java376
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java455
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt586
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java47
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt302
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java429
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt113
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimation.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java30
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java240
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java331
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java76
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java88
-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/BubbleFlyoutView.java62
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java127
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java525
-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.java22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java20
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java53
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java25
-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/BubbleBarExpandedViewDragController.kt56
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java90
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt93
-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/DevicePostureController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java2
-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.java27
-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/MultiInstanceHelper.kt10
-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/SyncTransactionQueue.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt210
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.aidl19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.kt63
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java55
-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/bubbles/DismissView.kt2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS6
-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/magnetictarget/MagnetizedObject.kt2
-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/PhoneSizeSpecSource.kt80
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java25
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDisplayLayoutState.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt49
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java76
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java48
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java17
-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/AppCompatUtils.kt24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java61
-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/compatui/UserAspectRatioSettingsWindowManager.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java67
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java99
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java78
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt84
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt394
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt49
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java107
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt223
-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/DesktopModeUiEventLogger.kt93
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt173
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java45
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt1154
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt218
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt96
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt57
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt559
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ToggleResizeDesktopTaskTransitionHandler.kt6
-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/draganddrop/DragAndDropController.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java27
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java55
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java40
-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.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java61
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java123
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java37
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java22
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java17
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java36
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java1
-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.java279
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java311
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java188
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java790
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java598
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java117
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java47
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java1128
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java427
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java325
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java322
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl10
-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/RecentTasks.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java141
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java167
-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/SplitScreen.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java54
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java23
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java284
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java16
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java38
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java35
-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.java25
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java77
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java89
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java55
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java78
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java30
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java25
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHandler.java31
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java58
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java108
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java93
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java280
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java91
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java28
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java378
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java456
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java97
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java424
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java529
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt79
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java244
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt48
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt48
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt534
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt44
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java270
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt417
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java27
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java8
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java391
-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/extension/TaskInfo.kt8
-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)25
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt500
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt178
-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/OWNERS2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml3
-rw-r--r--libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml3
-rw-r--r--libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml3
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt10
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt7
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt7
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt23
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt7
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt4
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt34
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipToOtherOrientationTest.kt163
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/EnterPipTransition.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/MovePipShelfHeightTransition.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml3
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/OWNERS5
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt47
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt47
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt144
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt44
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt43
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizeLandscape.kt43
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizePortrait.kt43
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt77
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt67
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt68
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt14
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt8
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt6
-rw-r--r--libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml3
-rw-r--r--libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt4
-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/SwitchBetweenSplitPairsNoPip.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt9
-rw-r--r--libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt2
-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/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt58
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt17
-rw-r--r--libs/WindowManager/Shell/tests/unittest/Android.bp5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java176
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java13
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java48
-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.java106
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java49
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt264
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java237
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt246
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java93
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTest.java17
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt35
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleBarLocationTest.kt53
-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/common/magnetictarget/MagnetizedObjectTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java23
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt59
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java23
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java13
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java66
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java18
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt637
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt271
-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/DesktopModeUiEventLoggerTest.kt111
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt19
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt2577
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt320
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt7
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt163
-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/draganddrop/DragAndDropPolicyTest.java31
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java14
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java182
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java115
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java236
-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/shared/animation/PhysicsAnimatorTest.kt (renamed from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt)12
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java16
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java116
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java4
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java68
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java26
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java43
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java21
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java135
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java9
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java26
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt205
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java299
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt172
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java390
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt132
-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.kt228
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt108
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java245
-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/LocaleDataTables.cpp2
-rw-r--r--libs/androidfw/ResourceTypes.cpp41
-rw-r--r--libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp5
-rw-r--r--libs/dream/lowlight/tests/Android.bp2
-rw-r--r--libs/hostgraphics/ADisplay.cpp12
-rw-r--r--libs/hostgraphics/ANativeWindow.cpp106
-rw-r--r--libs/hostgraphics/Android.bp12
-rw-r--r--libs/hostgraphics/Fence.cpp2
-rw-r--r--libs/hostgraphics/HostBufferQueue.cpp63
-rw-r--r--libs/hostgraphics/PublicFormat.cpp2
-rw-r--r--libs/hostgraphics/gui/BufferItem.h8
-rw-r--r--libs/hostgraphics/gui/BufferItemConsumer.h43
-rw-r--r--libs/hostgraphics/gui/BufferQueue.h2
-rw-r--r--libs/hostgraphics/gui/ConsumerBase.h4
-rw-r--r--libs/hostgraphics/gui/IGraphicBufferConsumer.h8
-rw-r--r--libs/hostgraphics/gui/IGraphicBufferProducer.h7
-rw-r--r--libs/hostgraphics/gui/Surface.h99
-rw-r--r--libs/hostgraphics/ui/Fence.h27
-rw-r--r--libs/hostgraphics/ui/GraphicBuffer.h58
-rw-r--r--libs/hwui/Android.bp51
-rw-r--r--libs/hwui/AnimatorManager.cpp8
-rw-r--r--libs/hwui/ColorFilter.h29
-rw-r--r--libs/hwui/DeviceInfo.cpp4
-rw-r--r--libs/hwui/DeviceInfo.h10
-rw-r--r--libs/hwui/Mesh.cpp48
-rw-r--r--libs/hwui/Mesh.h225
-rw-r--r--libs/hwui/Properties.cpp23
-rw-r--r--libs/hwui/Properties.h9
-rw-r--r--libs/hwui/RecordingCanvas.cpp13
-rw-r--r--libs/hwui/RecordingCanvas.h17
-rw-r--r--libs/hwui/RenderNode.cpp22
-rw-r--r--libs/hwui/RootRenderNode.cpp24
-rw-r--r--libs/hwui/RootRenderNode.h2
-rw-r--r--libs/hwui/SkiaCanvas.cpp4
-rw-r--r--libs/hwui/SkiaInterpolator.cpp1
-rw-r--r--libs/hwui/SkiaWrapper.h56
-rw-r--r--libs/hwui/VectorDrawable.cpp2
-rw-r--r--libs/hwui/WebViewFunctorManager.cpp43
-rw-r--r--libs/hwui/WebViewFunctorManager.h20
-rw-r--r--libs/hwui/aconfig/hwui_flags.aconfig20
-rw-r--r--libs/hwui/apex/LayoutlibLoader.cpp4
-rw-r--r--libs/hwui/apex/jni_runtime.cpp13
-rw-r--r--libs/hwui/effects/GainmapRenderer.cpp36
-rw-r--r--libs/hwui/hwui/AnimatedImageDrawable.cpp14
-rw-r--r--libs/hwui/hwui/AnimatedImageThread.cpp4
-rw-r--r--libs/hwui/hwui/Canvas.h2
-rw-r--r--libs/hwui/hwui/DrawTextFunctor.h46
-rw-r--r--libs/hwui/jni/AnimatedImageDrawable.cpp21
-rw-r--r--libs/hwui/jni/Bitmap.cpp18
-rw-r--r--libs/hwui/jni/BitmapFactory.cpp4
-rw-r--r--libs/hwui/jni/Graphics.cpp23
-rw-r--r--libs/hwui/jni/HardwareBufferHelpers.cpp2
-rw-r--r--libs/hwui/jni/Shader.cpp26
-rw-r--r--libs/hwui/jni/android_graphics_Color.cpp55
-rw-r--r--libs/hwui/jni/android_graphics_ColorSpace.cpp2
-rw-r--r--libs/hwui/jni/android_graphics_DisplayListCanvas.cpp15
-rw-r--r--libs/hwui/jni/android_graphics_HardwareRenderer.cpp32
-rw-r--r--libs/hwui/jni/android_graphics_Matrix.cpp13
-rw-r--r--libs/hwui/jni/android_graphics_Mesh.cpp14
-rw-r--r--libs/hwui/jni/android_graphics_RenderNode.cpp14
-rw-r--r--libs/hwui/jni/pdf/PdfRenderer.cpp134
-rw-r--r--libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp132
-rw-r--r--libs/hwui/pipeline/skia/SkiaCpuPipeline.h77
-rw-r--r--libs/hwui/pipeline/skia/SkiaDisplayList.cpp25
-rw-r--r--libs/hwui/pipeline/skia/SkiaDisplayList.h3
-rw-r--r--libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp194
-rw-r--r--libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp16
-rw-r--r--libs/hwui/pipeline/skia/SkiaPipeline.cpp242
-rw-r--r--libs/hwui/pipeline/skia/SkiaPipeline.h25
-rw-r--r--libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp2
-rw-r--r--libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp13
-rw-r--r--libs/hwui/pipeline/skia/VkFunctorDrawable.cpp2
-rw-r--r--libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h59
-rw-r--r--libs/hwui/platform/android/pipeline/skia/SkiaOpenGLPipeline.h (renamed from libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h)4
-rw-r--r--libs/hwui/platform/android/pipeline/skia/SkiaVulkanPipeline.h (renamed from libs/hwui/pipeline/skia/SkiaVulkanPipeline.h)4
-rw-r--r--libs/hwui/platform/android/thread/CommonPoolBase.h57
-rw-r--r--libs/hwui/platform/darwin/utils/SharedLib.cpp33
-rw-r--r--libs/hwui/platform/host/WebViewFunctorManager.cpp9
l---------libs/hwui/platform/host/android/api-level.h1
-rw-r--r--libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h83
-rw-r--r--libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h35
-rw-r--r--libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h35
-rw-r--r--libs/hwui/platform/host/renderthread/HintSessionWrapper.cpp61
-rw-r--r--libs/hwui/platform/host/renderthread/ReliableSurface.cpp39
-rw-r--r--libs/hwui/platform/host/renderthread/RenderThread.cpp2
-rw-r--r--libs/hwui/platform/host/thread/CommonPoolBase.h56
-rw-r--r--libs/hwui/platform/host/utils/MessageHandler.h34
-rw-r--r--libs/hwui/platform/linux/utils/SharedLib.cpp33
-rw-r--r--libs/hwui/private/hwui/WebViewFunctor.h5
-rw-r--r--libs/hwui/renderstate/RenderState.h6
-rw-r--r--libs/hwui/renderthread/CanvasContext.cpp34
-rw-r--r--libs/hwui/renderthread/DrawFrameTask.cpp4
-rw-r--r--libs/hwui/renderthread/EglManager.cpp19
-rw-r--r--libs/hwui/renderthread/HintSessionWrapper.cpp36
-rw-r--r--libs/hwui/renderthread/HintSessionWrapper.h17
-rw-r--r--libs/hwui/renderthread/IRenderPipeline.h3
-rw-r--r--libs/hwui/renderthread/ReliableSurface.h4
-rw-r--r--libs/hwui/renderthread/RenderProxy.cpp18
-rw-r--r--libs/hwui/renderthread/VulkanManager.cpp13
-rw-r--r--libs/hwui/renderthread/VulkanSurface.cpp15
-rw-r--r--libs/hwui/tests/unit/HintSessionWrapperTests.cpp100
-rw-r--r--libs/hwui/thread/CommonPool.cpp27
-rw-r--r--libs/hwui/thread/CommonPool.h13
-rw-r--r--libs/hwui/utils/Color.cpp13
-rw-r--r--libs/hwui/utils/Color.h2
-rw-r--r--libs/hwui/utils/SharedLib.h34
-rw-r--r--libs/input/Android.bp2
-rw-r--r--libs/input/MouseCursorController.cpp16
-rw-r--r--libs/input/MouseCursorController.h6
-rw-r--r--libs/input/PointerController.cpp144
-rw-r--r--libs/input/PointerController.h38
-rw-r--r--libs/input/PointerControllerContext.cpp8
-rw-r--r--libs/input/PointerControllerContext.h21
-rw-r--r--libs/input/SpriteController.cpp74
-rw-r--r--libs/input/SpriteController.h45
-rw-r--r--libs/input/SpriteIcon.h2
-rw-r--r--libs/input/TouchSpotController.cpp14
-rw-r--r--libs/input/TouchSpotController.h9
-rw-r--r--libs/input/tests/PointerController_test.cpp167
-rw-r--r--libs/input/tests/mocks/MockSprite.h3
-rw-r--r--libs/input/tests/mocks/MockSpriteController.h2
667 files changed, 34894 insertions, 9642 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
index 88fd461debbe..98935e95deaf 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/DeviceStateManagerFoldingFeatureProducer.java
@@ -16,16 +16,17 @@
package androidx.window.common;
-import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_UNKNOWN;
-import static androidx.window.common.CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE;
import static androidx.window.common.CommonFoldingFeature.parseListFromString;
import android.annotation.NonNull;
import android.content.Context;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback;
+import android.hardware.devicestate.DeviceStateUtil;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseIntArray;
@@ -54,29 +55,27 @@ public final class DeviceStateManagerFoldingFeatureProducer
private static final boolean DEBUG = false;
/**
- * Emulated device state {@link DeviceStateManager.DeviceStateCallback#onStateChanged(int)} to
+ * Emulated device state
+ * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)} to
* {@link CommonFoldingFeature.State} map.
*/
private final SparseIntArray mDeviceStateToPostureMap = new SparseIntArray();
/**
- * Emulated device state received via
- * {@link DeviceStateManager.DeviceStateCallback#onStateChanged(int)}.
- * "Emulated" states differ from "base" state in the sense that they may not correspond 1:1 with
- * physical device states. They represent the state of the device when various software
- * features and APIs are applied. The emulated states generally consist of all "base" states,
- * but may have additional states such as "concurrent" or "rear display". Concurrent mode for
- * example is activated via public API and can be active in both the "open" and "half folded"
- * device states.
+ * Device state received via
+ * {@link DeviceStateManager.DeviceStateCallback#onDeviceStateChanged(DeviceState)}.
+ * The identifier returned through {@link DeviceState#getIdentifier()} may not correspond 1:1
+ * with the physical state of the device. This could correspond to the system state of the
+ * device when various software features or overrides are applied. The emulated states generally
+ * consist of all "base" states, but may have additional states such as "concurrent" or
+ * "rear display". Concurrent mode for example is activated via public API and can be active in
+ * both the "open" and "half folded" device states.
*/
- private int mCurrentDeviceState = INVALID_DEVICE_STATE;
+ private DeviceState mCurrentDeviceState = new DeviceState(
+ new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER,
+ "INVALID").build());
- /**
- * Base device state received via
- * {@link DeviceStateManager.DeviceStateCallback#onBaseStateChanged(int)}.
- * "Base" in this context means the "physical" state of the device.
- */
- private int mCurrentBaseDeviceState = INVALID_DEVICE_STATE;
+ private List<DeviceState> mSupportedStates;
@NonNull
private final RawFoldingFeatureProducer mRawFoldSupplier;
@@ -85,22 +84,11 @@ public final class DeviceStateManagerFoldingFeatureProducer
private final DeviceStateCallback mDeviceStateCallback = new DeviceStateCallback() {
@Override
- public void onStateChanged(int state) {
+ public void onDeviceStateChanged(@NonNull DeviceState state) {
mCurrentDeviceState = state;
mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer
.this::notifyFoldingFeatureChange);
}
-
- @Override
- public void onBaseStateChanged(int state) {
- mCurrentBaseDeviceState = state;
-
- if (mDeviceStateToPostureMap.get(mCurrentDeviceState)
- == COMMON_STATE_USE_BASE_STATE) {
- mRawFoldSupplier.getData(DeviceStateManagerFoldingFeatureProducer
- .this::notifyFoldingFeatureChange);
- }
- }
};
public DeviceStateManagerFoldingFeatureProducer(@NonNull Context context,
@@ -109,6 +97,7 @@ public final class DeviceStateManagerFoldingFeatureProducer
mRawFoldSupplier = rawFoldSupplier;
String[] deviceStatePosturePairs = context.getResources()
.getStringArray(R.array.config_device_state_postures);
+ mSupportedStates = deviceStateManager.getSupportedDeviceStates();
boolean isHalfOpenedSupported = false;
for (String deviceStatePosturePair : deviceStatePosturePairs) {
String[] deviceStatePostureMapping = deviceStatePosturePair.split(":");
@@ -168,7 +157,7 @@ public final class DeviceStateManagerFoldingFeatureProducer
*/
private boolean isCurrentStateValid() {
// If the device state is not found in the map, indexOfKey returns a negative number.
- return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState) >= 0;
+ return mDeviceStateToPostureMap.indexOfKey(mCurrentDeviceState.getIdentifier()) >= 0;
}
@Override
@@ -177,7 +166,9 @@ public final class DeviceStateManagerFoldingFeatureProducer
if (hasListeners()) {
mRawFoldSupplier.addDataChangedCallback(this::notifyFoldingFeatureChange);
} else {
- mCurrentDeviceState = INVALID_DEVICE_STATE;
+ mCurrentDeviceState = new DeviceState(
+ new DeviceState.Configuration.Builder(INVALID_DEVICE_STATE_IDENTIFIER,
+ "INVALID").build());
mRawFoldSupplier.removeDataChangedCallback(this::notifyFoldingFeatureChange);
}
}
@@ -251,10 +242,13 @@ public final class DeviceStateManagerFoldingFeatureProducer
@CommonFoldingFeature.State
private int currentHingeState() {
@CommonFoldingFeature.State
- int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState, COMMON_STATE_UNKNOWN);
+ int posture = mDeviceStateToPostureMap.get(mCurrentDeviceState.getIdentifier(),
+ COMMON_STATE_UNKNOWN);
if (posture == CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE) {
- posture = mDeviceStateToPostureMap.get(mCurrentBaseDeviceState, COMMON_STATE_UNKNOWN);
+ posture = mDeviceStateToPostureMap.get(
+ DeviceStateUtil.calculateBaseStateIdentifier(mCurrentDeviceState,
+ mSupportedStates), COMMON_STATE_UNKNOWN);
}
return posture;
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java
index d923a46c3b5d..d24164159b2b 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/common/EmptyLifecycleCallbacksAdapter.java
@@ -16,6 +16,8 @@
package androidx.window.common;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
@@ -26,30 +28,30 @@ import android.os.Bundle;
*/
public class EmptyLifecycleCallbacksAdapter implements Application.ActivityLifecycleCallbacks {
@Override
- public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+ public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
}
@Override
- public void onActivityStarted(Activity activity) {
+ public void onActivityStarted(@NonNull Activity activity) {
}
@Override
- public void onActivityResumed(Activity activity) {
+ public void onActivityResumed(@NonNull Activity activity) {
}
@Override
- public void onActivityPaused(Activity activity) {
+ public void onActivityPaused(@NonNull Activity activity) {
}
@Override
- public void onActivityStopped(Activity activity) {
+ public void onActivityStopped(@NonNull Activity activity) {
}
@Override
- public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+ public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
- public void onActivityDestroyed(Activity activity) {
+ public void onActivityDestroyed(@NonNull Activity activity) {
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index 6714263ad952..ecf47209a802 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -16,15 +16,20 @@
package androidx.window.extensions;
-import android.app.ActivityTaskManager;
+import static android.view.WindowManager.ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15;
+import static android.view.WindowManager.ENABLE_ACTIVITY_EMBEDDING_FOR_ANDROID_15;
+
import android.app.ActivityThread;
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;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
import androidx.window.common.RawFoldingFeatureProducer;
import androidx.window.extensions.area.WindowAreaComponent;
@@ -38,25 +43,59 @@ import java.util.Objects;
/**
- * The reference implementation of {@link WindowExtensions} that implements the initial API version.
+ * The reference implementation of {@link WindowExtensions} that implements the latest WindowManager
+ * Extensions APIs.
*/
-public class WindowExtensionsImpl implements WindowExtensions {
+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
+ static final int EXTENSIONS_VERSION_CURRENT_PLATFORM = 6;
+
private final Object mLock = new Object();
private volatile DeviceStateManagerFoldingFeatureProducer mFoldingFeatureProducer;
private volatile WindowLayoutComponentImpl mWindowLayoutComponent;
private volatile SplitController mSplitController;
private volatile WindowAreaComponent mWindowAreaComponent;
- public WindowExtensionsImpl() {
- Log.i(TAG, "Initializing Window Extensions.");
+ private final int mVersion = EXTENSIONS_VERSION_CURRENT_PLATFORM;
+ private final boolean mIsActivityEmbeddingEnabled;
+
+ WindowExtensionsImpl() {
+ mIsActivityEmbeddingEnabled = isActivityEmbeddingEnabled();
+
+ 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 5;
+ 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
@@ -74,8 +113,8 @@ public class WindowExtensionsImpl implements WindowExtensions {
if (mFoldingFeatureProducer == null) {
synchronized (mLock) {
if (mFoldingFeatureProducer == null) {
- Context context = getApplication();
- RawFoldingFeatureProducer foldingFeatureProducer =
+ final Context context = getApplication();
+ final RawFoldingFeatureProducer foldingFeatureProducer =
new RawFoldingFeatureProducer(context);
mFoldingFeatureProducer =
new DeviceStateManagerFoldingFeatureProducer(context,
@@ -91,8 +130,8 @@ public class WindowExtensionsImpl implements WindowExtensions {
if (mWindowLayoutComponent == null) {
synchronized (mLock) {
if (mWindowLayoutComponent == null) {
- Context context = getApplication();
- DeviceStateManagerFoldingFeatureProducer producer =
+ final Context context = getApplication();
+ final DeviceStateManagerFoldingFeatureProducer producer =
getFoldingFeatureProducer();
mWindowLayoutComponent = new WindowLayoutComponentImpl(context, producer);
}
@@ -102,29 +141,35 @@ public class WindowExtensionsImpl implements WindowExtensions {
}
/**
- * Returns a reference implementation of {@link WindowLayoutComponent} if available,
- * {@code null} otherwise. The implementation must match the API level reported in
- * {@link WindowExtensions#getWindowLayoutComponent()}.
+ * Returns a reference implementation of the latest {@link WindowLayoutComponent}.
+ *
+ * The implementation must match the API level reported in
+ * {@link WindowExtensions#getVendorApiLevel()}.
+ *
* @return {@link WindowLayoutComponent} OEM implementation
*/
+ @NonNull
@Override
public WindowLayoutComponent getWindowLayoutComponent() {
return getWindowLayoutComponentImpl();
}
/**
- * Returns a reference implementation of {@link ActivityEmbeddingComponent} if available,
- * {@code null} otherwise. The implementation must match the API level reported in
- * {@link WindowExtensions#getWindowLayoutComponent()}.
+ * Returns a reference implementation of the latest {@link ActivityEmbeddingComponent} if the
+ * device supports this feature, {@code null} otherwise.
+ *
+ * The implementation must match the API level reported in
+ * {@link WindowExtensions#getVendorApiLevel()}.
+ *
* @return {@link ActivityEmbeddingComponent} OEM implementation.
*/
@Nullable
+ @Override
public ActivityEmbeddingComponent getActivityEmbeddingComponent() {
+ if (!mIsActivityEmbeddingEnabled) {
+ return null;
+ }
if (mSplitController == null) {
- if (!ActivityTaskManager.supportsMultiWindow(getApplication())) {
- // Disable AE for device that doesn't support multi window.
- return null;
- }
synchronized (mLock) {
if (mSplitController == null) {
mSplitController = new SplitController(
@@ -138,21 +183,35 @@ public class WindowExtensionsImpl implements WindowExtensions {
}
/**
- * Returns a reference implementation of {@link WindowAreaComponent} if available,
- * {@code null} otherwise. The implementation must match the API level reported in
- * {@link WindowExtensions#getWindowAreaComponent()}.
+ * Returns a reference implementation of the latest {@link WindowAreaComponent}
+ *
+ * The implementation must match the API level reported in
+ * {@link WindowExtensions#getVendorApiLevel()}.
+ *
* @return {@link WindowAreaComponent} OEM implementation.
*/
+ @Nullable
+ @Override
public WindowAreaComponent getWindowAreaComponent() {
if (mWindowAreaComponent == null) {
synchronized (mLock) {
if (mWindowAreaComponent == null) {
- Context context = ActivityThread.currentApplication();
- mWindowAreaComponent =
- new WindowAreaComponentImpl(context);
+ final Context context = getApplication();
+ mWindowAreaComponent = new WindowAreaComponentImpl(context);
}
}
}
return mWindowAreaComponent;
}
+
+ @VisibleForTesting
+ static boolean isActivityEmbeddingEnabled() {
+ if (!ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15) {
+ // Device enables it for all apps without targetSDK check.
+ // This must be true for all large screen devices.
+ return true;
+ }
+ // Use compat framework to guard the feature with targetSDK 15.
+ return CompatChanges.isChangeEnabled(ENABLE_ACTIVITY_EMBEDDING_FOR_ANDROID_15);
+ }
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java
index f9e1f077cffc..5d4c7cbe60e4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsProvider.java
@@ -16,14 +16,20 @@
package androidx.window.extensions;
-import android.annotation.NonNull;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.area.WindowAreaComponent;
+import androidx.window.extensions.embedding.ActivityEmbeddingComponent;
+import androidx.window.extensions.layout.WindowLayoutComponent;
/**
* Provides the OEM implementation of {@link WindowExtensions}.
*/
public class WindowExtensionsProvider {
- private static final WindowExtensions sWindowExtensions = new WindowExtensionsImpl();
+ private static volatile WindowExtensions sWindowExtensions;
/**
* Returns the OEM implementation of {@link WindowExtensions}. This method is implemented in
@@ -33,6 +39,44 @@ public class WindowExtensionsProvider {
*/
@NonNull
public static WindowExtensions getWindowExtensions() {
+ if (sWindowExtensions == null) {
+ synchronized (WindowExtensionsProvider.class) {
+ if (sWindowExtensions == null) {
+ sWindowExtensions = WindowManager.hasWindowExtensionsEnabled()
+ ? new WindowExtensionsImpl()
+ : new DisabledWindowExtensions();
+ }
+ }
+ }
return sWindowExtensions;
}
+
+ /**
+ * The stub version to return when the WindowManager Extensions is disabled
+ * @see WindowManager#hasWindowExtensionsEnabled
+ */
+ private static class DisabledWindowExtensions implements WindowExtensions {
+ @Override
+ public int getVendorApiLevel() {
+ return 0;
+ }
+
+ @Nullable
+ @Override
+ public WindowLayoutComponent getWindowLayoutComponent() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public ActivityEmbeddingComponent getActivityEmbeddingComponent() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public WindowAreaComponent getWindowAreaComponent() {
+ return null;
+ }
+ }
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
index b315f94b5d00..a3d2d7f4dcdf 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java
@@ -16,10 +16,11 @@
package androidx.window.extensions.area;
-import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
+import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE_IDENTIFIER;
import android.app.Activity;
import android.content.Context;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateRequest;
import android.hardware.display.DisplayManager;
@@ -40,6 +41,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
+import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -79,7 +81,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
private int mRearDisplaySessionStatus = WindowAreaComponent.SESSION_STATE_INACTIVE;
@GuardedBy("mLock")
- private int mCurrentDeviceState = INVALID_DEVICE_STATE;
+ private int mCurrentDeviceState = INVALID_DEVICE_STATE_IDENTIFIER;
@GuardedBy("mLock")
private int[] mCurrentSupportedDeviceStates;
@@ -101,7 +103,9 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
mDisplayManager = context.getSystemService(DisplayManager.class);
mExecutor = context.getMainExecutor();
- mCurrentSupportedDeviceStates = mDeviceStateManager.getSupportedStates();
+ // TODO(b/329436166): Update the usage of device state manager API's
+ mCurrentSupportedDeviceStates = getSupportedStateIdentifiers(
+ mDeviceStateManager.getSupportedDeviceStates());
mFoldedDeviceStates = context.getResources().getIntArray(
R.array.config_foldedDeviceStates);
@@ -143,7 +147,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
mRearDisplayStatusListeners.add(consumer);
// If current device state is still invalid, the initial value has not been provided.
- if (mCurrentDeviceState == INVALID_DEVICE_STATE) {
+ if (mCurrentDeviceState == INVALID_DEVICE_STATE_IDENTIFIER) {
return;
}
consumer.accept(getCurrentRearDisplayModeStatus());
@@ -308,7 +312,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
mRearDisplayPresentationStatusListeners.add(consumer);
// If current device state is still invalid, the initial value has not been provided
- if (mCurrentDeviceState == INVALID_DEVICE_STATE) {
+ if (mCurrentDeviceState == INVALID_DEVICE_STATE_IDENTIFIER) {
return;
}
@WindowAreaStatus int currentStatus = getCurrentRearDisplayPresentationModeStatus();
@@ -446,9 +450,10 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
}
@Override
- public void onSupportedStatesChanged(int[] supportedStates) {
+ public void onSupportedStatesChanged(@NonNull List<DeviceState> supportedStates) {
synchronized (mLock) {
- mCurrentSupportedDeviceStates = supportedStates;
+ // TODO(b/329436166): Update the usage of device state manager API's
+ mCurrentSupportedDeviceStates = getSupportedStateIdentifiers(supportedStates);
updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
updateRearDisplayPresentationStatusListeners(
getCurrentRearDisplayPresentationModeStatus());
@@ -456,9 +461,10 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
}
@Override
- public void onStateChanged(int state) {
+ public void onDeviceStateChanged(@NonNull DeviceState state) {
synchronized (mLock) {
- mCurrentDeviceState = state;
+ // TODO(b/329436166): Update the usage of device state manager API's
+ mCurrentDeviceState = state.getIdentifier();
updateRearDisplayStatusListeners(getCurrentRearDisplayModeStatus());
updateRearDisplayPresentationStatusListeners(
getCurrentRearDisplayPresentationModeStatus());
@@ -467,7 +473,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
@GuardedBy("mLock")
private int getCurrentRearDisplayModeStatus() {
- if (mRearDisplayState == INVALID_DEVICE_STATE) {
+ if (mRearDisplayState == INVALID_DEVICE_STATE_IDENTIFIER) {
return WindowAreaComponent.STATUS_UNSUPPORTED;
}
@@ -482,6 +488,15 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
return WindowAreaComponent.STATUS_AVAILABLE;
}
+ // TODO(b/329436166): Remove and update the usage of device state manager API's
+ private int[] getSupportedStateIdentifiers(@NonNull List<DeviceState> states) {
+ int[] identifiers = new int[states.size()];
+ for (int i = 0; i < states.size(); i++) {
+ identifiers[i] = states.get(i).getIdentifier();
+ }
+ return identifiers;
+ }
+
/**
* Helper method to determine if a rear display session is currently active by checking
* if the current device state is that which corresponds to {@code mRearDisplayState}.
@@ -495,7 +510,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
@GuardedBy("mLock")
private void updateRearDisplayStatusListeners(@WindowAreaStatus int windowAreaStatus) {
- if (mRearDisplayState == INVALID_DEVICE_STATE) {
+ if (mRearDisplayState == INVALID_DEVICE_STATE_IDENTIFIER) {
return;
}
synchronized (mLock) {
@@ -507,7 +522,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
@GuardedBy("mLock")
private int getCurrentRearDisplayPresentationModeStatus() {
- if (mConcurrentDisplayState == INVALID_DEVICE_STATE) {
+ if (mConcurrentDisplayState == INVALID_DEVICE_STATE_IDENTIFIER) {
return WindowAreaComponent.STATUS_UNSUPPORTED;
}
@@ -530,7 +545,7 @@ public class WindowAreaComponentImpl implements WindowAreaComponent,
@GuardedBy("mLock")
private void updateRearDisplayPresentationStatusListeners(
@WindowAreaStatus int windowAreaStatus) {
- if (mConcurrentDisplayState == INVALID_DEVICE_STATE) {
+ if (mConcurrentDisplayState == INVALID_DEVICE_STATE_IDENTIFIER) {
return;
}
RearDisplayPresentationStatus consumerValue = new RearDisplayPresentationStatus(
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
new file mode 100644
index 000000000000..290fefa5abfa
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -0,0 +1,1413 @@
+/*
+ * 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.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;
+import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_DECOR_SURFACE_BOOSTED;
+
+import static androidx.window.extensions.embedding.DividerAttributes.RATIO_SYSTEM_DEFAULT;
+import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_SYSTEM_DEFAULT;
+import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityThread;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Color;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.RotateDrawable;
+import android.hardware.display.DisplayManager;
+import android.os.IBinder;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.WindowlessWindowManager;
+import android.view.animation.PathInterpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.window.InputTransferToken;
+import android.window.TaskFragmentOperation;
+import android.window.TaskFragmentParentInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.window.extensions.core.util.function.Consumer;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Manages the rendering and interaction of the divider.
+ */
+class DividerPresenter implements View.OnTouchListener {
+ static final float RATIO_EXPANDED_PRIMARY = 1.0f;
+ static final float RATIO_EXPANDED_SECONDARY = 0.0f;
+ private static final String WINDOW_NAME = "AE Divider";
+ private static final int VEIL_LAYER = 0;
+ private static final int DIVIDER_LAYER = 1;
+
+ // TODO(b/327067596) Update based on UX guidance.
+ private static final Color DEFAULT_PRIMARY_VEIL_COLOR = Color.valueOf(Color.BLACK);
+ private static final Color DEFAULT_SECONDARY_VEIL_COLOR = Color.valueOf(Color.GRAY);
+ @VisibleForTesting
+ static final float DEFAULT_MIN_RATIO = 0.35f;
+ @VisibleForTesting
+ static final float DEFAULT_MAX_RATIO = 0.65f;
+ @VisibleForTesting
+ static final int DEFAULT_DIVIDER_WIDTH_DP = 24;
+
+ @VisibleForTesting
+ static final PathInterpolator FLING_ANIMATION_INTERPOLATOR =
+ new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+ @VisibleForTesting
+ static final int FLING_ANIMATION_DURATION = 250;
+ @VisibleForTesting
+ static final int MIN_DISMISS_VELOCITY_DP_PER_SECOND = 600;
+ @VisibleForTesting
+ static final int MIN_FLING_VELOCITY_DP_PER_SECOND = 400;
+
+ private final int mTaskId;
+
+ @NonNull
+ private final Object mLock = new Object();
+
+ @NonNull
+ private final DragEventCallback mDragEventCallback;
+
+ @NonNull
+ private final Executor mCallbackExecutor;
+
+ /**
+ * The VelocityTracker of the divider, used to track the dragging velocity. This field is
+ * {@code null} until dragging starts.
+ */
+ @GuardedBy("mLock")
+ @Nullable
+ VelocityTracker mVelocityTracker;
+
+ /**
+ * The {@link Properties} of the divider. This field is {@code null} when no divider should be
+ * drawn, e.g. when the split doesn't have {@link DividerAttributes} or when the decor surface
+ * is not available.
+ */
+ @GuardedBy("mLock")
+ @Nullable
+ @VisibleForTesting
+ Properties mProperties;
+
+ /**
+ * The {@link Renderer} of the divider. This field is {@code null} when no divider should be
+ * drawn, i.e. when {@link #mProperties} is {@code null}. The {@link Renderer} is recreated or
+ * updated when {@link #mProperties} is changed.
+ */
+ @GuardedBy("mLock")
+ @Nullable
+ @VisibleForTesting
+ Renderer mRenderer;
+
+ /**
+ * The owner TaskFragment token of the decor surface. The decor surface is placed right above
+ * the owner TaskFragment surface and is removed if the owner TaskFragment is destroyed.
+ */
+ @GuardedBy("mLock")
+ @Nullable
+ @VisibleForTesting
+ IBinder mDecorSurfaceOwner;
+
+ /**
+ * The current divider position relative to the Task bounds. For vertical split (left-to-right
+ * or right-to-left), it is the x coordinate in the task window, and for horizontal split
+ * (top-to-bottom or bottom-to-top), it is the y coordinate in the task window.
+ */
+ @GuardedBy("mLock")
+ private int mDividerPosition;
+
+ DividerPresenter(int taskId, @NonNull DragEventCallback dragEventCallback,
+ @NonNull Executor callbackExecutor) {
+ mTaskId = taskId;
+ mDragEventCallback = dragEventCallback;
+ mCallbackExecutor = callbackExecutor;
+ }
+
+ /** Updates the divider when external conditions are changed. */
+ void updateDivider(
+ @NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentParentInfo parentInfo,
+ @Nullable SplitContainer topSplitContainer) {
+ if (!Flags.activityEmbeddingInteractiveDividerFlag()) {
+ return;
+ }
+
+ synchronized (mLock) {
+ // Clean up the decor surface if top SplitContainer is null.
+ if (topSplitContainer == null) {
+ removeDecorSurfaceAndDivider(wct);
+ return;
+ }
+
+ final SplitAttributes splitAttributes = topSplitContainer.getCurrentSplitAttributes();
+ final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes();
+
+ // Clean up the decor surface if DividerAttributes is null.
+ if (dividerAttributes == null) {
+ removeDecorSurfaceAndDivider(wct);
+ return;
+ }
+
+ // At this point, a divider is required.
+ final TaskFragmentContainer primaryContainer =
+ topSplitContainer.getPrimaryContainer();
+ final TaskFragmentContainer secondaryContainer =
+ topSplitContainer.getSecondaryContainer();
+
+ // Create the decor surface if one is not available yet.
+ final SurfaceControl decorSurface = parentInfo.getDecorSurface();
+ if (decorSurface == null) {
+ // Clean up when the decor surface is currently unavailable.
+ removeDivider();
+ // Request to create the decor surface
+ createOrMoveDecorSurfaceLocked(wct, primaryContainer);
+ return;
+ }
+
+ // Update the decor surface owner if needed.
+ boolean isDraggableExpandType =
+ SplitAttributesHelper.isDraggableExpandType(splitAttributes);
+ final TaskFragmentContainer decorSurfaceOwnerContainer =
+ isDraggableExpandType ? secondaryContainer : primaryContainer;
+
+ if (!Objects.equals(
+ mDecorSurfaceOwner, decorSurfaceOwnerContainer.getTaskFragmentToken())) {
+ createOrMoveDecorSurfaceLocked(wct, decorSurfaceOwnerContainer);
+ }
+
+ final Configuration parentConfiguration = parentInfo.getConfiguration();
+ final Rect taskBounds = parentConfiguration.windowConfiguration.getBounds();
+ final boolean isVerticalSplit = isVerticalSplit(splitAttributes);
+ final boolean isReversedLayout = isReversedLayout(splitAttributes, parentConfiguration);
+ final int dividerWidthPx = getDividerWidthPx(dividerAttributes);
+
+ updateProperties(
+ new Properties(
+ parentConfiguration,
+ dividerAttributes,
+ decorSurface,
+ getInitialDividerPosition(
+ primaryContainer, secondaryContainer, taskBounds,
+ dividerWidthPx, isDraggableExpandType, isVerticalSplit,
+ isReversedLayout),
+ isVerticalSplit,
+ isReversedLayout,
+ parentInfo.getDisplayId(),
+ isDraggableExpandType,
+ primaryContainer,
+ secondaryContainer)
+ );
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void updateProperties(@NonNull Properties properties) {
+ if (Properties.equalsForDivider(mProperties, properties)) {
+ return;
+ }
+ final Properties previousProperties = mProperties;
+ mProperties = properties;
+
+ if (mRenderer == null) {
+ // Create a new renderer when a renderer doesn't exist yet.
+ mRenderer = new Renderer(mProperties, this);
+ } else if (!Properties.areSameSurfaces(
+ previousProperties.mDecorSurface, mProperties.mDecorSurface)
+ || previousProperties.mDisplayId != mProperties.mDisplayId) {
+ // Release and recreate the renderer if the decor surface or the display has changed.
+ mRenderer.release();
+ mRenderer = new Renderer(mProperties, this);
+ } else {
+ // Otherwise, update the renderer for the new properties.
+ mRenderer.update(mProperties);
+ }
+ }
+
+ /**
+ * Returns the window background color of the top activity in the container if set, or the
+ * default color if the background color of the top activity is unavailable.
+ */
+ @VisibleForTesting
+ @NonNull
+ static Color getContainerBackgroundColor(
+ @NonNull TaskFragmentContainer container, @NonNull Color defaultColor) {
+ final Activity activity = container.getTopNonFinishingActivity();
+ if (activity == null) {
+ // This can happen when the activities in the container are from a different process.
+ // TODO(b/340984203) Report whether the top activity is in the same process. Use default
+ // color if not.
+ return defaultColor;
+ }
+
+ final Drawable drawable = activity.getWindow().getDecorView().getBackground();
+ if (drawable instanceof ColorDrawable colorDrawable) {
+ return Color.valueOf(colorDrawable.getColor());
+ }
+ return defaultColor;
+ }
+
+ /**
+ * Creates a decor surface for the TaskFragment if no decor surface exists, or changes the owner
+ * of the existing decor surface to be the specified TaskFragment.
+ *
+ * See {@link TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE}.
+ */
+ void createOrMoveDecorSurface(
+ @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) {
+ synchronized (mLock) {
+ createOrMoveDecorSurfaceLocked(wct, container);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void createOrMoveDecorSurfaceLocked(
+ @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) {
+ mDecorSurfaceOwner = container.getTaskFragmentToken();
+ final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+ OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE)
+ .build();
+ wct.addTaskFragmentOperation(mDecorSurfaceOwner, operation);
+ }
+
+ @GuardedBy("mLock")
+ private void removeDecorSurfaceAndDivider(@NonNull WindowContainerTransaction wct) {
+ if (mDecorSurfaceOwner != null) {
+ final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+ OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE)
+ .build();
+ wct.addTaskFragmentOperation(mDecorSurfaceOwner, operation);
+ mDecorSurfaceOwner = null;
+ }
+ removeDivider();
+ }
+
+ @GuardedBy("mLock")
+ private void removeDivider() {
+ if (mRenderer != null) {
+ mRenderer.release();
+ }
+ mProperties = null;
+ mRenderer = null;
+ }
+
+ @VisibleForTesting
+ static int getInitialDividerPosition(
+ @NonNull TaskFragmentContainer primaryContainer,
+ @NonNull TaskFragmentContainer secondaryContainer,
+ @NonNull Rect taskBounds,
+ int dividerWidthPx,
+ boolean isDraggableExpandType,
+ boolean isVerticalSplit,
+ boolean isReversedLayout) {
+ if (isDraggableExpandType) {
+ // If the secondary container is fully expanded by dragging the divider, we display the
+ // divider on the edge.
+ final int fullyExpandedPosition = isVerticalSplit
+ ? taskBounds.width() - dividerWidthPx
+ : taskBounds.height() - dividerWidthPx;
+ return isReversedLayout ? fullyExpandedPosition : 0;
+ } else {
+ final Rect primaryBounds = primaryContainer.getLastRequestedBounds();
+ final Rect secondaryBounds = secondaryContainer.getLastRequestedBounds();
+ return isVerticalSplit
+ ? Math.min(primaryBounds.right, secondaryBounds.right)
+ : Math.min(primaryBounds.bottom, secondaryBounds.bottom);
+ }
+ }
+
+ private static boolean isVerticalSplit(@NonNull SplitAttributes splitAttributes) {
+ final int layoutDirection = splitAttributes.getLayoutDirection();
+ switch (layoutDirection) {
+ case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT:
+ case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT:
+ case SplitAttributes.LayoutDirection.LOCALE:
+ return true;
+ case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM:
+ case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP:
+ return false;
+ default:
+ throw new IllegalArgumentException("Invalid layout direction:" + layoutDirection);
+ }
+ }
+
+ private static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) {
+ int dividerWidthDp = dividerAttributes.getWidthDp();
+ return convertDpToPixel(dividerWidthDp);
+ }
+
+ private static int convertDpToPixel(int dp) {
+ // TODO(b/329193115) support divider on secondary display
+ final Context applicationContext = ActivityThread.currentActivityThread().getApplication();
+
+ return (int) TypedValue.applyDimension(
+ COMPLEX_UNIT_DIP,
+ dp,
+ applicationContext.getResources().getDisplayMetrics());
+ }
+
+ private static float getDisplayDensity() {
+ // TODO(b/329193115) support divider on secondary display
+ final Context applicationContext =
+ ActivityThread.currentActivityThread().getApplication();
+ return applicationContext.getResources().getDisplayMetrics().density;
+ }
+
+ /**
+ * Returns the container bound offset that is a result of the presence of a divider.
+ *
+ * The offset is the relative position change for the container edge that is next to the divider
+ * due to the presence of the divider. The value could be negative or positive depending on the
+ * container position. Positive values indicate that the edge is shifting towards the right
+ * (or bottom) and negative values indicate that the edge is shifting towards the left (or top).
+ *
+ * @param splitAttributes the {@link SplitAttributes} of the split container that we want to
+ * compute bounds offset.
+ * @param position the position of the container in the split that we want to compute
+ * bounds offset for.
+ * @return the bounds offset in pixels.
+ */
+ static int getBoundsOffsetForDivider(
+ @NonNull SplitAttributes splitAttributes,
+ @SplitPresenter.ContainerPosition int position) {
+ if (!Flags.activityEmbeddingInteractiveDividerFlag()) {
+ return 0;
+ }
+ final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes();
+ if (dividerAttributes == null) {
+ return 0;
+ }
+ final int dividerWidthPx = getDividerWidthPx(dividerAttributes);
+ return getBoundsOffsetForDivider(
+ dividerWidthPx,
+ splitAttributes.getSplitType(),
+ position);
+ }
+
+ @VisibleForTesting
+ static int getBoundsOffsetForDivider(
+ int dividerWidthPx,
+ @NonNull SplitType splitType,
+ @SplitPresenter.ContainerPosition int position) {
+ if (splitType instanceof ExpandContainersSplitType) {
+ // No divider offset is needed for the ExpandContainersSplitType.
+ return 0;
+ }
+ int primaryOffset;
+ if (splitType instanceof final RatioSplitType splitRatio) {
+ // When a divider is present, both containers shrink by an amount proportional to their
+ // split ratio and sum to the width of the divider, so that the ending sizing of the
+ // containers still maintain the same ratio.
+ primaryOffset = (int) (dividerWidthPx * splitRatio.getRatio());
+ } else {
+ // Hinge split type (and other future split types) will have the divider width equally
+ // distributed to both containers.
+ primaryOffset = dividerWidthPx / 2;
+ }
+ final int secondaryOffset = dividerWidthPx - primaryOffset;
+ switch (position) {
+ case CONTAINER_POSITION_LEFT:
+ case CONTAINER_POSITION_TOP:
+ return -primaryOffset;
+ case CONTAINER_POSITION_RIGHT:
+ case CONTAINER_POSITION_BOTTOM:
+ return secondaryOffset;
+ default:
+ throw new IllegalArgumentException("Unknown position:" + position);
+ }
+ }
+
+ /**
+ * Sanitizes and sets default values in the {@link DividerAttributes}.
+ *
+ * Unset values will be set with system default values. See
+ * {@link DividerAttributes#WIDTH_SYSTEM_DEFAULT} and
+ * {@link DividerAttributes#RATIO_SYSTEM_DEFAULT}.
+ *
+ * @param dividerAttributes input {@link DividerAttributes}
+ * @return a {@link DividerAttributes} that has all values properly set.
+ */
+ @Nullable
+ static DividerAttributes sanitizeDividerAttributes(
+ @Nullable DividerAttributes dividerAttributes) {
+ if (dividerAttributes == null) {
+ return null;
+ }
+ int widthDp = dividerAttributes.getWidthDp();
+ float minRatio = dividerAttributes.getPrimaryMinRatio();
+ float maxRatio = dividerAttributes.getPrimaryMaxRatio();
+
+ if (widthDp == WIDTH_SYSTEM_DEFAULT) {
+ widthDp = DEFAULT_DIVIDER_WIDTH_DP;
+ }
+
+ if (dividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) {
+ // Update minRatio and maxRatio only when it is a draggable divider.
+ if (minRatio == RATIO_SYSTEM_DEFAULT) {
+ minRatio = DEFAULT_MIN_RATIO;
+ }
+ if (maxRatio == RATIO_SYSTEM_DEFAULT) {
+ maxRatio = DEFAULT_MAX_RATIO;
+ }
+ }
+
+ return new DividerAttributes.Builder(dividerAttributes)
+ .setWidthDp(widthDp)
+ .setPrimaryMinRatio(minRatio)
+ .setPrimaryMaxRatio(maxRatio)
+ .build();
+ }
+
+ @Override
+ public boolean onTouch(@NonNull View view, @NonNull MotionEvent event) {
+ synchronized (mLock) {
+ if (mProperties != null && mRenderer != null) {
+ final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+ mDividerPosition = calculateDividerPosition(
+ event, taskBounds, mProperties.mDividerWidthPx,
+ mProperties.mDividerAttributes, mProperties.mIsVerticalSplit,
+ calculateMinPosition(), calculateMaxPosition());
+ mRenderer.setDividerPosition(mDividerPosition);
+
+ // Convert to use screen-based coordinates to prevent lost track of motion events
+ // while moving divider bar and calculating dragging velocity.
+ event.setLocation(event.getRawX(), event.getRawY());
+ final int action = event.getAction() & MotionEvent.ACTION_MASK;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ onStartDragging(event);
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ onFinishDragging(event);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ onDrag(event);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ // Returns true to prevent the default button click callback. The button pressed state is
+ // set/unset when starting/finishing dragging.
+ return true;
+ }
+
+ @GuardedBy("mLock")
+ private void onStartDragging(@NonNull MotionEvent event) {
+ mVelocityTracker = VelocityTracker.obtain();
+ mVelocityTracker.addMovement(event);
+
+ mRenderer.mIsDragging = true;
+ mRenderer.mDragHandle.setPressed(mRenderer.mIsDragging);
+ mRenderer.updateSurface();
+
+ // Veil visibility change should be applied together with the surface boost transaction in
+ // the wct.
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ mRenderer.showVeils(t);
+
+ // Callbacks must be executed on the executor to release mLock and prevent deadlocks.
+ mCallbackExecutor.execute(() -> {
+ mDragEventCallback.onStartDragging(
+ wct -> {
+ synchronized (mLock) {
+ setDecorSurfaceBoosted(wct, mDecorSurfaceOwner, true /* boosted */, t);
+ }
+ });
+ });
+ }
+
+ @GuardedBy("mLock")
+ private void onDrag(@NonNull MotionEvent event) {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.addMovement(event);
+ }
+ mRenderer.updateSurface();
+ }
+
+ @GuardedBy("mLock")
+ private void onFinishDragging(@NonNull MotionEvent event) {
+ float velocity = 0.0f;
+ if (mVelocityTracker != null) {
+ mVelocityTracker.addMovement(event);
+ mVelocityTracker.computeCurrentVelocity(1000 /* units */);
+ velocity = mProperties.mIsVerticalSplit
+ ? mVelocityTracker.getXVelocity()
+ : mVelocityTracker.getYVelocity();
+ mVelocityTracker.recycle();
+ }
+
+ final int prevDividerPosition = mDividerPosition;
+ mDividerPosition = dividerPositionForSnapPoints(mDividerPosition, velocity);
+ if (mDividerPosition != prevDividerPosition) {
+ ValueAnimator animator = getFlingAnimator(prevDividerPosition, mDividerPosition);
+ animator.start();
+ } else {
+ onDraggingEnd();
+ }
+ }
+
+ @GuardedBy("mLock")
+ @NonNull
+ @VisibleForTesting
+ ValueAnimator getFlingAnimator(int prevDividerPosition, int snappedDividerPosition) {
+ final ValueAnimator animator =
+ getValueAnimator(prevDividerPosition, snappedDividerPosition);
+ animator.addUpdateListener(animation -> {
+ synchronized (mLock) {
+ updateDividerPosition((int) animation.getAnimatedValue());
+ }
+ });
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ synchronized (mLock) {
+ onDraggingEnd();
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ synchronized (mLock) {
+ onDraggingEnd();
+ }
+ }
+ });
+ return animator;
+ }
+
+ @VisibleForTesting
+ static ValueAnimator getValueAnimator(int prevDividerPosition, int snappedDividerPosition) {
+ ValueAnimator animator = ValueAnimator
+ .ofInt(prevDividerPosition, snappedDividerPosition)
+ .setDuration(FLING_ANIMATION_DURATION);
+ animator.setInterpolator(FLING_ANIMATION_INTERPOLATOR);
+ return animator;
+ }
+
+ @GuardedBy("mLock")
+ private void updateDividerPosition(int position) {
+ mRenderer.setDividerPosition(position);
+ mRenderer.updateSurface();
+ }
+
+ @GuardedBy("mLock")
+ private void onDraggingEnd() {
+ // Veil visibility change should be applied together with the surface boost transaction in
+ // the wct.
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ mRenderer.hideVeils(t);
+
+ // Callbacks must be executed on the executor to release mLock and prevent deadlocks.
+ // mDecorSurfaceOwner may change between here and when the callback is executed,
+ // e.g. when the decor surface owner becomes the secondary container when it is expanded to
+ // fullscreen.
+ mCallbackExecutor.execute(() -> {
+ mDragEventCallback.onFinishDragging(
+ mTaskId,
+ wct -> {
+ synchronized (mLock) {
+ setDecorSurfaceBoosted(wct, mDecorSurfaceOwner, false /* boosted */, t);
+ }
+ });
+ });
+ mRenderer.mIsDragging = false;
+ mRenderer.mDragHandle.setPressed(mRenderer.mIsDragging);
+ }
+
+ /**
+ * Returns the divider position adjusted for the min max ratio and fullscreen expansion.
+ * The adjusted divider position is in the range of [minPosition, maxPosition] for a split, 0
+ * for expanded right (bottom) container, or task width (height) minus the divider width for
+ * expanded left (top) container.
+ */
+ @GuardedBy("mLock")
+ private int dividerPositionForSnapPoints(int dividerPosition, float velocity) {
+ final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+ final int minPosition = calculateMinPosition();
+ final int maxPosition = calculateMaxPosition();
+ final int fullyExpandedPosition = mProperties.mIsVerticalSplit
+ ? taskBounds.width() - mProperties.mDividerWidthPx
+ : taskBounds.height() - mProperties.mDividerWidthPx;
+
+ 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 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 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 (Math.abs(velocity) >= minFlingVelocityPxPerSecond) {
+ return dividerPositionForFling(
+ dividerPosition, minPosition, maxPosition, velocity);
+ }
+ if (dividerPosition >= minPosition && dividerPosition <= maxPosition) {
+ return dividerPosition;
+ }
+ 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 dividerPosition > minPosition ? maxPosition : minPosition;
+ }
+ }
+
+ /**
+ * 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;
+ for (int position : possiblePositions) {
+ float distance = Math.abs(dividerPosition - position);
+ if (distance < minDistance) {
+ snappedPosition = position;
+ minDistance = distance;
+ }
+ }
+ return snappedPosition;
+ }
+
+ private static void setDecorSurfaceBoosted(
+ @NonNull WindowContainerTransaction wct,
+ @Nullable IBinder decorSurfaceOwner,
+ boolean boosted,
+ @NonNull SurfaceControl.Transaction clientTransaction) {
+ if (decorSurfaceOwner == null) {
+ return;
+ }
+ wct.addTaskFragmentOperation(
+ decorSurfaceOwner,
+ new TaskFragmentOperation.Builder(OP_TYPE_SET_DECOR_SURFACE_BOOSTED)
+ .setBooleanValue(boosted)
+ .setSurfaceTransaction(clientTransaction)
+ .build()
+ );
+ }
+
+ /** Calculates the new divider position based on the touch event and divider attributes. */
+ @VisibleForTesting
+ static int calculateDividerPosition(@NonNull MotionEvent event, @NonNull Rect taskBounds,
+ int dividerWidthPx, @NonNull DividerAttributes dividerAttributes,
+ boolean isVerticalSplit, int minPosition, int maxPosition) {
+ // The touch event is in display space. Converting it into the task window space.
+ final int touchPositionInTaskSpace = isVerticalSplit
+ ? (int) (event.getRawX()) - taskBounds.left
+ : (int) (event.getRawY()) - taskBounds.top;
+
+ // Assuming that the touch position is at the center of the divider bar, so the divider
+ // position is offset by half of the divider width.
+ int dividerPosition = touchPositionInTaskSpace - dividerWidthPx / 2;
+
+ // If dragging to fullscreen is not allowed, limit the divider position to the min and max
+ // ratios set in DividerAttributes. Otherwise, dragging beyond the min and max ratios is
+ // temporarily allowed and the final ratio will be adjusted in onFinishDragging.
+ if (!isDraggingToFullscreenAllowed(dividerAttributes)) {
+ dividerPosition = Math.clamp(dividerPosition, minPosition, maxPosition);
+ }
+ return dividerPosition;
+ }
+
+ @GuardedBy("mLock")
+ private int calculateMinPosition() {
+ return calculateMinPosition(
+ mProperties.mConfiguration.windowConfiguration.getBounds(),
+ mProperties.mDividerWidthPx, mProperties.mDividerAttributes,
+ mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout);
+ }
+
+ @GuardedBy("mLock")
+ private int calculateMaxPosition() {
+ return calculateMaxPosition(
+ mProperties.mConfiguration.windowConfiguration.getBounds(),
+ mProperties.mDividerWidthPx, mProperties.mDividerAttributes,
+ mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout);
+ }
+
+ /** Calculates the min position of the divider that the user is allowed to drag to. */
+ @VisibleForTesting
+ static int calculateMinPosition(@NonNull Rect taskBounds, int dividerWidthPx,
+ @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit,
+ boolean isReversedLayout) {
+ // The usable size is the task window size minus the divider bar width. This is shared
+ // between the primary and secondary containers based on the split ratio.
+ final int usableSize = isVerticalSplit
+ ? taskBounds.width() - dividerWidthPx
+ : taskBounds.height() - dividerWidthPx;
+ return (int) (isReversedLayout
+ ? usableSize - usableSize * dividerAttributes.getPrimaryMaxRatio()
+ : usableSize * dividerAttributes.getPrimaryMinRatio());
+ }
+
+ /** Calculates the max position of the divider that the user is allowed to drag to. */
+ @VisibleForTesting
+ static int calculateMaxPosition(@NonNull Rect taskBounds, int dividerWidthPx,
+ @NonNull DividerAttributes dividerAttributes, boolean isVerticalSplit,
+ boolean isReversedLayout) {
+ // The usable size is the task window size minus the divider bar width. This is shared
+ // between the primary and secondary containers based on the split ratio.
+ final int usableSize = isVerticalSplit
+ ? taskBounds.width() - dividerWidthPx
+ : taskBounds.height() - dividerWidthPx;
+ return (int) (isReversedLayout
+ ? usableSize - usableSize * dividerAttributes.getPrimaryMinRatio()
+ : usableSize * dividerAttributes.getPrimaryMaxRatio());
+ }
+
+ /**
+ * Returns the new split ratio of the {@link SplitContainer} based on the current divider
+ * position.
+ */
+ float calculateNewSplitRatio() {
+ synchronized (mLock) {
+ return calculateNewSplitRatio(
+ mDividerPosition,
+ mProperties.mConfiguration.windowConfiguration.getBounds(),
+ mProperties.mDividerWidthPx,
+ mProperties.mIsVerticalSplit,
+ mProperties.mIsReversedLayout,
+ calculateMinPosition(),
+ calculateMaxPosition(),
+ isDraggingToFullscreenAllowed(mProperties.mDividerAttributes));
+ }
+ }
+
+ private static boolean isDraggingToFullscreenAllowed(
+ @NonNull DividerAttributes dividerAttributes) {
+ // TODO(b/293654166) Use DividerAttributes.isDraggingToFullscreenAllowed when extension is
+ // updated to v7.
+ return false;
+ }
+
+ /**
+ * Returns the new split ratio of the {@link SplitContainer} based on the current divider
+ * position.
+ *
+ * @param dividerPosition the divider position. See {@link #mDividerPosition}.
+ * @param taskBounds the task bounds
+ * @param dividerWidthPx the width of the divider in pixels.
+ * @param isVerticalSplit if {@code true}, the split is a vertical split. If {@code false}, the
+ * split is a horizontal split. See
+ * {@link #isVerticalSplit(SplitAttributes)}.
+ * @param isReversedLayout if {@code true}, the split layout is reversed, i.e. right-to-left or
+ * bottom-to-top. If {@code false}, the split is not reversed, i.e.
+ * left-to-right or top-to-bottom. See
+ * {@link SplitAttributesHelper#isReversedLayout}
+ * @return the computed split ratio of the primary container. If the primary container is fully
+ * expanded, {@link #RATIO_EXPANDED_PRIMARY} is returned. If the secondary container is fully
+ * expanded, {@link #RATIO_EXPANDED_SECONDARY} is returned.
+ */
+ @VisibleForTesting
+ static float calculateNewSplitRatio(
+ int dividerPosition,
+ @NonNull Rect taskBounds,
+ int dividerWidthPx,
+ boolean isVerticalSplit,
+ boolean isReversedLayout,
+ int minPosition,
+ int maxPosition,
+ boolean isDraggingToFullscreenAllowed) {
+
+ // Handle the fully expanded cases.
+ if (isDraggingToFullscreenAllowed) {
+ // The divider position is already adjusted by the snap algorithm in onFinishDragging.
+ // If the divider position is not in the range [minPosition, maxPosition], then one of
+ // the containers is fully expanded.
+ if (dividerPosition < minPosition) {
+ return isReversedLayout ? RATIO_EXPANDED_PRIMARY : RATIO_EXPANDED_SECONDARY;
+ }
+ if (dividerPosition > maxPosition) {
+ return isReversedLayout ? RATIO_EXPANDED_SECONDARY : RATIO_EXPANDED_PRIMARY;
+ }
+ } else {
+ dividerPosition = Math.clamp(dividerPosition, minPosition, maxPosition);
+ }
+
+ final int usableSize = isVerticalSplit
+ ? taskBounds.width() - dividerWidthPx
+ : taskBounds.height() - dividerWidthPx;
+
+ final float newRatio;
+ if (isVerticalSplit) {
+ final int newPrimaryWidth = isReversedLayout
+ ? taskBounds.width() - (dividerPosition + dividerWidthPx)
+ : dividerPosition;
+ newRatio = 1.0f * newPrimaryWidth / usableSize;
+ } else {
+ final int newPrimaryHeight = isReversedLayout
+ ? taskBounds.height() - (dividerPosition + dividerWidthPx)
+ : dividerPosition;
+ newRatio = 1.0f * newPrimaryHeight / usableSize;
+ }
+ return newRatio;
+ }
+
+ /** Callbacks for drag events */
+ interface DragEventCallback {
+ /**
+ * Called when the user starts dragging the divider. Callbacks are executed on
+ * {@link #mCallbackExecutor}.
+ *
+ * @param action additional action that should be applied to the
+ * {@link WindowContainerTransaction}
+ */
+ void onStartDragging(@NonNull Consumer<WindowContainerTransaction> action);
+
+ /**
+ * Called when the user finishes dragging the divider. Callbacks are executed on
+ * {@link #mCallbackExecutor}.
+ *
+ * @param taskId the Task id of the {@link TaskContainer} that this divider belongs to.
+ * @param action additional action that should be applied to the
+ * {@link WindowContainerTransaction}
+ */
+ void onFinishDragging(int taskId, @NonNull Consumer<WindowContainerTransaction> action);
+ }
+
+ /**
+ * Properties for the {@link DividerPresenter}. The rendering of the divider solely depends on
+ * these properties. When any value is updated, the divider is re-rendered. The Properties
+ * instance is created only when all the pre-conditions of drawing a divider are met.
+ */
+ @VisibleForTesting
+ static class Properties {
+ private static final int CONFIGURATION_MASK_FOR_DIVIDER =
+ CONFIG_DENSITY | CONFIG_WINDOW_CONFIGURATION | CONFIG_LAYOUT_DIRECTION;
+ @NonNull
+ private final Configuration mConfiguration;
+ @NonNull
+ private final DividerAttributes mDividerAttributes;
+ @NonNull
+ private final SurfaceControl mDecorSurface;
+
+ /** The initial position of the divider calculated based on container bounds. */
+ private final int mInitialDividerPosition;
+
+ /** Whether the split is vertical, such as left-to-right or right-to-left split. */
+ private final boolean mIsVerticalSplit;
+
+ private final int mDisplayId;
+ private final boolean mIsReversedLayout;
+ private final boolean mIsDraggableExpandType;
+ @NonNull
+ private final TaskFragmentContainer mPrimaryContainer;
+ @NonNull
+ private final TaskFragmentContainer mSecondaryContainer;
+ private final int mDividerWidthPx;
+
+ @VisibleForTesting
+ Properties(
+ @NonNull Configuration configuration,
+ @NonNull DividerAttributes dividerAttributes,
+ @NonNull SurfaceControl decorSurface,
+ int initialDividerPosition,
+ boolean isVerticalSplit,
+ boolean isReversedLayout,
+ int displayId,
+ boolean isDraggableExpandType,
+ @NonNull TaskFragmentContainer primaryContainer,
+ @NonNull TaskFragmentContainer secondaryContainer) {
+ mConfiguration = configuration;
+ mDividerAttributes = dividerAttributes;
+ mDecorSurface = decorSurface;
+ mInitialDividerPosition = initialDividerPosition;
+ mIsVerticalSplit = isVerticalSplit;
+ mIsReversedLayout = isReversedLayout;
+ mDisplayId = displayId;
+ mIsDraggableExpandType = isDraggableExpandType;
+ mPrimaryContainer = primaryContainer;
+ mSecondaryContainer = secondaryContainer;
+ mDividerWidthPx = getDividerWidthPx(dividerAttributes);
+ }
+
+ /**
+ * Compares whether two Properties objects are equal for rendering the divider. The
+ * Configuration is checked for rendering related fields, and other fields are checked for
+ * regular equality.
+ */
+ private static boolean equalsForDivider(@Nullable Properties a, @Nullable Properties b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null || b == null) {
+ return false;
+ }
+ return areSameSurfaces(a.mDecorSurface, b.mDecorSurface)
+ && Objects.equals(a.mDividerAttributes, b.mDividerAttributes)
+ && areConfigurationsEqualForDivider(a.mConfiguration, b.mConfiguration)
+ && a.mInitialDividerPosition == b.mInitialDividerPosition
+ && a.mIsVerticalSplit == b.mIsVerticalSplit
+ && a.mDisplayId == b.mDisplayId
+ && a.mIsReversedLayout == b.mIsReversedLayout
+ && a.mIsDraggableExpandType == b.mIsDraggableExpandType
+ && a.mPrimaryContainer == b.mPrimaryContainer
+ && a.mSecondaryContainer == b.mSecondaryContainer;
+ }
+
+ private static boolean areSameSurfaces(
+ @Nullable SurfaceControl sc1, @Nullable SurfaceControl sc2) {
+ if (sc1 == sc2) {
+ // If both are null or both refer to the same object.
+ return true;
+ }
+ if (sc1 == null || sc2 == null) {
+ return false;
+ }
+ return sc1.isSameSurface(sc2);
+ }
+
+ private static boolean areConfigurationsEqualForDivider(
+ @NonNull Configuration a, @NonNull Configuration b) {
+ final int diff = a.diff(b);
+ return (diff & CONFIGURATION_MASK_FOR_DIVIDER) == 0;
+ }
+ }
+
+ /**
+ * Handles the rendering of the divider. When the decor surface is updated, the renderer is
+ * recreated. When other fields in the Properties are changed, the renderer is updated.
+ */
+ @VisibleForTesting
+ static class Renderer {
+ @NonNull
+ private final SurfaceControl mDividerSurface;
+ @NonNull
+ private final WindowlessWindowManager mWindowlessWindowManager;
+ @NonNull
+ private final SurfaceControlViewHost mViewHost;
+ @NonNull
+ private final FrameLayout mDividerLayout;
+ @NonNull
+ private final View mDividerLine;
+ private View mDragHandle;
+ @NonNull
+ private final View.OnTouchListener mListener;
+ @NonNull
+ private Properties mProperties;
+ private int mHandleWidthPx;
+ @Nullable
+ private SurfaceControl mPrimaryVeil;
+ @Nullable
+ private SurfaceControl mSecondaryVeil;
+ private boolean mIsDragging;
+ private int mDividerPosition;
+ private int mDividerSurfaceWidthPx;
+
+ private Renderer(@NonNull Properties properties, @NonNull View.OnTouchListener listener) {
+ mProperties = properties;
+ mListener = listener;
+
+ mDividerSurface = createChildSurface("DividerSurface", true /* visible */);
+ mWindowlessWindowManager = new WindowlessWindowManager(
+ mProperties.mConfiguration,
+ mDividerSurface,
+ new InputTransferToken());
+
+ final Context context = ActivityThread.currentActivityThread().getApplication();
+ final DisplayManager displayManager = context.getSystemService(DisplayManager.class);
+ mViewHost = new SurfaceControlViewHost(
+ context, displayManager.getDisplay(mProperties.mDisplayId),
+ mWindowlessWindowManager, "DividerContainer");
+ mDividerLayout = new FrameLayout(context);
+ mDividerLine = new View(context);
+
+ update();
+ }
+
+ /** Updates the divider when properties are changed */
+ private void update(@NonNull Properties newProperties) {
+ mProperties = newProperties;
+ update();
+ }
+
+ /** Updates the divider when initializing or when properties are changed */
+ @VisibleForTesting
+ void update() {
+ mDividerPosition = mProperties.mInitialDividerPosition;
+ mWindowlessWindowManager.setConfiguration(mProperties.mConfiguration);
+
+ if (mProperties.mDividerAttributes.getDividerType()
+ == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) {
+ // TODO(b/329193115) support divider on secondary display
+ final Context context = ActivityThread.currentActivityThread().getApplication();
+ mHandleWidthPx = context.getResources().getDimensionPixelSize(
+ R.dimen.activity_embedding_divider_touch_target_width);
+ } else {
+ mHandleWidthPx = 0;
+ }
+
+ // TODO handle synchronization between surface transactions and WCT.
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ updateSurface(t);
+ updateLayout();
+ updateDivider(t);
+ t.apply();
+ }
+
+ @VisibleForTesting
+ void release() {
+ mViewHost.release();
+ // TODO handle synchronization between surface transactions and WCT.
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ t.remove(mDividerSurface);
+ removeVeils(t);
+ t.apply();
+ }
+
+ private void setDividerPosition(int dividerPosition) {
+ mDividerPosition = dividerPosition;
+ }
+
+ /**
+ * Updates the positions and crops of the divider surface and veil surfaces. This method
+ * should be called when {@link #mProperties} is changed or while dragging to update the
+ * position of the divider surface and the veil surfaces.
+ *
+ * This method applies the changes in a stand-alone surface transaction immediately.
+ */
+ private void updateSurface() {
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ updateSurface(t);
+ t.apply();
+ }
+
+ /**
+ * Updates the positions and crops of the divider surface and veil surfaces. This method
+ * should be called when {@link #mProperties} is changed or while dragging to update the
+ * position of the divider surface and the veil surfaces.
+ *
+ * This method applies the changes in the provided surface transaction and can be synced
+ * with other changes.
+ */
+ private void updateSurface(@NonNull SurfaceControl.Transaction t) {
+ final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+
+ int dividerSurfacePosition;
+ if (mProperties.mDividerAttributes.getDividerType()
+ == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) {
+ // When the divider drag handle width is larger than the divider width, the position
+ // of the divider surface is adjusted so that it is large enough to host both the
+ // divider line and the divider drag handle.
+ mDividerSurfaceWidthPx = Math.max(mProperties.mDividerWidthPx, mHandleWidthPx);
+ dividerSurfacePosition = mProperties.mIsReversedLayout
+ ? mDividerPosition
+ : mDividerPosition + mProperties.mDividerWidthPx - mDividerSurfaceWidthPx;
+ dividerSurfacePosition =
+ Math.clamp(dividerSurfacePosition, 0,
+ mProperties.mIsVerticalSplit
+ ? taskBounds.width() - mDividerSurfaceWidthPx
+ : taskBounds.height() - mDividerSurfaceWidthPx);
+ } else {
+ mDividerSurfaceWidthPx = mProperties.mDividerWidthPx;
+ dividerSurfacePosition = mDividerPosition;
+ }
+
+ if (mProperties.mIsVerticalSplit) {
+ t.setPosition(mDividerSurface, dividerSurfacePosition, 0.0f);
+ t.setWindowCrop(mDividerSurface, mDividerSurfaceWidthPx, taskBounds.height());
+ } else {
+ t.setPosition(mDividerSurface, 0.0f, dividerSurfacePosition);
+ t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerSurfaceWidthPx);
+ }
+
+ // Update divider line position in the surface
+ final int offset = mDividerPosition - dividerSurfacePosition;
+ mDividerLine.setX(mProperties.mIsVerticalSplit ? offset : 0);
+ mDividerLine.setY(mProperties.mIsVerticalSplit ? 0 : offset);
+
+ if (mIsDragging) {
+ updateVeils(t);
+ }
+ }
+
+ /**
+ * Updates the layout parameters of the layout used to host the divider. This method should
+ * be called only when {@link #mProperties} is changed. This should not be called while
+ * dragging, because the layout parameters are not changed during dragging.
+ */
+ private void updateLayout() {
+ final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+ final WindowManager.LayoutParams lp = mProperties.mIsVerticalSplit
+ ? new WindowManager.LayoutParams(
+ mDividerSurfaceWidthPx,
+ taskBounds.height(),
+ TYPE_APPLICATION_PANEL,
+ FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY,
+ PixelFormat.TRANSLUCENT)
+ : new WindowManager.LayoutParams(
+ taskBounds.width(),
+ mDividerSurfaceWidthPx,
+ TYPE_APPLICATION_PANEL,
+ 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);
+ }
+
+ /**
+ * Updates the UI component of the divider, including the drag handle and the veils. This
+ * method should be called only when {@link #mProperties} is changed. This should not be
+ * called while dragging, because the UI components are not changed during dragging and
+ * only their surface positions are changed.
+ */
+ private void updateDivider(@NonNull SurfaceControl.Transaction t) {
+ mDividerLayout.removeAllViews();
+ mDividerLayout.addView(mDividerLine);
+ if (mProperties.mIsDraggableExpandType && !mIsDragging) {
+ // If a container is fully expanded, the divider overlays on the expanded container.
+ mDividerLine.setBackgroundColor(Color.TRANSPARENT);
+ } else {
+ mDividerLine.setBackgroundColor(mProperties.mDividerAttributes.getDividerColor());
+ }
+ final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+ mDividerLine.setLayoutParams(
+ mProperties.mIsVerticalSplit
+ ? new FrameLayout.LayoutParams(
+ mProperties.mDividerWidthPx, taskBounds.height())
+ : new FrameLayout.LayoutParams(
+ taskBounds.width(), mProperties.mDividerWidthPx)
+ );
+ if (mProperties.mDividerAttributes.getDividerType()
+ == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) {
+ createVeils();
+ drawDragHandle();
+ } else {
+ removeVeils(t);
+ }
+ mViewHost.getView().invalidate();
+ }
+
+ private void drawDragHandle() {
+ final Context context = mDividerLayout.getContext();
+ final ImageButton button = new ImageButton(context);
+ final FrameLayout.LayoutParams params = mProperties.mIsVerticalSplit
+ ? new FrameLayout.LayoutParams(
+ context.getResources().getDimensionPixelSize(
+ R.dimen.activity_embedding_divider_touch_target_width),
+ context.getResources().getDimensionPixelSize(
+ R.dimen.activity_embedding_divider_touch_target_height))
+ : new FrameLayout.LayoutParams(
+ context.getResources().getDimensionPixelSize(
+ R.dimen.activity_embedding_divider_touch_target_height),
+ context.getResources().getDimensionPixelSize(
+ R.dimen.activity_embedding_divider_touch_target_width));
+ params.gravity = Gravity.CENTER;
+ button.setLayoutParams(params);
+ button.setBackgroundColor(Color.TRANSPARENT);
+
+ final Drawable handle = context.getResources().getDrawable(
+ R.drawable.activity_embedding_divider_handle, context.getTheme());
+ if (mProperties.mIsVerticalSplit) {
+ button.setImageDrawable(handle);
+ } else {
+ // Rotate the handle drawable
+ RotateDrawable rotatedHandle = new RotateDrawable();
+ rotatedHandle.setFromDegrees(90f);
+ rotatedHandle.setToDegrees(90f);
+ rotatedHandle.setPivotXRelative(true);
+ rotatedHandle.setPivotYRelative(true);
+ rotatedHandle.setPivotX(0.5f);
+ rotatedHandle.setPivotY(0.5f);
+ rotatedHandle.setLevel(1);
+ rotatedHandle.setDrawable(handle);
+
+ button.setImageDrawable(rotatedHandle);
+ }
+
+ button.setOnTouchListener(mListener);
+ mDragHandle = button;
+ mDividerLayout.addView(button);
+ }
+
+ @NonNull
+ private SurfaceControl createChildSurface(@NonNull String name, boolean visible) {
+ final Rect bounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+ return new SurfaceControl.Builder()
+ .setParent(mProperties.mDecorSurface)
+ .setName(name)
+ .setHidden(!visible)
+ .setCallsite("DividerManager.createChildSurface")
+ .setBufferSize(bounds.width(), bounds.height())
+ .setEffectLayer()
+ .build();
+ }
+
+ private void createVeils() {
+ if (mPrimaryVeil == null) {
+ mPrimaryVeil = createChildSurface("DividerPrimaryVeil", false /* visible */);
+ }
+ if (mSecondaryVeil == null) {
+ mSecondaryVeil = createChildSurface("DividerSecondaryVeil", false /* visible */);
+ }
+ }
+
+ private void removeVeils(@NonNull SurfaceControl.Transaction t) {
+ if (mPrimaryVeil != null) {
+ t.remove(mPrimaryVeil);
+ }
+ if (mSecondaryVeil != null) {
+ t.remove(mSecondaryVeil);
+ }
+ mPrimaryVeil = null;
+ mSecondaryVeil = null;
+ }
+
+ private void showVeils(@NonNull SurfaceControl.Transaction t) {
+ final Color primaryVeilColor = getContainerBackgroundColor(
+ mProperties.mPrimaryContainer, DEFAULT_PRIMARY_VEIL_COLOR);
+ final Color secondaryVeilColor = getContainerBackgroundColor(
+ mProperties.mSecondaryContainer, DEFAULT_SECONDARY_VEIL_COLOR);
+ t.setColor(mPrimaryVeil, colorToFloatArray(primaryVeilColor))
+ .setColor(mSecondaryVeil, colorToFloatArray(secondaryVeilColor))
+ .setLayer(mDividerSurface, DIVIDER_LAYER)
+ .setLayer(mPrimaryVeil, VEIL_LAYER)
+ .setLayer(mSecondaryVeil, VEIL_LAYER)
+ .setVisibility(mPrimaryVeil, true)
+ .setVisibility(mSecondaryVeil, true);
+ updateVeils(t);
+ }
+
+ private void hideVeils(@NonNull SurfaceControl.Transaction t) {
+ t.setVisibility(mPrimaryVeil, false).setVisibility(mSecondaryVeil, false);
+ }
+
+ private void updateVeils(@NonNull SurfaceControl.Transaction t) {
+ final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds();
+
+ // Relative bounds of the primary and secondary containers in the Task.
+ Rect primaryBounds;
+ Rect secondaryBounds;
+ if (mProperties.mIsVerticalSplit) {
+ final Rect boundsLeft = new Rect(0, 0, mDividerPosition, taskBounds.height());
+ final Rect boundsRight = new Rect(mDividerPosition + mProperties.mDividerWidthPx, 0,
+ taskBounds.width(), taskBounds.height());
+ primaryBounds = mProperties.mIsReversedLayout ? boundsRight : boundsLeft;
+ secondaryBounds = mProperties.mIsReversedLayout ? boundsLeft : boundsRight;
+ } else {
+ final Rect boundsTop = new Rect(0, 0, taskBounds.width(), mDividerPosition);
+ final Rect boundsBottom = new Rect(
+ 0, mDividerPosition + mProperties.mDividerWidthPx,
+ taskBounds.width(), taskBounds.height());
+ primaryBounds = mProperties.mIsReversedLayout ? boundsBottom : boundsTop;
+ secondaryBounds = mProperties.mIsReversedLayout ? boundsTop : boundsBottom;
+ }
+ 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) {
+ return new float[]{color.red(), color.green(), color.blue()};
+ }
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 80afb16d5832..f9a6caf42e6e 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -21,6 +21,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_PINNED;
import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior;
import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior;
@@ -165,10 +166,11 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
/**
* Expands an existing TaskFragment to fill parent.
* @param wct WindowContainerTransaction in which the task fragment should be resized.
- * @param fragmentToken token of an existing TaskFragment.
+ * @param container the {@link TaskFragmentContainer} to be expanded.
*/
void expandTaskFragment(@NonNull WindowContainerTransaction wct,
- @NonNull IBinder fragmentToken) {
+ @NonNull TaskFragmentContainer container) {
+ final IBinder fragmentToken = container.getTaskFragmentToken();
resizeTaskFragment(wct, fragmentToken, new Rect());
clearAdjacentTaskFragments(wct, fragmentToken);
updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED);
@@ -353,14 +355,21 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
void setTaskFragmentIsolatedNavigation(@NonNull WindowContainerTransaction wct,
@NonNull IBinder fragmentToken, boolean isolatedNav) {
final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
- OP_TYPE_SET_ISOLATED_NAVIGATION).setIsolatedNav(isolatedNav).build();
+ OP_TYPE_SET_ISOLATED_NAVIGATION).setBooleanValue(isolatedNav).build();
+ wct.addTaskFragmentOperation(fragmentToken, operation);
+ }
+
+ void setTaskFragmentPinned(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder fragmentToken, boolean pinned) {
+ final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+ OP_TYPE_SET_PINNED).setBooleanValue(pinned).build();
wct.addTaskFragmentOperation(fragmentToken, operation);
}
void setTaskFragmentDimOnTask(@NonNull WindowContainerTransaction wct,
@NonNull IBinder fragmentToken, boolean dimOnTask) {
final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
- OP_TYPE_SET_DIM_ON_TASK).setDimOnTask(dimOnTask).build();
+ OP_TYPE_SET_DIM_ON_TASK).setBooleanValue(dimOnTask).build();
wct.addTaskFragmentOperation(fragmentToken, operation);
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java
new file mode 100644
index 000000000000..4541a843f479
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitAttributesHelper.java
@@ -0,0 +1,60 @@
+/*
+ * 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.embedding;
+
+import android.content.res.Configuration;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType;
+
+/** Helper functions for {@link SplitAttributes} */
+class SplitAttributesHelper {
+ /**
+ * Returns whether the split layout direction is reversed. Right-to-left and bottom-to-top are
+ * considered reversed.
+ */
+ static boolean isReversedLayout(
+ @NonNull SplitAttributes splitAttributes, @NonNull Configuration configuration) {
+ switch (splitAttributes.getLayoutDirection()) {
+ case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT:
+ case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM:
+ return false;
+ case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT:
+ case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP:
+ return true;
+ case SplitAttributes.LayoutDirection.LOCALE:
+ return configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ default:
+ throw new IllegalArgumentException(
+ "Invalid layout direction:" + splitAttributes.getLayoutDirection());
+ }
+ }
+
+ /**
+ * Returns whether the {@link SplitAttributes} is an {@link ExpandContainersSplitType} and it
+ * should show a draggable handle that allows the user to drag and restore it into a split.
+ * This state is a result of user dragging the divider to fully expand the secondary container.
+ */
+ static boolean isDraggableExpandType(@NonNull SplitAttributes splitAttributes) {
+ final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes();
+ return splitAttributes.getSplitType() instanceof ExpandContainersSplitType
+ && dividerAttributes != null
+ && dividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE;
+
+ }
+}
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 038d0081ead8..f78e2b5170fc 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -21,12 +21,14 @@ import static android.app.ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT;
import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO;
import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE;
import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE;
+import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_DRAG_RESIZE;
import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN;
import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK;
import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED;
@@ -48,6 +50,7 @@ import static androidx.window.extensions.embedding.SplitPresenter.getActivityInt
import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions;
import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds;
import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit;
+import static androidx.window.extensions.embedding.TaskFragmentContainer.OverlayContainerRestoreParams;
import android.annotation.CallbackExecutor;
import android.app.Activity;
@@ -56,6 +59,7 @@ import android.app.ActivityOptions;
import android.app.ActivityThread;
import android.app.Application;
import android.app.Instrumentation;
+import android.app.servertransaction.ClientTransactionListenerController;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -65,7 +69,6 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
-import android.os.SystemProperties;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -87,7 +90,7 @@ import androidx.annotation.Nullable;
import androidx.window.common.CommonFoldingFeature;
import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
-import androidx.window.extensions.WindowExtensionsImpl;
+import androidx.window.extensions.WindowExtensions;
import androidx.window.extensions.core.util.function.Consumer;
import androidx.window.extensions.core.util.function.Function;
import androidx.window.extensions.core.util.function.Predicate;
@@ -103,15 +106,20 @@ import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
/**
* Main controller class that manages split states and presentation.
*/
public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback,
- ActivityEmbeddingComponent {
+ ActivityEmbeddingComponent, DividerPresenter.DragEventCallback {
static final String TAG = "SplitController";
- static final boolean ENABLE_SHELL_TRANSITIONS =
- SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
+ static final boolean ENABLE_SHELL_TRANSITIONS = true;
+
+ // TODO(b/243518738): Move to WM Extensions if we have requirement of overlay without
+ // association. It's not set in WM Extensions nor Wm Jetpack library currently.
+ private static final String KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY =
+ "androidx.window.extensions.embedding.shouldAssociateWithLaunchingActivity";
@VisibleForTesting
@GuardedBy("mLock")
@@ -126,6 +134,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
private final List<EmbeddingRule> mSplitRules = new ArrayList<>();
/**
+ * Stores the token of the associated Activity that maps to the
+ * {@link OverlayContainerRestoreParams} of the most recent created overlay container.
+ */
+ @GuardedBy("mLock")
+ final ArrayMap<IBinder, OverlayContainerRestoreParams> mOverlayRestoreParams = new ArrayMap<>();
+
+ /**
* A developer-defined {@link SplitAttributes} calculator to compute the current
* {@link SplitAttributes} with the current device and window states.
* It is registered via {@link #setSplitAttributesCalculator(Function)}
@@ -161,6 +176,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@GuardedBy("mLock")
final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>();
+ /** Map from Task id to {@link DividerPresenter} which manages the divider in the Task. */
+ @GuardedBy("mLock")
+ private final SparseArray<DividerPresenter> mDividerPresenters = new SparseArray<>();
+
/** Callback to Jetpack to notify about changes to split states. */
@GuardedBy("mLock")
@Nullable
@@ -178,16 +197,31 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
private final List<ActivityStack> mLastReportedActivityStacks = new ArrayList<>();
+ /** WM Jetpack set callback for {@link EmbeddedActivityWindowInfo}. */
+ @GuardedBy("mLock")
+ @Nullable
+ private Pair<Executor, Consumer<EmbeddedActivityWindowInfo>>
+ mEmbeddedActivityWindowInfoCallback;
+
+ /** Listener registered to {@link ClientTransactionListenerController}. */
+ @GuardedBy("mLock")
+ @Nullable
+ private final BiConsumer<IBinder, ActivityWindowInfo> mActivityWindowInfoListener =
+ Flags.activityWindowInfoFlag()
+ ? this::onActivityWindowInfoChanged
+ : null;
+
private final Handler mHandler;
+ private final MainThreadExecutor mExecutor;
final Object mLock = new Object();
private final ActivityStartMonitor mActivityStartMonitor;
public SplitController(@NonNull WindowLayoutComponentImpl windowLayoutComponent,
@NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer) {
Log.i(TAG, "Initializing Activity Embedding Controller.");
- final MainThreadExecutor executor = new MainThreadExecutor();
- mHandler = executor.mHandler;
- mPresenter = new SplitPresenter(executor, windowLayoutComponent, this);
+ mExecutor = new MainThreadExecutor();
+ mHandler = mExecutor.mHandler;
+ mPresenter = new SplitPresenter(mExecutor, windowLayoutComponent, this);
mTransactionManager = new TransactionManager(mPresenter);
final ActivityThread activityThread = ActivityThread.currentActivityThread();
final Application application = activityThread.getApplication();
@@ -324,8 +358,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// Resets the isolated navigation and updates the container.
final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
final WindowContainerTransaction wct = transactionRecord.getTransaction();
- mPresenter.setTaskFragmentIsolatedNavigation(wct, containerToUnpin,
- false /* isolated */);
+ mPresenter.setTaskFragmentPinned(wct, containerToUnpin, false /* pinned */);
updateContainer(wct, containerToUnpin);
transactionRecord.apply(false /* shouldApplyIndependently */);
updateCallbackIfNecessary();
@@ -394,7 +427,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* Registers the split organizer callback to notify about changes to active splits.
*
* @deprecated Use {@link #setSplitInfoCallback(Consumer)} starting with
- * {@link WindowExtensionsImpl#getVendorApiLevel()} 2.
+ * {@link WindowExtensions#getVendorApiLevel()} 2.
*/
@Deprecated
@Override
@@ -407,7 +440,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
/**
* Registers the split organizer callback to notify about changes to active splits.
*
- * @since {@link WindowExtensionsImpl#getVendorApiLevel()} 2
+ * @since {@link WindowExtensions#getVendorApiLevel()} 2
*/
@Override
public void setSplitInfoCallback(Consumer<List<SplitInfo>> callback) {
@@ -661,11 +694,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
exception);
break;
case TYPE_ACTIVITY_REPARENTED_TO_TASK:
+ final IBinder candidateAssociatedActToken, lastOverlayToken;
+ if (Flags.fixPipRestoreToOverlay()) {
+ candidateAssociatedActToken = change.getOtherActivityToken();
+ lastOverlayToken = change.getTaskFragmentToken();
+ } else {
+ candidateAssociatedActToken = lastOverlayToken = null;
+ }
onActivityReparentedToTask(
wct,
taskId,
change.getActivityIntent(),
- change.getActivityToken());
+ change.getActivityToken(),
+ candidateAssociatedActToken,
+ lastOverlayToken);
break;
default:
throw new IllegalArgumentException(
@@ -825,13 +867,28 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
Log.e(TAG, "onTaskFragmentParentInfoChanged on empty Task id=" + taskId);
return;
}
+
+ if (!parentInfo.isVisible()) {
+ // Only making the TaskContainer invisible and drops the other info, and perform the
+ // update when the next time the Task becomes visible.
+ if (taskContainer.isVisible()) {
+ taskContainer.setInvisible();
+ }
+ return;
+ }
+
// Checks if container should be updated before apply new parentInfo.
final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo);
taskContainer.updateTaskFragmentParentInfo(parentInfo);
- // If the last direct activity of the host task is dismissed and the overlay container is
- // the only taskFragment, the overlay container should also be dismissed.
- dismissOverlayContainerIfNeeded(wct, taskContainer);
+ // 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);
+
+ // 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.
+ dismissAlwaysOnTopOverlayIfNeeded(wct, taskContainer);
if (!shouldUpdateContainer) {
return;
@@ -877,11 +934,28 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* different process, the server will generate a temporary token that
* the organizer can use to reparent the activity through
* {@link WindowContainerTransaction} if needed.
+ * @param candidateAssociatedActToken The token of the candidate associated-activity.
+ * @param lastOverlayToken The last parent overlay container token.
*/
@VisibleForTesting
@GuardedBy("mLock")
void onActivityReparentedToTask(@NonNull WindowContainerTransaction wct,
- int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken) {
+ int taskId, @NonNull Intent activityIntent, @NonNull IBinder activityToken,
+ @Nullable IBinder candidateAssociatedActToken, @Nullable IBinder lastOverlayToken) {
+ // Reparent the activity to an overlay container if needed.
+ final OverlayContainerRestoreParams params = getOverlayContainerRestoreParams(
+ candidateAssociatedActToken, lastOverlayToken);
+ if (params != null) {
+ final Activity associatedActivity = getActivity(candidateAssociatedActToken);
+ final TaskFragmentContainer targetContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
+ wct, params.mOptions, params.mIntent, associatedActivity);
+ if (targetContainer != null) {
+ wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(),
+ activityToken);
+ return;
+ }
+ }
+
// If the activity belongs to the current app process, we treat it as a new activity
// launch.
final Activity activity = getActivity(activityToken);
@@ -926,6 +1000,43 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
/**
+ * Returns the {@link OverlayContainerRestoreParams} that stored last time the {@code
+ * associatedActivityToken} associated with and only if data matches the {@code overlayToken}.
+ * Otherwise, return {@code null}.
+ */
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ @Nullable
+ OverlayContainerRestoreParams getOverlayContainerRestoreParams(
+ @Nullable IBinder associatedActivityToken, @Nullable IBinder overlayToken) {
+ if (!Flags.fixPipRestoreToOverlay()) {
+ return null;
+ }
+
+ if (associatedActivityToken == null || overlayToken == null) {
+ return null;
+ }
+
+ final TaskFragmentContainer.OverlayContainerRestoreParams params =
+ mOverlayRestoreParams.get(associatedActivityToken);
+ if (params == null) {
+ return null;
+ }
+
+ if (params.mOverlayToken != overlayToken) {
+ // Not the same overlay container, no need to restore.
+ return null;
+ }
+
+ final Activity associatedActivity = getActivity(associatedActivityToken);
+ if (associatedActivity == null || associatedActivity.isFinishing()) {
+ return null;
+ }
+
+ return params;
+ }
+
+ /**
* Called when the {@link WindowContainerTransaction} created with
* {@link WindowContainerTransaction#setErrorCallbackToken(IBinder)} failed on the server side.
*
@@ -990,6 +1101,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
if (taskContainer.isEmpty()) {
// Cleanup the TaskContainer if it becomes empty.
mTaskContainers.remove(taskContainer.getTaskId());
+ mDividerPresenters.remove(taskContainer.getTaskId());
}
return;
}
@@ -1036,8 +1148,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return true;
}
- // Skip resolving if the activity is on an isolated navigated TaskFragmentContainer.
- if (container != null && container.isIsolatedNavigationEnabled()) {
+ if (container != null && container.shouldSkipActivityResolving()) {
return true;
}
@@ -1208,10 +1319,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
final TaskFragmentContainer container = getContainerWithActivity(activity);
if (shouldContainerBeExpanded(container)) {
// Make sure that the existing container is expanded.
- mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken());
+ mPresenter.expandTaskFragment(wct, container);
} else {
// Put activity into a new expanded container.
- final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity));
+ final TaskFragmentContainer newContainer =
+ new TaskFragmentContainer.Builder(this, getTaskId(activity), activity)
+ .setPendingAppearedActivity(activity).build();
mPresenter.expandActivity(wct, newContainer.getTaskFragmentToken(), activity);
}
}
@@ -1378,9 +1491,29 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
launchPlaceholderIfNecessary(wct, activity, false /* isOnCreated */);
}
+ @GuardedBy("mLock")
+ private void onActivityPaused(@NonNull WindowContainerTransaction wct,
+ @NonNull Activity activity) {
+ // Checks if there's any finishing activity in paused state associate with an overlay
+ // container. #OnActivityPostDestroyed is a very late signal, which is called after activity
+ // is not visible and the next activity shows on screen.
+ if (!activity.isFinishing()) {
+ // onPaused is triggered without finishing. Early return.
+ return;
+ }
+ // Check if we should dismiss the overlay container with this finishing activity.
+ final IBinder activityToken = activity.getActivityToken();
+ for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
+ mTaskContainers.valueAt(i).onFinishingActivityPaused(wct, activityToken);
+ }
+
+ mOverlayRestoreParams.remove(activity.getActivityToken());
+ updateCallbackIfNecessary();
+ }
+
@VisibleForTesting
@GuardedBy("mLock")
- void onActivityDestroyed(@NonNull Activity activity) {
+ void onActivityDestroyed(@NonNull WindowContainerTransaction wct, @NonNull Activity activity) {
if (!activity.isFinishing()) {
// onDestroyed is triggered without finishing. This happens when the activity is
// relaunched. In this case, we don't want to cleanup the record.
@@ -1390,8 +1523,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// organizer.
final IBinder activityToken = activity.getActivityToken();
for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
- mTaskContainers.valueAt(i).onActivityDestroyed(activityToken);
+ mTaskContainers.valueAt(i).onActivityDestroyed(wct, activityToken);
}
+
+ mOverlayRestoreParams.remove(activity.getActivityToken());
// We didn't trigger the callback if there were any pending appeared activities, so check
// again after the pending is removed.
updateCallbackIfNecessary();
@@ -1471,12 +1606,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@GuardedBy("mLock")
TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct,
int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) {
- // Skip resolving if started from an isolated navigated TaskFragmentContainer.
if (launchingActivity != null) {
final TaskFragmentContainer taskFragmentContainer = getContainerWithActivity(
launchingActivity);
if (taskFragmentContainer != null
- && taskFragmentContainer.isIsolatedNavigationEnabled()) {
+ && taskFragmentContainer.shouldSkipActivityResolving()) {
+ return null;
+ }
+ if (isAssociatedWithOverlay(launchingActivity)) {
+ // Skip resolving if the launching activity associated with an overlay.
return null;
}
}
@@ -1570,7 +1708,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@Nullable Activity launchingActivity) {
return createEmptyContainer(wct, intent, taskId,
new ActivityStackAttributes.Builder().build(), launchingActivity,
- null /* overlayTag */, null /* launchOptions */);
+ null /* overlayTag */, null /* launchOptions */,
+ false /* shouldAssociateWithLaunchingActivity */);
}
/**
@@ -1585,7 +1724,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId,
@NonNull ActivityStackAttributes activityStackAttributes,
@Nullable Activity launchingActivity, @Nullable String overlayTag,
- @Nullable Bundle launchOptions) {
+ @Nullable Bundle launchOptions, boolean associateLaunchingActivity) {
// We need an activity in the organizer process in the same Task to use as the owner
// activity, as well as to get the Task window info.
final Activity activityInTask;
@@ -1601,17 +1740,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
// Can't find any activity in the Task that we can use as the owner activity.
return null;
}
- final TaskFragmentContainer container = newContainer(null /* pendingAppearedActivity */,
- intent, activityInTask, taskId, null /* pairedPrimaryContainer*/, overlayTag,
- launchOptions);
+ final TaskFragmentContainer container =
+ new TaskFragmentContainer.Builder(this, taskId, activityInTask)
+ .setPendingAppearedIntent(intent)
+ .setOverlayTag(overlayTag)
+ .setLaunchOptions(launchOptions)
+ .setAssociatedActivity(associateLaunchingActivity ? activityInTask : null)
+ .build();
final IBinder taskFragmentToken = container.getTaskFragmentToken();
// Note that taskContainer will not exist before calling #newContainer if the container
// is the first embedded TF in the task.
final TaskContainer taskContainer = container.getTaskContainer();
// TODO(b/265271880): remove redundant logic after all TF operations take fragmentToken.
- final Rect taskBounds = taskContainer.getBounds();
final Rect sanitizedBounds = sanitizeBounds(activityStackAttributes.getRelativeBounds(),
- getMinDimensions(intent), taskBounds);
+ getMinDimensions(intent), container);
final int windowingMode = taskContainer
.getWindowingModeForTaskFragment(sanitizedBounds);
mPresenter.createTaskFragment(wct, taskFragmentToken, activityInTask.getActivityToken(),
@@ -1672,82 +1814,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@GuardedBy("mLock")
@Nullable
TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) {
- // Check pending appeared activity first because there can be a delay for the server
- // update.
- TaskFragmentContainer taskFragmentContainer =
- getContainer(container -> container.hasPendingAppearedActivity(activityToken));
- if (taskFragmentContainer != null) {
- return taskFragmentContainer;
- }
-
-
- // Check appeared activity if there is no such pending appeared activity.
- return getContainer(container -> container.hasAppearedActivity(activityToken));
- }
-
- @GuardedBy("mLock")
- TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity, int taskId) {
- return newContainer(pendingAppearedActivity, pendingAppearedActivity, taskId);
- }
-
- @GuardedBy("mLock")
- TaskFragmentContainer newContainer(@NonNull Activity pendingAppearedActivity,
- @NonNull Activity activityInTask, int taskId) {
- return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */,
- activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */,
- null /* launchOptions */);
- }
-
- @GuardedBy("mLock")
- TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent,
- @NonNull Activity activityInTask, int taskId) {
- return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent,
- activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */,
- null /* launchOptions */);
- }
-
- @GuardedBy("mLock")
- TaskFragmentContainer newContainer(@NonNull Intent pendingAppearedIntent,
- @NonNull Activity activityInTask, int taskId,
- @NonNull TaskFragmentContainer pairedPrimaryContainer) {
- return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent,
- activityInTask, taskId, pairedPrimaryContainer, null /* tag */,
- null /* launchOptions */);
- }
-
- /**
- * Creates and registers a new organized container with an optional activity that will be
- * re-parented to it in a WCT.
- *
- * @param pendingAppearedActivity the activity that will be reparented to the TaskFragment.
- * @param pendingAppearedIntent the Intent that will be started in the TaskFragment.
- * @param activityInTask activity in the same Task so that we can get the Task bounds
- * if needed.
- * @param taskId parent Task of the new TaskFragment.
- * @param pairedPrimaryContainer the paired primary {@link TaskFragmentContainer}. When it is
- * set, the new container will be added right above it.
- * @param overlayTag The tag for the new created overlay container. It must be
- * needed if {@code isOverlay} is {@code true}. Otherwise,
- * it should be {@code null}.
- * @param launchOptions The launch options bundle to create a container. Must be
- * specified for overlay container.
- */
- @GuardedBy("mLock")
- TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity,
- @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId,
- @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag,
- @Nullable Bundle launchOptions) {
- if (activityInTask == null) {
- throw new IllegalArgumentException("activityInTask must not be null,");
- }
- if (!mTaskContainers.contains(taskId)) {
- mTaskContainers.put(taskId, new TaskContainer(taskId, activityInTask));
+ for (int i = mTaskContainers.size() - 1; i >= 0; --i) {
+ final TaskFragmentContainer container = mTaskContainers.valueAt(i)
+ .getContainerWithActivity(activityToken);
+ if (container != null) {
+ return container;
+ }
}
- final TaskContainer taskContainer = mTaskContainers.get(taskId);
- final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity,
- pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer, overlayTag,
- launchOptions);
- return container;
+ return null;
}
/**
@@ -1912,7 +1986,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
if (shouldContainerBeExpanded(container)) {
if (container.getInfo() != null) {
- mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken());
+ mPresenter.expandTaskFragment(wct, container);
}
// If the info is not available yet the task fragment will be expanded when it's ready
return;
@@ -1935,7 +2009,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@NonNull TaskFragmentContainer container) {
final TaskContainer taskContainer = container.getTaskContainer();
- if (dismissOverlayContainerIfNeeded(wct, taskContainer)) {
+ if (dismissAlwaysOnTopOverlayIfNeeded(wct, taskContainer)) {
return;
}
@@ -1959,22 +2033,27 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
}
- /** Dismisses the overlay container in the {@code taskContainer} if needed. */
+ /**
+ * Dismisses {@link TaskFragmentContainer#isAlwaysOnTopOverlay()} in the {@code taskContainer}
+ * if needed.
+ */
@GuardedBy("mLock")
- private boolean dismissOverlayContainerIfNeeded(@NonNull WindowContainerTransaction wct,
- @NonNull TaskContainer taskContainer) {
- final TaskFragmentContainer overlayContainer = taskContainer.getOverlayContainer();
- if (overlayContainer == null) {
+ private boolean dismissAlwaysOnTopOverlayIfNeeded(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskContainer taskContainer) {
+ // Dismiss always-on-top overlay container if it's the only container in the task and
+ // there's no direct activity in the parent task.
+ final List<TaskFragmentContainer> containers = taskContainer.getTaskFragmentContainers();
+ if (containers.size() != 1 || taskContainer.hasDirectActivity()) {
return false;
}
- // Dismiss the overlay container if it's the only container in the task and there's no
- // direct activity in the parent task.
- if (taskContainer.getTaskFragmentContainers().size() == 1
- && !taskContainer.hasDirectActivity()) {
- mPresenter.cleanupContainer(wct, overlayContainer, false /* shouldFinishDependant */);
- return true;
+
+ final TaskFragmentContainer container = containers.getLast();
+ if (!container.isAlwaysOnTopOverlay()) {
+ return false;
}
- return false;
+
+ mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependant */);
+ return true;
}
/**
@@ -2038,19 +2117,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
if (container == null) {
return null;
}
- final List<SplitContainer> splitContainers =
- container.getTaskContainer().getSplitContainers();
- if (splitContainers.isEmpty()) {
- return null;
- }
- for (int i = splitContainers.size() - 1; i >= 0; i--) {
- final SplitContainer splitContainer = splitContainers.get(i);
- if (container.equals(splitContainer.getSecondaryContainer())
- || container.equals(splitContainer.getPrimaryContainer())) {
- return splitContainer;
- }
- }
- return null;
+ return container.getTaskContainer().getActiveSplitForContainer(container);
}
/**
@@ -2100,6 +2167,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return false;
}
+ if (isAssociatedWithOverlay(activity)) {
+ // Can't launch the placeholder if the activity associates an overlay.
+ return false;
+ }
+
final TaskFragmentContainer container = getContainerWithActivity(activity);
if (container != null && !allowLaunchPlaceholder(container)) {
// We don't allow activity in this TaskFragment to launch placeholder.
@@ -2113,6 +2185,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return false;
}
+ if (container != null && container.getTaskContainer().isPlaceholderRuleSuppressed()) {
+ return false;
+ }
+
final TaskContainer.TaskProperties taskProperties = mPresenter.getTaskProperties(activity);
final Pair<Size, Size> minDimensionsPair = getActivityIntentMinDimensionsPair(activity,
placeholderRule.getPlaceholderIntent());
@@ -2135,6 +2211,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
*/
@GuardedBy("mLock")
private boolean allowLaunchPlaceholder(@NonNull TaskFragmentContainer container) {
+ if (container.isOverlay()) {
+ // Don't launch placeholder if the container is an overlay.
+ return false;
+ }
+
final TaskFragmentContainer topContainer = container.getTaskContainer()
.getTopNonFinishingTaskFragmentContainer();
if (container != topContainer) {
@@ -2208,6 +2289,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
if (SplitPresenter.shouldShowSplit(splitAttributes)) {
return false;
}
+ if (SplitPresenter.shouldShowPlaceholderWhenExpanded(splitAttributes)) {
+ return false;
+ }
mTransactionManager.getCurrentTransactionRecord()
.setOriginType(TASK_FRAGMENT_TRANSIT_CLOSE);
@@ -2405,13 +2489,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@GuardedBy("mLock")
TaskFragmentContainer getContainer(@NonNull Predicate<TaskFragmentContainer> predicate) {
for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
- final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i)
- .getTaskFragmentContainers();
- for (int j = containers.size() - 1; j >= 0; j--) {
- final TaskFragmentContainer container = containers.get(j);
- if (predicate.test(container)) {
- return container;
- }
+ final TaskFragmentContainer container = mTaskContainers.valueAt(i)
+ .getContainer(predicate);
+ if (container != null) {
+ return container;
}
}
return null;
@@ -2438,6 +2519,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return mTaskContainers.get(taskId);
}
+ @GuardedBy("mLock")
+ void addTaskContainer(int taskId, TaskContainer taskContainer) {
+ mTaskContainers.put(taskId, taskContainer);
+ mDividerPresenters.put(taskId, new DividerPresenter(taskId, this, mExecutor));
+ }
+
Handler getHandler() {
return mHandler;
}
@@ -2456,6 +2543,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
@VisibleForTesting
+ @Nullable
+ ActivityThread.ActivityClientRecord getActivityClientRecord(@NonNull Activity activity) {
+ return ActivityThread.currentActivityThread()
+ .getActivityClient(activity.getActivityToken());
+ }
+
+ @VisibleForTesting
ActivityStartMonitor getActivityStartMonitor() {
return mActivityStartMonitor;
}
@@ -2468,8 +2562,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@VisibleForTesting
@Nullable
IBinder getTaskFragmentTokenFromActivityClientRecord(@NonNull Activity activity) {
- final ActivityThread.ActivityClientRecord record = ActivityThread.currentActivityThread()
- .getActivityClient(activity.getActivityToken());
+ final ActivityThread.ActivityClientRecord record = getActivityClientRecord(activity);
return record != null ? record.mTaskFragmentToken : null;
}
@@ -2552,25 +2645,52 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
/**
* Gets all overlay containers from all tasks in this process, or an empty list if there's
* no overlay container.
- * <p>
- * Note that we only support one overlay container for each task, but an app could have multiple
- * tasks.
*/
@VisibleForTesting
@GuardedBy("mLock")
@NonNull
- List<TaskFragmentContainer> getAllOverlayTaskFragmentContainers() {
+ List<TaskFragmentContainer> getAllNonFinishingOverlayContainers() {
final List<TaskFragmentContainer> overlayContainers = new ArrayList<>();
for (int i = 0; i < mTaskContainers.size(); i++) {
final TaskContainer taskContainer = mTaskContainers.valueAt(i);
- final TaskFragmentContainer overlayContainer = taskContainer.getOverlayContainer();
- if (overlayContainer != null) {
- overlayContainers.add(overlayContainer);
- }
+ final List<TaskFragmentContainer> overlayContainersPerTask = taskContainer
+ .getTaskFragmentContainers()
+ .stream()
+ .filter(c -> c.isOverlay() && !c.isFinished())
+ .toList();
+ overlayContainers.addAll(overlayContainersPerTask);
}
return overlayContainers;
}
+ @GuardedBy("mLock")
+ private boolean isAssociatedWithOverlay(@NonNull Activity activity) {
+ final TaskContainer taskContainer = getTaskContainer(getTaskId(activity));
+ if (taskContainer == null) {
+ return false;
+ }
+ return taskContainer.getContainer(c -> c.isOverlay() && !c.isFinished()
+ && c.getAssociatedActivityToken() == activity.getActivityToken()) != null;
+ }
+
+ /**
+ * Creates an overlay container or updates a visible overlay container if its
+ * {@link TaskFragmentContainer#getTaskId()}, {@link TaskFragmentContainer#getOverlayTag()}
+ * and {@link TaskFragmentContainer#getAssociatedActivityToken()} matches.
+ * <p>
+ * This method will also dismiss any existing overlay container if:
+ * <ul>
+ * <li>it's visible but not meet the criteria to update overlay</li>
+ * <li>{@link TaskFragmentContainer#getOverlayTag()} matches but not meet the criteria to
+ * update overlay</li>
+ * </ul>
+ *
+ * @param wct the {@link WindowContainerTransaction}
+ * @param options the {@link ActivityOptions} to launch the overlay
+ * @param intent the intent of activity to launch
+ * @param launchActivity the activity to launch the overlay container
+ * @return the overlay container
+ */
@VisibleForTesting
// Suppress GuardedBy warning because lint ask to mark this method as
// @GuardedBy(container.mController.mLock), which is mLock itself
@@ -2580,9 +2700,17 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded(
@NonNull WindowContainerTransaction wct, @NonNull Bundle options,
@NonNull Intent intent, @NonNull Activity launchActivity) {
+ if (isActivityFromSplit(launchActivity)) {
+ // We restrict to launch the overlay from split. Fallback to treat it as normal
+ // launch.
+ return null;
+ }
+
final List<TaskFragmentContainer> overlayContainers =
- getAllOverlayTaskFragmentContainers();
+ getAllNonFinishingOverlayContainers();
final String overlayTag = Objects.requireNonNull(options.getString(KEY_OVERLAY_TAG));
+ final boolean associateLaunchingActivity = options
+ .getBoolean(KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY, true);
// If the requested bounds of OverlayCreateParams are smaller than minimum dimensions
// specified by Intent, expand the overlay container to fill the parent task instead.
@@ -2603,35 +2731,100 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
final int taskId = getTaskId(launchActivity);
if (!overlayContainers.isEmpty()) {
for (final TaskFragmentContainer overlayContainer : overlayContainers) {
- if (!overlayTag.equals(overlayContainer.getOverlayTag())
- && taskId == overlayContainer.getTaskId()) {
- // If there's an overlay container with different tag shown in the same
+ final boolean isTopNonFinishingOverlay = overlayContainer.equals(
+ overlayContainer.getTaskContainer().getTopNonFinishingTaskFragmentContainer(
+ true /* includePin */, true /* includeOverlay */));
+ if (taskId != overlayContainer.getTaskId()) {
+ // If there's an overlay container with same tag in a different task,
+ // dismiss the overlay container since the tag must be unique per process.
+ if (overlayTag.equals(overlayContainer.getOverlayTag())) {
+ Log.w(TAG, "The overlay container with tag:"
+ + overlayContainer.getOverlayTag() + " is dismissed because"
+ + " there's an existing overlay container with the same tag but"
+ + " different task ID:" + overlayContainer.getTaskId() + ". "
+ + "The new associated activity is " + launchActivity);
+ mPresenter.cleanupContainer(wct, overlayContainer,
+ false /* shouldFinishDependant */);
+ }
+ continue;
+ }
+ if (!overlayTag.equals(overlayContainer.getOverlayTag())) {
+ // If there's an overlay container with different tag on top in the same
// task, dismiss the existing overlay container.
+ if (isTopNonFinishingOverlay) {
+ mPresenter.cleanupContainer(wct, overlayContainer,
+ false /* shouldFinishDependant */);
+ }
+ continue;
+ }
+ // The overlay container has the same tag and task ID with the new launching
+ // overlay container.
+ if (!isTopNonFinishingOverlay) {
+ // Dismiss the invisible overlay container regardless of activity
+ // association if it collides the tag of new launched overlay container .
+ Log.w(TAG, "The invisible overlay container with tag:"
+ + overlayContainer.getOverlayTag() + " is dismissed because"
+ + " there's a launching overlay container with the same tag."
+ + " The new associated activity is " + launchActivity);
mPresenter.cleanupContainer(wct, overlayContainer,
false /* shouldFinishDependant */);
+ continue;
}
- if (overlayTag.equals(overlayContainer.getOverlayTag())
- && taskId != overlayContainer.getTaskId()) {
- // If there's an overlay container with same tag in a different task,
- // dismiss the overlay container since the tag must be unique per process.
+ // Requesting an always-on-top overlay.
+ if (!associateLaunchingActivity) {
+ if (overlayContainer.isOverlayWithActivityAssociation()) {
+ // Dismiss the overlay container since it has associated with an activity.
+ Log.w(TAG, "The overlay container with tag:"
+ + overlayContainer.getOverlayTag() + " is dismissed because"
+ + " there's an existing overlay container with the same tag but"
+ + " different associated launching activity. The overlay container"
+ + " doesn't associate with any activity.");
+ mPresenter.cleanupContainer(wct, overlayContainer,
+ false /* shouldFinishDependant */);
+ continue;
+ } else {
+ // The existing overlay container doesn't associate an activity as well.
+ // Just update the overlay and return.
+ // Note that going to this condition means the tag, task ID matches a
+ // visible always-on-top overlay, and won't dismiss any overlay any more.
+ mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs,
+ getMinDimensions(intent));
+ return overlayContainer;
+ }
+ }
+ if (launchActivity.getActivityToken()
+ != overlayContainer.getAssociatedActivityToken()) {
+ Log.w(TAG, "The overlay container with tag:"
+ + overlayContainer.getOverlayTag() + " is dismissed because"
+ + " there's an existing overlay container with the same tag but"
+ + " different associated launching activity. The new associated"
+ + " activity is " + launchActivity);
+ // The associated activity must be the same, or it will be dismissed.
mPresenter.cleanupContainer(wct, overlayContainer,
false /* shouldFinishDependant */);
+ continue;
}
- if (overlayTag.equals(overlayContainer.getOverlayTag())
- && taskId == overlayContainer.getTaskId()) {
- mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs,
- getMinDimensions(intent));
- // We can just return the updated overlay container and don't need to
- // check other condition since we only have one OverlayCreateParams, and
- // if the tag and task are matched, it's impossible to match another task
- // or tag since tags and tasks are all unique.
- return overlayContainer;
- }
+ // Reaching here means the launching activity launch an overlay container with the
+ // same task ID, tag, while there's a previously launching visible overlay
+ // container. We'll regard it as updating the existing overlay container.
+ mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs,
+ getMinDimensions(intent));
+ return overlayContainer;
+
}
}
// Launch the overlay container to the task with taskId.
return createEmptyContainer(wct, intent, taskId, attrs, launchActivity, overlayTag,
- options);
+ options, associateLaunchingActivity);
+ }
+
+ @GuardedBy("mLock")
+ private boolean isActivityFromSplit(@NonNull Activity activity) {
+ final TaskFragmentContainer container = getContainerWithActivity(activity);
+ if (container == null) {
+ return false;
+ }
+ return getActiveSplitForContainer(container) != null;
}
private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter {
@@ -2720,6 +2913,23 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
@Override
+ public void onActivityPostPaused(@NonNull Activity activity) {
+ if (activity.isChild()) {
+ // Skip Activity that is child of another Activity (ActivityGroup) because it's
+ // window will just be a child of the parent Activity window.
+ return;
+ }
+ synchronized (mLock) {
+ final TransactionRecord transactionRecord = mTransactionManager
+ .startNewTransaction();
+ transactionRecord.setOriginType(TRANSIT_CLOSE);
+ SplitController.this.onActivityPaused(
+ transactionRecord.getTransaction(), activity);
+ transactionRecord.apply(false /* shouldApplyIndependently */);
+ }
+ }
+
+ @Override
public void onActivityPostDestroyed(@NonNull Activity activity) {
if (activity.isChild()) {
// Skip Activity that is child of another Activity (ActivityGroup) because it's
@@ -2727,7 +2937,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return;
}
synchronized (mLock) {
- SplitController.this.onActivityDestroyed(activity);
+ final TransactionRecord transactionRecord = mTransactionManager
+ .startNewTransaction();
+ transactionRecord.setOriginType(TRANSIT_CLOSE);
+ SplitController.this.onActivityDestroyed(
+ transactionRecord.getTransaction(), activity);
+ transactionRecord.apply(false /* shouldApplyIndependently */);
}
}
}
@@ -2876,17 +3091,100 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
}
+ @Override
+ public void setEmbeddedActivityWindowInfoCallback(@NonNull Executor executor,
+ @NonNull Consumer<EmbeddedActivityWindowInfo> callback) {
+ if (!Flags.activityWindowInfoFlag()) {
+ return;
+ }
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ synchronized (mLock) {
+ if (mEmbeddedActivityWindowInfoCallback == null) {
+ ClientTransactionListenerController.getInstance()
+ .registerActivityWindowInfoChangedListener(getActivityWindowInfoListener());
+ }
+ mEmbeddedActivityWindowInfoCallback = new Pair<>(executor, callback);
+ }
+ }
+
+ @Override
+ public void clearEmbeddedActivityWindowInfoCallback() {
+ if (!Flags.activityWindowInfoFlag()) {
+ return;
+ }
+ synchronized (mLock) {
+ if (mEmbeddedActivityWindowInfoCallback == null) {
+ return;
+ }
+ mEmbeddedActivityWindowInfoCallback = null;
+ ClientTransactionListenerController.getInstance()
+ .unregisterActivityWindowInfoChangedListener(getActivityWindowInfoListener());
+ }
+ }
+
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ @Nullable
+ BiConsumer<IBinder, ActivityWindowInfo> getActivityWindowInfoListener() {
+ return mActivityWindowInfoListener;
+ }
+
@Nullable
- private static ActivityWindowInfo getActivityWindowInfo(@NonNull Activity activity) {
+ @Override
+ public EmbeddedActivityWindowInfo getEmbeddedActivityWindowInfo(@NonNull Activity activity) {
+ if (!Flags.activityWindowInfoFlag()) {
+ return null;
+ }
+ synchronized (mLock) {
+ final ActivityWindowInfo activityWindowInfo = getActivityWindowInfo(activity);
+ return activityWindowInfo != null
+ ? translateActivityWindowInfo(activity, activityWindowInfo)
+ : null;
+ }
+ }
+
+ @VisibleForTesting
+ void onActivityWindowInfoChanged(@NonNull IBinder activityToken,
+ @NonNull ActivityWindowInfo activityWindowInfo) {
+ synchronized (mLock) {
+ if (mEmbeddedActivityWindowInfoCallback == null) {
+ return;
+ }
+ final Executor executor = mEmbeddedActivityWindowInfoCallback.first;
+ final Consumer<EmbeddedActivityWindowInfo> callback =
+ mEmbeddedActivityWindowInfoCallback.second;
+
+ final Activity activity = getActivity(activityToken);
+ if (activity == null) {
+ return;
+ }
+ final EmbeddedActivityWindowInfo info = translateActivityWindowInfo(
+ activity, activityWindowInfo);
+
+ executor.execute(() -> callback.accept(info));
+ }
+ }
+
+ @Nullable
+ private ActivityWindowInfo getActivityWindowInfo(@NonNull Activity activity) {
if (activity.isFinishing()) {
return null;
}
- final ActivityThread.ActivityClientRecord record =
- ActivityThread.currentActivityThread()
- .getActivityClient(activity.getActivityToken());
+ final ActivityThread.ActivityClientRecord record = getActivityClientRecord(activity);
return record != null ? record.getActivityWindowInfo() : null;
}
+ @NonNull
+ private static EmbeddedActivityWindowInfo translateActivityWindowInfo(
+ @NonNull Activity activity, @NonNull ActivityWindowInfo activityWindowInfo) {
+ final boolean isEmbedded = activityWindowInfo.isEmbedded();
+ final Rect taskBounds = new Rect(activityWindowInfo.getTaskBounds());
+ final Rect activityStackBounds = new Rect(activityWindowInfo.getTaskFragmentBounds());
+ return new EmbeddedActivityWindowInfo(activity, isEmbedded, taskBounds,
+ activityStackBounds);
+ }
+
/**
* If the two rules have the same presentation, and the calculated {@link SplitAttributes}
* matches the {@link SplitAttributes} of {@link SplitContainer}, we can reuse the same
@@ -2957,4 +3255,50 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
return configuration != null
&& configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED;
}
+
+ @GuardedBy("mLock")
+ void updateDivider(
+ @NonNull WindowContainerTransaction wct, @NonNull TaskContainer taskContainer) {
+ final DividerPresenter dividerPresenter = mDividerPresenters.get(taskContainer.getTaskId());
+ final TaskFragmentParentInfo parentInfo = taskContainer.getTaskFragmentParentInfo();
+ dividerPresenter.updateDivider(
+ wct, parentInfo, taskContainer.getTopNonFinishingSplitContainer());
+ }
+
+ @Override
+ public void onStartDragging(@NonNull Consumer<WindowContainerTransaction> action) {
+ synchronized (mLock) {
+ final TransactionRecord transactionRecord =
+ mTransactionManager.startNewTransaction();
+ final WindowContainerTransaction wct = transactionRecord.getTransaction();
+ action.accept(wct);
+ transactionRecord.apply(false /* shouldApplyIndependently */);
+ }
+ }
+
+ @Override
+ public void onFinishDragging(
+ int taskId,
+ @NonNull Consumer<WindowContainerTransaction> action) {
+ synchronized (mLock) {
+ final TransactionRecord transactionRecord =
+ mTransactionManager.startNewTransaction();
+ transactionRecord.setOriginType(TASK_FRAGMENT_TRANSIT_DRAG_RESIZE);
+ final WindowContainerTransaction wct = transactionRecord.getTransaction();
+ final TaskContainer taskContainer = mTaskContainers.get(taskId);
+ if (taskContainer != null) {
+ final DividerPresenter dividerPresenter =
+ mDividerPresenters.get(taskContainer.getTaskId());
+ final List<TaskFragmentContainer> containersToFinish = new ArrayList<>();
+ taskContainer.updateTopSplitContainerForDivider(
+ dividerPresenter, containersToFinish);
+ for (final TaskFragmentContainer container : containersToFinish) {
+ mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependent */);
+ }
+ updateContainersInTask(wct, taskContainer);
+ }
+ action.accept(wct);
+ transactionRecord.apply(false /* shouldApplyIndependently */);
+ }
+ }
}
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 b53b9c519cb6..d888fa9d6feb 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -18,6 +18,8 @@ package androidx.window.extensions.embedding;
import static android.content.pm.PackageManager.MATCH_ALL;
+import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider;
+import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout;
import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
import android.app.Activity;
@@ -32,7 +34,6 @@ import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
-import android.util.LayoutDirection;
import android.util.Pair;
import android.util.Size;
import android.view.View;
@@ -85,10 +86,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
})
private @interface Position {}
- private static final int CONTAINER_POSITION_LEFT = 0;
- private static final int CONTAINER_POSITION_TOP = 1;
- private static final int CONTAINER_POSITION_RIGHT = 2;
- private static final int CONTAINER_POSITION_BOTTOM = 3;
+ static final int CONTAINER_POSITION_LEFT = 0;
+ static final int CONTAINER_POSITION_TOP = 1;
+ static final int CONTAINER_POSITION_RIGHT = 2;
+ static final int CONTAINER_POSITION_BOTTOM = 3;
@IntDef(value = {
CONTAINER_POSITION_LEFT,
@@ -96,7 +97,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
CONTAINER_POSITION_RIGHT,
CONTAINER_POSITION_BOTTOM,
})
- private @interface ContainerPosition {}
+ @interface ContainerPosition {}
/**
* Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
@@ -185,8 +186,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
// Create new empty task fragment
final int taskId = primaryContainer.getTaskId();
- final TaskFragmentContainer secondaryContainer = mController.newContainer(
- secondaryIntent, primaryActivity, taskId);
+ final TaskFragmentContainer secondaryContainer =
+ new TaskFragmentContainer.Builder(mController, taskId, primaryActivity)
+ .setPendingAppearedIntent(secondaryIntent).build();
final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties,
splitAttributes);
final int windowingMode = mController.getTaskContainer(taskId)
@@ -260,7 +262,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
TaskFragmentContainer container = mController.getContainerWithActivity(activity);
final int taskId = container != null ? container.getTaskId() : activity.getTaskId();
if (container == null || container == containerToAvoid) {
- container = mController.newContainer(activity, taskId);
+ container = new TaskFragmentContainer.Builder(mController, taskId, activity)
+ .setPendingAppearedActivity(activity).build();
final int windowingMode = mController.getTaskContainer(taskId)
.getWindowingModeForTaskFragment(relBounds);
final IBinder reparentActivityToken = activity.getActivityToken();
@@ -303,15 +306,19 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
launchingActivity);
if (primaryContainer == null) {
- primaryContainer = mController.newContainer(launchingActivity,
- launchingActivity.getTaskId());
+ primaryContainer = new TaskFragmentContainer.Builder(mController,
+ launchingActivity.getTaskId(), launchingActivity)
+ .setPendingAppearedActivity(launchingActivity).build();
}
final int taskId = primaryContainer.getTaskId();
- final TaskFragmentContainer secondaryContainer = mController.newContainer(activityIntent,
- launchingActivity, taskId,
- // Pass in the primary container to make sure it is added right above the primary.
- primaryContainer);
+ final TaskFragmentContainer secondaryContainer =
+ new TaskFragmentContainer.Builder(mController, taskId, launchingActivity)
+ .setPendingAppearedIntent(activityIntent)
+ // Pass in the primary container to make sure it is added right above the
+ // primary.
+ .setPairedPrimaryContainer(primaryContainer)
+ .build();
final TaskContainer taskContainer = mController.getTaskContainer(taskId);
final int windowingMode = taskContainer.getWindowingModeForTaskFragment(
primaryRelBounds);
@@ -367,6 +374,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode);
updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes);
updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes);
+ mController.updateDivider(wct, taskContainer);
}
private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
@@ -399,18 +407,26 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
return;
}
- setTaskFragmentIsolatedNavigation(wct, secondaryContainer, !isStacked /* isolatedNav */);
+ setTaskFragmentPinned(wct, secondaryContainer, !isStacked /* pinned */);
if (isStacked && !splitPinRule.isSticky()) {
secondaryContainer.getTaskContainer().removeSplitPinContainer();
}
}
/**
- * Sets whether to enable isolated navigation for this {@link TaskFragmentContainer}
+ * Sets whether to enable isolated navigation for this {@link TaskFragmentContainer}.
+ * <p>
+ * If a container enables isolated navigation, activities can't be launched to this container
+ * unless explicitly requested to be launched to.
+ *
+ * @see TaskFragmentContainer#isOverlayWithActivityAssociation()
*/
void setTaskFragmentIsolatedNavigation(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container,
boolean isolatedNavigationEnabled) {
+ if (!Flags.activityEmbeddingOverlayPresentationFlag() && container.isOverlay()) {
+ return;
+ }
if (container.isIsolatedNavigationEnabled() == isolatedNavigationEnabled) {
return;
}
@@ -420,6 +436,28 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
}
/**
+ * Sets whether to pin this {@link TaskFragmentContainer}.
+ * <p>
+ * If a container is pinned, it won't be chosen as the launch target unless it's the launching
+ * container.
+ *
+ * @see TaskFragmentContainer#isAlwaysOnTopOverlay()
+ * @see TaskContainer#getSplitPinContainer()
+ */
+ void setTaskFragmentPinned(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentContainer container,
+ boolean pinned) {
+ if (!Flags.activityEmbeddingOverlayPresentationFlag() && container.isOverlay()) {
+ return;
+ }
+ if (container.isPinned() == pinned) {
+ return;
+ }
+ container.setPinned(pinned);
+ setTaskFragmentPinned(wct, container.getTaskFragmentToken(), pinned);
+ }
+
+ /**
* Resizes the task fragment if it was already registered. Skips the operation if the container
* creation has not been reported from the server yet.
*/
@@ -465,6 +503,11 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
reorderTaskFragmentToFront(wct,
pinnedContainer.getSecondaryContainer().getTaskFragmentToken());
}
+ final TaskFragmentContainer alwaysOnTopOverlayContainer = container.getTaskContainer()
+ .getAlwaysOnTopOverlayContainer();
+ if (alwaysOnTopOverlayContainer != null) {
+ reorderTaskFragmentToFront(wct, alwaysOnTopOverlayContainer.getTaskFragmentToken());
+ }
}
@Override
@@ -563,7 +606,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@Override
void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary,
- @Nullable IBinder secondary) {
+ @Nullable IBinder secondary) {
final TaskFragmentContainer container = mController.getContainer(primary);
if (container == null) {
throw new IllegalStateException("setCompanionTaskFragment on TaskFragment that is"
@@ -579,21 +622,30 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
super.setCompanionTaskFragment(wct, primary, secondary);
}
+ /**
+ * Applies the {@code attributes} to a standalone {@code container}.
+ *
+ * @param minDimensions the minimum dimension of the container.
+ */
void applyActivityStackAttributes(
@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container,
@NonNull ActivityStackAttributes attributes,
@Nullable Size minDimensions) {
- final Rect taskBounds = container.getTaskContainer().getBounds();
final Rect relativeBounds = sanitizeBounds(attributes.getRelativeBounds(), minDimensions,
- taskBounds);
+ container);
final boolean isFillParent = relativeBounds.isEmpty();
- final boolean isIsolatedNavigated = !isFillParent && container.isOverlay();
final boolean dimOnTask = !isFillParent
- && attributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK
- && Flags.fullscreenDimFlag();
+ && Flags.fullscreenDimFlag()
+ && attributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK;
final IBinder fragmentToken = container.getTaskFragmentToken();
+ if (container.isAlwaysOnTopOverlay()) {
+ setTaskFragmentPinned(wct, container, !isFillParent);
+ } else if (container.isOverlayWithActivityAssociation()) {
+ setTaskFragmentIsolatedNavigation(wct, container, !isFillParent);
+ }
+
// TODO(b/243518738): Update to resizeTaskFragment after we migrate WCT#setRelativeBounds
// and WCT#setWindowingMode to take fragmentToken.
resizeTaskFragmentIfRegistered(wct, container, relativeBounds);
@@ -602,30 +654,32 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode);
// Always use default animation for standalone ActivityStack.
updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
- setTaskFragmentIsolatedNavigation(wct, container, isIsolatedNavigated);
setTaskFragmentDimOnTask(wct, fragmentToken, dimOnTask);
}
/**
- * 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,
- @NonNull Rect taskBounds) {
- if (bounds.isEmpty()) {
+ static Rect sanitizeBounds(@NonNull Rect relBounds, @Nullable Size minDimension,
+ @NonNull TaskFragmentContainer container) {
+ 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();
}
- if (!taskBounds.contains(bounds)) {
+ final TaskContainer taskContainer = container.getTaskContainer();
+ 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
@@ -685,8 +739,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
splitContainer.getPrimaryContainer().getTaskFragmentToken();
final IBinder secondaryToken =
splitContainer.getSecondaryContainer().getTaskFragmentToken();
- expandTaskFragment(wct, primaryToken);
- expandTaskFragment(wct, secondaryToken);
+ expandTaskFragment(wct, splitContainer.getPrimaryContainer());
+ expandTaskFragment(wct, splitContainer.getSecondaryContainer());
// Set the companion TaskFragment when the two containers stacked.
setCompanionTaskFragment(wct, primaryToken, secondaryToken,
splitContainer.getSplitRule(), true /* isStacked */);
@@ -695,6 +749,17 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
return RESULT_NOT_EXPANDED;
}
+ /**
+ * Expands an existing TaskFragment to fill parent.
+ * @param wct WindowContainerTransaction in which the task fragment should be resized.
+ * @param container the {@link TaskFragmentContainer} to be expanded.
+ */
+ void expandTaskFragment(@NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentContainer container) {
+ super.expandTaskFragment(wct, container);
+ mController.updateDivider(wct, container.getTaskContainer());
+ }
+
static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) {
return shouldShowSplit(splitContainer.getCurrentSplitAttributes());
}
@@ -703,6 +768,12 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
return !(splitAttributes.getSplitType() instanceof ExpandContainersSplitType);
}
+ static boolean shouldShowPlaceholderWhenExpanded(@NonNull SplitAttributes splitAttributes) {
+ // The placeholder should be kept if the expand split type is a result of user dragging
+ // the divider.
+ return SplitAttributesHelper.isDraggableExpandType(splitAttributes);
+ }
+
@NonNull
SplitAttributes computeSplitAttributes(@NonNull TaskProperties taskProperties,
@NonNull SplitRule rule, @NonNull SplitAttributes defaultSplitAttributes,
@@ -738,6 +809,15 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
private SplitAttributes sanitizeSplitAttributes(@NonNull TaskProperties taskProperties,
@NonNull SplitAttributes splitAttributes,
@Nullable Pair<Size, Size> minDimensionsPair) {
+ // Sanitize the DividerAttributes and set default values.
+ if (splitAttributes.getDividerAttributes() != null) {
+ splitAttributes = new SplitAttributes.Builder(splitAttributes)
+ .setDividerAttributes(
+ DividerPresenter.sanitizeDividerAttributes(
+ splitAttributes.getDividerAttributes())
+ ).build();
+ }
+
if (minDimensionsPair == null) {
return splitAttributes;
}
@@ -930,18 +1010,18 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
*/
private static SplitAttributes updateSplitAttributesType(
@NonNull SplitAttributes splitAttributes, @NonNull SplitType splitTypeToUpdate) {
- return new SplitAttributes.Builder()
+ return new SplitAttributes.Builder(splitAttributes)
.setSplitType(splitTypeToUpdate)
- .setLayoutDirection(splitAttributes.getLayoutDirection())
- .setAnimationBackground(splitAttributes.getAnimationBackground())
.build();
}
@NonNull
private Rect getLeftContainerBounds(@NonNull Configuration taskConfiguration,
@NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+ final int dividerOffset = getBoundsOffsetForDivider(
+ splitAttributes, CONTAINER_POSITION_LEFT);
final int right = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
- CONTAINER_POSITION_LEFT, foldingFeature);
+ CONTAINER_POSITION_LEFT, foldingFeature) + dividerOffset;
final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds();
return new Rect(taskBounds.left, taskBounds.top, right, taskBounds.bottom);
}
@@ -949,8 +1029,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@NonNull
private Rect getRightContainerBounds(@NonNull Configuration taskConfiguration,
@NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+ final int dividerOffset = getBoundsOffsetForDivider(
+ splitAttributes, CONTAINER_POSITION_RIGHT);
final int left = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
- CONTAINER_POSITION_RIGHT, foldingFeature);
+ CONTAINER_POSITION_RIGHT, foldingFeature) + dividerOffset;
final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
return new Rect(left, parentBounds.top, parentBounds.right, parentBounds.bottom);
}
@@ -958,8 +1040,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@NonNull
private Rect getTopContainerBounds(@NonNull Configuration taskConfiguration,
@NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+ final int dividerOffset = getBoundsOffsetForDivider(
+ splitAttributes, CONTAINER_POSITION_TOP);
final int bottom = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
- CONTAINER_POSITION_TOP, foldingFeature);
+ CONTAINER_POSITION_TOP, foldingFeature) + dividerOffset;
final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
return new Rect(parentBounds.left, parentBounds.top, parentBounds.right, bottom);
}
@@ -967,8 +1051,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
@NonNull
private Rect getBottomContainerBounds(@NonNull Configuration taskConfiguration,
@NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
+ final int dividerOffset = getBoundsOffsetForDivider(
+ splitAttributes, CONTAINER_POSITION_BOTTOM);
final int top = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
- CONTAINER_POSITION_BOTTOM, foldingFeature);
+ CONTAINER_POSITION_BOTTOM, foldingFeature) + dividerOffset;
final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
return new Rect(parentBounds.left, top, parentBounds.right, parentBounds.bottom);
}
@@ -1091,7 +1177,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
*/
private SplitType computeSplitType(@NonNull SplitAttributes splitAttributes,
@NonNull Configuration taskConfiguration, @Nullable FoldingFeature foldingFeature) {
- final int layoutDirection = splitAttributes.getLayoutDirection();
final SplitType splitType = splitAttributes.getSplitType();
if (splitType instanceof ExpandContainersSplitType) {
return splitType;
@@ -1100,19 +1185,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
// Reverse the ratio for RIGHT_TO_LEFT and BOTTOM_TO_TOP to make the boundary
// computation have the same direction, which is from (top, left) to (bottom, right).
final SplitType reversedSplitType = new RatioSplitType(1 - splitRatio.getRatio());
- switch (layoutDirection) {
- case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT:
- case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM:
- return splitType;
- case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT:
- case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP:
- return reversedSplitType;
- case LayoutDirection.LOCALE: {
- boolean isLtr = taskConfiguration.getLayoutDirection()
- == View.LAYOUT_DIRECTION_LTR;
- return isLtr ? splitType : reversedSplitType;
- }
- }
+ return isReversedLayout(splitAttributes, taskConfiguration)
+ ? reversedSplitType
+ : splitType;
} else if (splitType instanceof HingeSplitType) {
final HingeSplitType hinge = (HingeSplitType) splitType;
@WindowingMode
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 73109e266905..ee00c4cd67eb 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -22,6 +22,9 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.inMultiWindowMode;
+import static androidx.window.extensions.embedding.DividerPresenter.RATIO_EXPANDED_PRIMARY;
+import static androidx.window.extensions.embedding.DividerPresenter.RATIO_EXPANDED_SECONDARY;
+
import android.app.Activity;
import android.app.ActivityClient;
import android.app.WindowConfiguration;
@@ -40,6 +43,10 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.window.extensions.core.util.function.Predicate;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType;
+import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType;
import java.util.ArrayList;
import java.util.List;
@@ -64,18 +71,14 @@ class TaskContainer {
@Nullable
private SplitPinContainer mSplitPinContainer;
- /** The overlay container in this Task. */
+ /**
+ * The {@link TaskFragmentContainer#isAlwaysOnTopOverlay()} in the Task.
+ */
@Nullable
- private TaskFragmentContainer mOverlayContainer;
+ private TaskFragmentContainer mAlwaysOnTopOverlayContainer;
@NonNull
- private final Configuration mConfiguration;
-
- private int mDisplayId;
-
- private boolean mIsVisible;
-
- private boolean mHasDirectActivity;
+ private TaskFragmentParentInfo mInfo;
/**
* TaskFragments that the organizer has requested to be closed. They should be removed when
@@ -86,13 +89,31 @@ class TaskContainer {
final Set<IBinder> mFinishedContainer = new ArraySet<>();
/**
+ * The {@link RatioSplitType} that will be applied to newly added containers. This is to ensure
+ * the required UX that, after user dragging the divider, the split ratio is persistent after
+ * launching a new activity into a new TaskFragment in the same Task.
+ */
+ private RatioSplitType mOverrideSplitType;
+
+ /**
+ * If {@code true}, suppress the placeholder rules in the {@link TaskContainer}.
+ * <p>
+ * This is used in case the user drags the divider to fully expand the primary container and
+ * dismiss the secondary container while a {@link SplitPlaceholderRule} is used. Without this
+ * flag, after dismissing the secondary container, a placeholder will be launched again.
+ * <p>
+ * This flag is set true when the primary container is fully expanded and cleared when a new
+ * split is added to the {@link TaskContainer}.
+ */
+ private boolean mPlaceholderRuleSuppressed;
+
+ /**
* The {@link TaskContainer} constructor
*
- * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with
- * {@code activityInTask}.
+ * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with
+ * {@code activityInTask}.
* @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to
* initialize the {@link TaskContainer} properties.
- *
*/
TaskContainer(int taskId, @NonNull Activity activityInTask) {
if (taskId == INVALID_TASK_ID) {
@@ -101,12 +122,14 @@ class TaskContainer {
mTaskId = taskId;
final TaskProperties taskProperties = TaskProperties
.getTaskPropertiesFromActivity(activityInTask);
- mConfiguration = taskProperties.getConfiguration();
- mDisplayId = taskProperties.getDisplayId();
- // Note that it is always called when there's a new Activity is started, which implies
- // the host task is visible and has an activity in the task.
- mIsVisible = true;
- mHasDirectActivity = true;
+ mInfo = new TaskFragmentParentInfo(
+ taskProperties.getConfiguration(),
+ taskProperties.getDisplayId(),
+ // Note that it is always called when there's a new Activity is started, which
+ // implies the host task is visible and has an activity in the task.
+ true /* visible */,
+ true /* hasDirectActivity */,
+ null /* decorSurface */);
}
int getTaskId() {
@@ -114,32 +137,39 @@ class TaskContainer {
}
int getDisplayId() {
- return mDisplayId;
+ return mInfo.getDisplayId();
}
boolean isVisible() {
- return mIsVisible;
+ return mInfo.isVisible();
+ }
+
+ void setInvisible() {
+ mInfo = new TaskFragmentParentInfo(mInfo.getConfiguration(), mInfo.getDisplayId(),
+ false /* visible */, mInfo.hasDirectActivity(), mInfo.getDecorSurface());
}
boolean hasDirectActivity() {
- return mHasDirectActivity;
+ return mInfo.hasDirectActivity();
}
@NonNull
Rect getBounds() {
- return mConfiguration.windowConfiguration.getBounds();
+ return mInfo.getConfiguration().windowConfiguration.getBounds();
}
@NonNull
TaskProperties getTaskProperties() {
- return new TaskProperties(mDisplayId, mConfiguration);
+ return new TaskProperties(mInfo.getDisplayId(), mInfo.getConfiguration());
}
void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) {
- mConfiguration.setTo(info.getConfiguration());
- mDisplayId = info.getDisplayId();
- mIsVisible = info.isVisible();
- mHasDirectActivity = info.hasDirectActivity();
+ mInfo = info;
+ }
+
+ @NonNull
+ TaskFragmentParentInfo getTaskFragmentParentInfo() {
+ return mInfo;
}
/**
@@ -148,21 +178,23 @@ class TaskContainer {
boolean shouldUpdateContainer(@NonNull TaskFragmentParentInfo info) {
final Configuration configuration = info.getConfiguration();
- return info.isVisible()
- // No need to update presentation in PIP until the Task exit PIP.
- && !isInPictureInPicture(configuration)
- // If the task properties equals regardless of starting position, don't need to
- // update the container.
- && (mConfiguration.diffPublicOnly(configuration) != 0
- || mDisplayId != info.getDisplayId());
+ if (isInPictureInPicture(configuration)) {
+ // No need to update presentation in PIP until the Task exit PIP.
+ return false;
+ }
+
+ // If the task properties equals regardless of starting position, don't
+ // need to update the container.
+ return mInfo.getConfiguration().diffPublicOnly(configuration) != 0
+ || mInfo.getDisplayId() != info.getDisplayId();
}
/**
* Returns the windowing mode for the TaskFragments below this Task, which should be split with
* other TaskFragments.
*
- * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when
- * the pair of TaskFragments are stacked due to the limited space.
+ * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when
+ * the pair of TaskFragments are stacked due to the limited space.
*/
@WindowingMode
int getWindowingModeForTaskFragment(@Nullable Rect taskFragmentBounds) {
@@ -181,7 +213,7 @@ class TaskContainer {
}
boolean isInPictureInPicture() {
- return isInPictureInPicture(mConfiguration);
+ return isInPictureInPicture(mInfo.getConfiguration());
}
private static boolean isInPictureInPicture(@NonNull Configuration configuration) {
@@ -194,7 +226,7 @@ class TaskContainer {
@WindowingMode
private int getWindowingMode() {
- return mConfiguration.windowConfiguration.getWindowingMode();
+ return mInfo.getConfiguration().windowConfiguration.getWindowingMode();
}
/** Whether there is any {@link TaskFragmentContainer} below this Task. */
@@ -202,10 +234,19 @@ class TaskContainer {
return mContainers.isEmpty() && mFinishedContainer.isEmpty();
}
+ /** Called when the activity {@link Activity#isFinishing()} and paused. */
+ void onFinishingActivityPaused(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder activityToken) {
+ for (TaskFragmentContainer container : mContainers) {
+ container.onFinishingActivityPaused(wct, activityToken);
+ }
+ }
+
/** Called when the activity is destroyed. */
- void onActivityDestroyed(@NonNull IBinder activityToken) {
+ void onActivityDestroyed(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder activityToken) {
for (TaskFragmentContainer container : mContainers) {
- container.onActivityDestroyed(activityToken);
+ container.onActivityDestroyed(wct, activityToken);
}
}
@@ -228,7 +269,7 @@ class TaskContainer {
@Nullable
TaskFragmentContainer getTopNonFinishingTaskFragmentContainer(boolean includePin,
- boolean includeOverlay) {
+ boolean includeOverlay) {
for (int i = mContainers.size() - 1; i >= 0; i--) {
final TaskFragmentContainer container = mContainers.get(i);
if (!includePin && isTaskFragmentContainerPinned(container)) {
@@ -273,17 +314,51 @@ class TaskContainer {
return null;
}
- /** Returns the overlay container in the task, or {@code null} if it doesn't exist. */
@Nullable
- TaskFragmentContainer getOverlayContainer() {
- return mOverlayContainer;
+ TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) {
+ return getContainer(container -> container.hasAppearedActivity(activityToken)
+ || container.hasPendingAppearedActivity(activityToken));
+ }
+
+ @Nullable
+ TaskFragmentContainer getContainer(@NonNull Predicate<TaskFragmentContainer> predicate) {
+ for (int i = mContainers.size() - 1; i >= 0; i--) {
+ final TaskFragmentContainer container = mContainers.get(i);
+ if (predicate.test(container)) {
+ return container;
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ SplitContainer getActiveSplitForContainer(@Nullable TaskFragmentContainer container) {
+ if (container == null) {
+ return null;
+ }
+ for (int i = mSplitContainers.size() - 1; i >= 0; i--) {
+ final SplitContainer splitContainer = mSplitContainers.get(i);
+ if (container.equals(splitContainer.getSecondaryContainer())
+ || container.equals(splitContainer.getPrimaryContainer())) {
+ return splitContainer;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the always-on-top overlay container in the task, or {@code null} if it doesn't exist.
+ */
+ @Nullable
+ TaskFragmentContainer getAlwaysOnTopOverlayContainer() {
+ return mAlwaysOnTopOverlayContainer;
}
int indexOf(@NonNull TaskFragmentContainer child) {
return mContainers.indexOf(child);
}
- /** Whether the Task is in an intermediate state waiting for the server update.*/
+ /** Whether the Task is in an intermediate state waiting for the server update. */
boolean isInIntermediateState() {
for (TaskFragmentContainer container : mContainers) {
if (container.isInIntermediateState()) {
@@ -304,6 +379,11 @@ class TaskContainer {
}
void addSplitContainer(@NonNull SplitContainer splitContainer) {
+ // Reset the placeholder rule suppression when a new split container is added.
+ mPlaceholderRuleSuppressed = false;
+
+ applyOverrideSplitTypeIfNeeded(splitContainer);
+
if (splitContainer instanceof SplitPinContainer) {
mSplitPinContainer = (SplitPinContainer) splitContainer;
mSplitContainers.add(splitContainer);
@@ -318,6 +398,39 @@ class TaskContainer {
}
}
+ boolean isPlaceholderRuleSuppressed() {
+ return mPlaceholderRuleSuppressed;
+ }
+
+ // If there is an override SplitType due to user dragging the divider, the split ratio should
+ // be applied to newly added SplitContainers.
+ private void applyOverrideSplitTypeIfNeeded(@NonNull SplitContainer splitContainer) {
+ if (mOverrideSplitType == null) {
+ return;
+ }
+ final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes();
+ final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes();
+ if (!(splitAttributes.getSplitType() instanceof RatioSplitType)) {
+ // Skip if the original split type is not a ratio type.
+ return;
+ }
+ if (dividerAttributes == null
+ || dividerAttributes.getDividerType() != DividerAttributes.DIVIDER_TYPE_DRAGGABLE) {
+ // Skip if the split does not have a draggable divider.
+ return;
+ }
+ updateDefaultSplitAttributes(splitContainer, mOverrideSplitType);
+ }
+
+ private static void updateDefaultSplitAttributes(
+ @NonNull SplitContainer splitContainer, @NonNull SplitType overrideSplitType) {
+ splitContainer.updateDefaultSplitAttributes(
+ new SplitAttributes.Builder(splitContainer.getDefaultSplitAttributes())
+ .setSplitType(overrideSplitType)
+ .build()
+ );
+ }
+
void removeSplitContainers(@NonNull List<SplitContainer> containers) {
mSplitContainers.removeAll(containers);
}
@@ -389,13 +502,82 @@ class TaskContainer {
return mContainers;
}
+ void updateTopSplitContainerForDivider(
+ @NonNull DividerPresenter dividerPresenter,
+ @NonNull List<TaskFragmentContainer> outContainersToFinish) {
+ final SplitContainer topSplitContainer = getTopNonFinishingSplitContainer();
+ if (topSplitContainer == null) {
+ return;
+ }
+ final TaskFragmentContainer primaryContainer = topSplitContainer.getPrimaryContainer();
+ final float newRatio = dividerPresenter.calculateNewSplitRatio();
+
+ // If the primary container is fully expanded, we should finish all the associated
+ // secondary containers.
+ if (newRatio == RATIO_EXPANDED_PRIMARY) {
+ for (final SplitContainer splitContainer : mSplitContainers) {
+ if (primaryContainer == splitContainer.getPrimaryContainer()) {
+ outContainersToFinish.add(splitContainer.getSecondaryContainer());
+ }
+ }
+
+ // Temporarily suppress the placeholder rule in the TaskContainer. This will be restored
+ // if a new split is added into the TaskContainer.
+ mPlaceholderRuleSuppressed = true;
+
+ mOverrideSplitType = null;
+ return;
+ }
+
+ final SplitType newSplitType;
+ if (newRatio == RATIO_EXPANDED_SECONDARY) {
+ newSplitType = new ExpandContainersSplitType();
+ // We do not want to apply ExpandContainersSplitType to new split containers.
+ mOverrideSplitType = null;
+ } else {
+ // We save the override RatioSplitType and apply to new split containers.
+ newSplitType = mOverrideSplitType = new RatioSplitType(newRatio);
+ }
+ for (final SplitContainer splitContainer : mSplitContainers) {
+ if (primaryContainer == splitContainer.getPrimaryContainer()) {
+ updateDefaultSplitAttributes(splitContainer, newSplitType);
+ }
+ }
+ }
+
+ @Nullable
+ SplitContainer getTopNonFinishingSplitContainer() {
+ for (int i = mSplitContainers.size() - 1; i >= 0; i--) {
+ final SplitContainer splitContainer = mSplitContainers.get(i);
+ if (!splitContainer.getPrimaryContainer().isFinished()
+ && !splitContainer.getSecondaryContainer().isFinished()) {
+ return splitContainer;
+ }
+ }
+ return null;
+ }
+
private void onTaskFragmentContainerUpdated() {
// TODO(b/300211704): Find a better mechanism to handle the z-order in case we introduce
// another special container that should also be on top in the future.
updateSplitPinContainerIfNecessary();
// Update overlay container after split pin container since the overlay should be on top of
// pin container.
- updateOverlayContainerIfNecessary();
+ updateAlwaysOnTopOverlayIfNecessary();
+ }
+
+ private void updateAlwaysOnTopOverlayIfNecessary() {
+ final List<TaskFragmentContainer> alwaysOnTopOverlays = mContainers
+ .stream().filter(TaskFragmentContainer::isAlwaysOnTopOverlay).toList();
+ if (alwaysOnTopOverlays.size() > 1) {
+ throw new IllegalStateException("There must be at most one always-on-top overlay "
+ + "container per Task");
+ }
+ mAlwaysOnTopOverlayContainer = alwaysOnTopOverlays.isEmpty()
+ ? null : alwaysOnTopOverlays.getFirst();
+ if (mAlwaysOnTopOverlayContainer != null) {
+ moveContainerToLastIfNecessary(mAlwaysOnTopOverlayContainer);
+ }
}
private void updateSplitPinContainerIfNecessary() {
@@ -423,18 +605,6 @@ class TaskContainer {
}
}
- private void updateOverlayContainerIfNecessary() {
- final List<TaskFragmentContainer> overlayContainers = mContainers.stream()
- .filter(TaskFragmentContainer::isOverlay).toList();
- if (overlayContainers.size() > 1) {
- throw new IllegalStateException("There must be at most one overlay container per Task");
- }
- mOverlayContainer = overlayContainers.isEmpty() ? null : overlayContainers.get(0);
- if (mOverlayContainer != null) {
- moveContainerToLastIfNecessary(mOverlayContainer);
- }
- }
-
/** Moves the {@code container} to the last to align taskFragments' z-order. */
private void moveContainerToLastIfNecessary(@NonNull TaskFragmentContainer container) {
final int index = mContainers.indexOf(container);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index a6bf99d4add5..7173b0c95230 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -36,6 +36,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
import java.util.ArrayList;
import java.util.Collections;
@@ -113,6 +114,18 @@ class TaskFragmentContainer {
@NonNull
private final Bundle mLaunchOptions = new Bundle();
+ /**
+ * The associated {@link Activity#getActivityToken()} of the overlay container.
+ * Must be {@code null} for non-overlay container.
+ * <p>
+ * If an overlay container is associated with an activity, this overlay container will be
+ * dismissed when the associated activity is destroyed. If the overlay container is visible,
+ * activity will be launched on top of the overlay container and expanded to fill the parent
+ * container.
+ */
+ @Nullable
+ private final IBinder mAssociatedActivityToken;
+
/** Indicates whether the container was cleaned up after the last activity was removed. */
private boolean mIsFinished;
@@ -172,23 +185,14 @@ class TaskFragmentContainer {
private boolean mIsIsolatedNavigationEnabled;
/**
- * Whether to apply dimming on the parent Task that was requested last.
+ * Whether this TaskFragment is pinned.
*/
- private boolean mLastDimOnTask;
+ private boolean mIsPinned;
/**
- * @see #TaskFragmentContainer(Activity, Intent, TaskContainer, SplitController,
- * TaskFragmentContainer, String, Bundle)
+ * Whether to apply dimming on the parent Task that was requested last.
*/
- TaskFragmentContainer(@Nullable Activity pendingAppearedActivity,
- @Nullable Intent pendingAppearedIntent,
- @NonNull TaskContainer taskContainer,
- @NonNull SplitController controller,
- @Nullable TaskFragmentContainer pairedPrimaryContainer) {
- this(pendingAppearedActivity, pendingAppearedIntent, taskContainer,
- controller, pairedPrimaryContainer, null /* overlayTag */,
- null /* launchOptions */);
- }
+ private boolean mLastDimOnTask;
/**
* Creates a container with an existing activity that will be re-parented to it in a window
@@ -197,12 +201,14 @@ class TaskFragmentContainer {
* @param overlayTag Sets to indicate this taskFragment is an overlay container
* @param launchOptions The launch options to create this container. Must not be
* {@code null} for an overlay container
+ * @param associatedActivity the associated activity of the overlay container. Must be
+ * {@code null} for a non-overlay container.
*/
- TaskFragmentContainer(@Nullable Activity pendingAppearedActivity,
+ private TaskFragmentContainer(@Nullable Activity pendingAppearedActivity,
@Nullable Intent pendingAppearedIntent, @NonNull TaskContainer taskContainer,
@NonNull SplitController controller,
@Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag,
- @Nullable Bundle launchOptions) {
+ @Nullable Bundle launchOptions, @Nullable Activity associatedActivity) {
if ((pendingAppearedActivity == null && pendingAppearedIntent == null)
|| (pendingAppearedActivity != null && pendingAppearedIntent != null)) {
throw new IllegalArgumentException(
@@ -212,9 +218,9 @@ class TaskFragmentContainer {
mToken = new Binder("TaskFragmentContainer");
mTaskContainer = taskContainer;
mOverlayTag = overlayTag;
- if (overlayTag != null) {
- Objects.requireNonNull(launchOptions);
- }
+ mAssociatedActivityToken = associatedActivity != null
+ ? associatedActivity.getActivityToken() : null;
+
if (launchOptions != null) {
mLaunchOptions.putAll(launchOptions);
}
@@ -249,6 +255,15 @@ class TaskFragmentContainer {
addPendingAppearedActivity(pendingAppearedActivity);
}
mPendingAppearedIntent = pendingAppearedIntent;
+
+ // Save the information necessary for restoring the overlay when needed.
+ if (Flags.fixPipRestoreToOverlay() && overlayTag != null && pendingAppearedIntent != null
+ && associatedActivity != null && !associatedActivity.isFinishing()) {
+ final IBinder associatedActivityToken = associatedActivity.getActivityToken();
+ final OverlayContainerRestoreParams params = new OverlayContainerRestoreParams(mToken,
+ launchOptions, pendingAppearedIntent);
+ mController.mOverlayRestoreParams.put(associatedActivityToken, params);
+ }
}
/**
@@ -420,14 +435,38 @@ class TaskFragmentContainer {
}
}
+ /** Called when the activity {@link Activity#isFinishing()} and paused. */
+ void onFinishingActivityPaused(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder activityToken) {
+ finishSelfWithActivityIfNeeded(wct, activityToken);
+ }
+
/** Called when the activity is destroyed. */
- void onActivityDestroyed(@NonNull IBinder activityToken) {
+ void onActivityDestroyed(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder activityToken) {
removePendingAppearedActivity(activityToken);
if (mInfo != null) {
// Remove the activity now because there can be a delay before the server callback.
mInfo.getActivities().remove(activityToken);
}
mActivitiesToFinishOnExit.remove(activityToken);
+ finishSelfWithActivityIfNeeded(wct, activityToken);
+ }
+
+ @VisibleForTesting
+ void finishSelfWithActivityIfNeeded(@NonNull WindowContainerTransaction wct,
+ @NonNull IBinder activityToken) {
+ if (mIsFinished) {
+ return;
+ }
+ // Early return if this container is not an overlay with activity association.
+ if (!isOverlayWithActivityAssociation()) {
+ return;
+ }
+ if (mAssociatedActivityToken == activityToken) {
+ // If the associated activity is destroyed, also finish this overlay container.
+ mController.mPresenter.cleanupContainer(wct, this, false /* shouldFinishDependent */);
+ }
}
@Nullable
@@ -456,7 +495,7 @@ class TaskFragmentContainer {
// sure the controller considers this container as the one containing the activity.
// This is needed when the activity is added as pending appeared activity to one
// TaskFragment while it is also an appeared activity in another.
- return mController.getContainerWithActivity(activityToken) == this;
+ return mTaskContainer.getContainerWithActivity(activityToken) == this;
}
/** Whether this activity has appeared in the TaskFragment on the server side. */
@@ -748,6 +787,10 @@ class TaskFragmentContainer {
}
}
+ @NonNull Rect getLastRequestedBounds() {
+ return mLastRequestedBounds;
+ }
+
/**
* Checks if last requested windowing mode is equal to the provided value.
* @see WindowContainerTransaction#setWindowingMode
@@ -845,6 +888,34 @@ class TaskFragmentContainer {
mIsIsolatedNavigationEnabled = isolatedNavigationEnabled;
}
+ /**
+ * Returns whether this container is pinned.
+ *
+ * @see android.window.TaskFragmentOperation#OP_TYPE_SET_PINNED
+ */
+ boolean isPinned() {
+ return mIsPinned;
+ }
+
+ /**
+ * Sets whether to pin this container or not.
+ *
+ * @see #isPinned()
+ */
+ void setPinned(boolean pinned) {
+ mIsPinned = pinned;
+ }
+
+ /**
+ * Indicates to skip activity resolving if the activity is from this container.
+ *
+ * @see #isIsolatedNavigationEnabled()
+ * @see #isPinned()
+ */
+ boolean shouldSkipActivityResolving() {
+ return isIsolatedNavigationEnabled() || isPinned();
+ }
+
/** Sets whether to apply dim on the parent Task. */
void setLastDimOnTask(boolean lastDimOnTask) {
mLastDimOnTask = lastDimOnTask;
@@ -957,6 +1028,32 @@ class TaskFragmentContainer {
return mLaunchOptions;
}
+ /**
+ * Returns the associated Activity token of this overlay container. It must be {@code null}
+ * for non-overlay container.
+ * <p>
+ * If an overlay container is associated with an activity, this overlay container will be
+ * dismissed when the associated activity is destroyed. If the overlay container is visible,
+ * activity will be launched on top of the overlay container and expanded to fill the parent
+ * container.
+ */
+ @Nullable
+ IBinder getAssociatedActivityToken() {
+ return mAssociatedActivityToken;
+ }
+
+ /**
+ * Returns {@code true} if the overlay container should be always on top, which should be
+ * a non-fill-parent overlay without activity association.
+ */
+ boolean isAlwaysOnTopOverlay() {
+ return isOverlay() && mAssociatedActivityToken == null;
+ }
+
+ boolean isOverlayWithActivityAssociation() {
+ return isOverlay() && mAssociatedActivityToken != null;
+ }
+
@Override
public String toString() {
return toString(true /* includeContainersToFinishOnExit */);
@@ -976,6 +1073,7 @@ class TaskFragmentContainer {
+ " runningActivityCount=" + getRunningActivityCount()
+ " isFinished=" + mIsFinished
+ " overlayTag=" + mOverlayTag
+ + " associatedActivityToken=" + mAssociatedActivityToken
+ " lastRequestedBounds=" + mLastRequestedBounds
+ " pendingAppearedActivities=" + mPendingAppearedActivities
+ (includeContainersToFinishOnExit ? " containersToFinishOnExit="
@@ -997,4 +1095,136 @@ class TaskFragmentContainer {
}
return sb.append("]").toString();
}
+
+ static final class Builder {
+ @NonNull
+ private final SplitController mSplitController;
+
+ // The parent Task id of the new TaskFragment.
+ private final int mTaskId;
+
+ // The activity in the same Task so that we can get the Task bounds if needed.
+ @NonNull
+ private final Activity mActivityInTask;
+
+ // The activity that will be reparented to the TaskFragment.
+ @Nullable
+ private Activity mPendingAppearedActivity;
+
+ // The Intent that will be started in the TaskFragment.
+ @Nullable
+ private Intent mPendingAppearedIntent;
+
+ // The paired primary {@link TaskFragmentContainer}. When it is set, the new container
+ // will be added right above it.
+ @Nullable
+ private TaskFragmentContainer mPairedPrimaryContainer;
+
+ // The launch options bundle to create a container. Must be specified for overlay container.
+ @Nullable
+ private Bundle mLaunchOptions;
+
+ // The tag for the new created overlay container. This is required when creating an
+ // overlay container.
+ @Nullable
+ private String mOverlayTag;
+
+ // The associated activity of the overlay container. Must be {@code null} for a
+ // non-overlay container.
+ @Nullable
+ private Activity mAssociatedActivity;
+
+ Builder(@NonNull SplitController splitController, int taskId,
+ @Nullable Activity activityInTask) {
+ if (taskId <= 0) {
+ throw new IllegalArgumentException("taskId is invalid, " + taskId);
+ }
+
+ mSplitController = splitController;
+ mTaskId = taskId;
+ mActivityInTask = activityInTask;
+ }
+
+ @NonNull
+ Builder setPendingAppearedActivity(@Nullable Activity pendingAppearedActivity) {
+ mPendingAppearedActivity = pendingAppearedActivity;
+ return this;
+ }
+
+ @NonNull
+ Builder setPendingAppearedIntent(@Nullable Intent pendingAppearedIntent) {
+ mPendingAppearedIntent = pendingAppearedIntent;
+ return this;
+ }
+
+ @NonNull
+ Builder setPairedPrimaryContainer(@Nullable TaskFragmentContainer pairedPrimaryContainer) {
+ mPairedPrimaryContainer = pairedPrimaryContainer;
+ return this;
+ }
+
+ @NonNull
+ Builder setLaunchOptions(@Nullable Bundle launchOptions) {
+ mLaunchOptions = launchOptions;
+ return this;
+ }
+
+ @NonNull
+ Builder setOverlayTag(@Nullable String overlayTag) {
+ mOverlayTag = overlayTag;
+ return this;
+ }
+
+ @NonNull
+ Builder setAssociatedActivity(@Nullable Activity associatedActivity) {
+ mAssociatedActivity = associatedActivity;
+ return this;
+ }
+
+ @NonNull
+ TaskFragmentContainer build() {
+ if (mOverlayTag != null) {
+ Objects.requireNonNull(mLaunchOptions);
+ } else if (mAssociatedActivity != null) {
+ throw new IllegalArgumentException("Associated activity must be null for "
+ + "non-overlay activity.");
+ }
+
+ TaskContainer taskContainer = mSplitController.getTaskContainer(mTaskId);
+ if (taskContainer == null && mActivityInTask == null) {
+ throw new IllegalArgumentException("mActivityInTask must be set.");
+ }
+
+ if (taskContainer == null) {
+ // Adding a TaskContainer if no existed one.
+ taskContainer = new TaskContainer(mTaskId, mActivityInTask);
+ mSplitController.addTaskContainer(mTaskId, taskContainer);
+ }
+
+ return new TaskFragmentContainer(mPendingAppearedActivity, mPendingAppearedIntent,
+ taskContainer, mSplitController, mPairedPrimaryContainer, mOverlayTag,
+ mLaunchOptions, mAssociatedActivity);
+ }
+ }
+
+ static class OverlayContainerRestoreParams {
+ /** The token of the overlay container */
+ @NonNull
+ final IBinder mOverlayToken;
+
+ /** The launch options to create this container. */
+ @NonNull
+ final Bundle mOptions;
+
+ /** The Intent that used to be started in the overlay container. */
+ @NonNull
+ final Intent mIntent;
+
+ OverlayContainerRestoreParams(@NonNull IBinder overlayToken, @NonNull Bundle options,
+ @NonNull Intent intent) {
+ mOverlayToken = overlayToken;
+ mOptions = options;
+ mIntent = intent;
+ }
+ }
}
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 4fd11c495529..070fa5bcfae4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java
@@ -45,6 +45,7 @@ import androidx.window.common.CommonFoldingFeature;
import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;
import androidx.window.extensions.core.util.function.Consumer;
+import androidx.window.extensions.util.DeduplicateConsumer;
import java.util.ArrayList;
import java.util.Collections;
@@ -62,7 +63,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
private final Object mLock = new Object();
@GuardedBy("mLock")
- private final Map<Context, Consumer<WindowLayoutInfo>> mWindowLayoutChangeListeners =
+ private final Map<Context, DeduplicateConsumer<WindowLayoutInfo>> mWindowLayoutChangeListeners =
new ArrayMap<>();
@GuardedBy("mLock")
@@ -130,7 +131,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
if (mWindowLayoutChangeListeners.containsKey(context)
// In theory this method can be called on the same consumer with different
// context.
- || mWindowLayoutChangeListeners.containsValue(consumer)) {
+ || containsConsumer(consumer)) {
return;
}
if (!context.isUiContext()) {
@@ -141,7 +142,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
WindowLayoutInfo newWindowLayout = getWindowLayoutInfo(context, features);
consumer.accept(newWindowLayout);
});
- mWindowLayoutChangeListeners.put(context, consumer);
+ mWindowLayoutChangeListeners.put(context, new DeduplicateConsumer<>(consumer));
final IBinder windowContextToken = context.getWindowContextToken();
if (windowContextToken != null) {
@@ -176,19 +177,35 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent {
@Override
public void removeWindowLayoutInfoListener(@NonNull Consumer<WindowLayoutInfo> consumer) {
synchronized (mLock) {
+ DeduplicateConsumer<WindowLayoutInfo> consumerToRemove = null;
for (Context context : mWindowLayoutChangeListeners.keySet()) {
- if (!mWindowLayoutChangeListeners.get(context).equals(consumer)) {
+ final DeduplicateConsumer<WindowLayoutInfo> deduplicateConsumer =
+ mWindowLayoutChangeListeners.get(context);
+ if (!deduplicateConsumer.matchesConsumer(consumer)) {
continue;
}
final IBinder token = context.getWindowContextToken();
+ consumerToRemove = deduplicateConsumer;
if (token != null) {
context.unregisterComponentCallbacks(mConfigurationChangeListeners.get(token));
mConfigurationChangeListeners.remove(token);
}
break;
}
- mWindowLayoutChangeListeners.values().remove(consumer);
+ if (consumerToRemove != null) {
+ mWindowLayoutChangeListeners.values().remove(consumerToRemove);
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
+ private boolean containsConsumer(@NonNull Consumer<WindowLayoutInfo> consumer) {
+ for (DeduplicateConsumer<WindowLayoutInfo> c : mWindowLayoutChangeListeners.values()) {
+ if (c.matchesConsumer(consumer)) {
+ return true;
+ }
}
+ return false;
}
@GuardedBy("mLock")
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/util/DeduplicateConsumer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/util/DeduplicateConsumer.java
new file mode 100644
index 000000000000..ee271aa57003
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/util/DeduplicateConsumer.java
@@ -0,0 +1,63 @@
+/*
+ * 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.util;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.window.extensions.core.util.function.Consumer;
+
+/**
+ * A utility class that will not report a value if it is the same as the last reported value.
+ * @param <T> generic values to be reported.
+ */
+public class DeduplicateConsumer<T> implements Consumer<T> {
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ @Nullable
+ private T mLastReportedValue = null;
+ @NonNull
+ private final Consumer<T> mConsumer;
+
+ public DeduplicateConsumer(@NonNull Consumer<T> consumer) {
+ mConsumer = consumer;
+ }
+
+ /**
+ * Returns {@code true} if the given consumer matches this object or the wrapped
+ * {@link Consumer}, {@code false} otherwise
+ */
+ public boolean matchesConsumer(@NonNull Consumer<T> consumer) {
+ return consumer == this || mConsumer.equals(consumer);
+ }
+
+ /**
+ * Accepts a new value and relays it if it is different from
+ * the last reported value.
+ * @param value to report if different.
+ */
+ @Override
+ public void accept(@NonNull T value) {
+ synchronized (mLock) {
+ if (mLastReportedValue != null && mLastReportedValue.equals(value)) {
+ return;
+ }
+ mLastReportedValue = value;
+ }
+ mConsumer.accept(value);
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
index 56c3bce87d6e..339908a3a9a4 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SampleSidecarImpl.java
@@ -16,16 +16,10 @@
package androidx.window.sidecar;
-import static android.view.Display.DEFAULT_DISPLAY;
-
-import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation;
-import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect;
-
+import android.annotation.Nullable;
import android.app.Activity;
-import android.app.ActivityThread;
import android.app.Application;
import android.content.Context;
-import android.graphics.Rect;
import android.hardware.devicestate.DeviceStateManager;
import android.os.Bundle;
import android.os.IBinder;
@@ -38,7 +32,6 @@ import androidx.window.common.RawFoldingFeatureProducer;
import androidx.window.util.BaseDataProducer;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
/**
@@ -76,64 +69,13 @@ class SampleSidecarImpl extends StubSidecar {
@NonNull
@Override
public SidecarDeviceState getDeviceState() {
- SidecarDeviceState deviceState = new SidecarDeviceState();
- deviceState.posture = deviceStateFromFeature();
- return deviceState;
- }
-
- private int deviceStateFromFeature() {
- for (int i = 0; i < mStoredFeatures.size(); i++) {
- CommonFoldingFeature feature = mStoredFeatures.get(i);
- final int state = feature.getState();
- switch (state) {
- case CommonFoldingFeature.COMMON_STATE_FLAT:
- return SidecarDeviceState.POSTURE_OPENED;
- case CommonFoldingFeature.COMMON_STATE_HALF_OPENED:
- return SidecarDeviceState.POSTURE_HALF_OPENED;
- case CommonFoldingFeature.COMMON_STATE_UNKNOWN:
- return SidecarDeviceState.POSTURE_UNKNOWN;
- }
- }
- return SidecarDeviceState.POSTURE_UNKNOWN;
+ return SidecarHelper.calculateDeviceState(mStoredFeatures);
}
@NonNull
@Override
public SidecarWindowLayoutInfo getWindowLayoutInfo(@NonNull IBinder windowToken) {
- Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken);
- SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo();
- if (activity == null) {
- return windowLayoutInfo;
- }
- windowLayoutInfo.displayFeatures = getDisplayFeatures(activity);
- return windowLayoutInfo;
- }
-
- private List<SidecarDisplayFeature> getDisplayFeatures(@NonNull Activity activity) {
- int displayId = activity.getDisplay().getDisplayId();
- if (displayId != DEFAULT_DISPLAY) {
- return Collections.emptyList();
- }
-
- if (activity.isInMultiWindowMode()) {
- // It is recommended not to report any display features in multi-window mode, since it
- // won't be possible to synchronize the display feature positions with window movement.
- return Collections.emptyList();
- }
-
- List<SidecarDisplayFeature> features = new ArrayList<>();
- final int rotation = activity.getResources().getConfiguration().windowConfiguration
- .getDisplayRotation();
- for (CommonFoldingFeature baseFeature : mStoredFeatures) {
- SidecarDisplayFeature feature = new SidecarDisplayFeature();
- Rect featureRect = baseFeature.getRect();
- rotateRectToDisplayRotation(displayId, rotation, featureRect);
- transformToWindowSpaceRect(activity, featureRect);
- feature.setRect(featureRect);
- feature.setType(baseFeature.getType());
- features.add(feature);
- }
- return Collections.unmodifiableList(features);
+ return SidecarHelper.calculateWindowLayoutInfo(windowToken, mStoredFeatures);
}
@Override
@@ -145,13 +87,14 @@ class SampleSidecarImpl extends StubSidecar {
private final class NotifyOnConfigurationChanged extends EmptyLifecycleCallbacksAdapter {
@Override
- public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+ public void onActivityCreated(@NonNull Activity activity,
+ @Nullable Bundle savedInstanceState) {
super.onActivityCreated(activity, savedInstanceState);
onDisplayFeaturesChangedForActivity(activity);
}
@Override
- public void onActivityConfigurationChanged(Activity activity) {
+ public void onActivityConfigurationChanged(@NonNull Activity activity) {
super.onActivityConfigurationChanged(activity);
onDisplayFeaturesChangedForActivity(activity);
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
new file mode 100644
index 000000000000..bb6ab47b144d
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java
@@ -0,0 +1,129 @@
+/*
+ * 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.sidecar;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static androidx.window.util.ExtensionHelper.rotateRectToDisplayRotation;
+import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.app.ActivityThread;
+import android.graphics.Rect;
+import android.os.IBinder;
+
+import androidx.window.common.CommonFoldingFeature;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A utility class for transforming between Sidecar and Extensions features.
+ */
+class SidecarHelper {
+
+ private SidecarHelper() {}
+
+ /**
+ * Returns the {@link SidecarDeviceState} posture that is calculated for the first fold in
+ * the feature list. Sidecar devices only have one fold so we only pick the first one to
+ * determine the state.
+ * @param featureList the {@link CommonFoldingFeature} that are currently active.
+ * @return the {@link SidecarDeviceState} calculated from the {@link List} of
+ * {@link CommonFoldingFeature}.
+ */
+ @SuppressWarnings("deprecation")
+ private static int deviceStateFromFeatureList(@NonNull List<CommonFoldingFeature> featureList) {
+ for (int i = 0; i < featureList.size(); i++) {
+ final CommonFoldingFeature feature = featureList.get(i);
+ final int state = feature.getState();
+ switch (state) {
+ case CommonFoldingFeature.COMMON_STATE_FLAT:
+ return SidecarDeviceState.POSTURE_OPENED;
+ case CommonFoldingFeature.COMMON_STATE_HALF_OPENED:
+ return SidecarDeviceState.POSTURE_HALF_OPENED;
+ case CommonFoldingFeature.COMMON_STATE_UNKNOWN:
+ return SidecarDeviceState.POSTURE_UNKNOWN;
+ case CommonFoldingFeature.COMMON_STATE_NO_FOLDING_FEATURES:
+ return SidecarDeviceState.POSTURE_UNKNOWN;
+ case CommonFoldingFeature.COMMON_STATE_USE_BASE_STATE:
+ return SidecarDeviceState.POSTURE_UNKNOWN;
+ }
+ }
+ return SidecarDeviceState.POSTURE_UNKNOWN;
+ }
+
+ /**
+ * Returns a {@link SidecarDeviceState} calculated from a {@link List} of
+ * {@link CommonFoldingFeature}s.
+ */
+ @SuppressWarnings("deprecation")
+ static SidecarDeviceState calculateDeviceState(
+ @NonNull List<CommonFoldingFeature> featureList) {
+ final SidecarDeviceState deviceState = new SidecarDeviceState();
+ deviceState.posture = deviceStateFromFeatureList(featureList);
+ return deviceState;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static List<SidecarDisplayFeature> calculateDisplayFeatures(
+ @NonNull Activity activity,
+ @NonNull List<CommonFoldingFeature> featureList
+ ) {
+ final int displayId = activity.getDisplay().getDisplayId();
+ if (displayId != DEFAULT_DISPLAY) {
+ return Collections.emptyList();
+ }
+
+ if (activity.isInMultiWindowMode()) {
+ // It is recommended not to report any display features in multi-window mode, since it
+ // won't be possible to synchronize the display feature positions with window movement.
+ return Collections.emptyList();
+ }
+
+ final List<SidecarDisplayFeature> features = new ArrayList<>();
+ final int rotation = activity.getResources().getConfiguration().windowConfiguration
+ .getDisplayRotation();
+ for (CommonFoldingFeature baseFeature : featureList) {
+ final SidecarDisplayFeature feature = new SidecarDisplayFeature();
+ final Rect featureRect = baseFeature.getRect();
+ rotateRectToDisplayRotation(displayId, rotation, featureRect);
+ transformToWindowSpaceRect(activity, featureRect);
+ feature.setRect(featureRect);
+ feature.setType(baseFeature.getType());
+ features.add(feature);
+ }
+ return Collections.unmodifiableList(features);
+ }
+
+ /**
+ * Returns a {@link SidecarWindowLayoutInfo} calculated from the {@link List} of
+ * {@link CommonFoldingFeature}.
+ */
+ @SuppressWarnings("deprecation")
+ static SidecarWindowLayoutInfo calculateWindowLayoutInfo(@NonNull IBinder windowToken,
+ @NonNull List<CommonFoldingFeature> featureList) {
+ final Activity activity = ActivityThread.currentActivityThread().getActivity(windowToken);
+ final SidecarWindowLayoutInfo windowLayoutInfo = new SidecarWindowLayoutInfo();
+ if (activity == null) {
+ return windowLayoutInfo;
+ }
+ windowLayoutInfo.displayFeatures = calculateDisplayFeatures(activity, featureList);
+ return windowLayoutInfo;
+ }
+}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java
index 62959b7b95e9..686a31b6be04 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarProvider.java
@@ -17,25 +17,48 @@
package androidx.window.sidecar;
import android.content.Context;
+import android.view.WindowManager;
+
+import androidx.annotation.Nullable;
/**
* Provider class that will instantiate the library implementation. It must be included in the
* vendor library, and the vendor implementation must match the signature of this class.
*/
public class SidecarProvider {
+
+ private static volatile Boolean sIsWindowExtensionsEnabled;
+
/**
* Provide a simple implementation of {@link SidecarInterface} that can be replaced by
* an OEM by overriding this method.
*/
+ @Nullable
public static SidecarInterface getSidecarImpl(Context context) {
- return new SampleSidecarImpl(context.getApplicationContext());
+ return isWindowExtensionsEnabled()
+ ? new SampleSidecarImpl(context.getApplicationContext())
+ : null;
}
/**
* The support library will use this method to check API version compatibility.
* @return API version string in MAJOR.MINOR.PATCH-description format.
*/
+ @Nullable
public static String getApiVersion() {
- return "1.0.0-reference";
+ return isWindowExtensionsEnabled()
+ ? "1.0.0-reference"
+ : null;
+ }
+
+ private static boolean isWindowExtensionsEnabled() {
+ if (sIsWindowExtensionsEnabled == null) {
+ synchronized (SidecarProvider.class) {
+ if (sIsWindowExtensionsEnabled == null) {
+ sIsWindowExtensionsEnabled = WindowManager.hasWindowExtensionsEnabled();
+ }
+ }
+ }
+ return sIsWindowExtensionsEnabled;
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java
index b9c808a6569b..46c1f3ba4691 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/StubSidecar.java
@@ -17,6 +17,7 @@
package androidx.window.sidecar;
import android.os.IBinder;
+import android.util.Log;
import androidx.annotation.NonNull;
@@ -29,6 +30,8 @@ import java.util.Set;
*/
abstract class StubSidecar implements SidecarInterface {
+ private static final String TAG = "WindowManagerSidecar";
+
private SidecarCallback mSidecarCallback;
final Set<IBinder> mWindowLayoutChangeListenerTokens = new HashSet<>();
private boolean mDeviceStateChangeListenerRegistered;
@@ -61,14 +64,22 @@ abstract class StubSidecar implements SidecarInterface {
void updateDeviceState(SidecarDeviceState newState) {
if (this.mSidecarCallback != null) {
- mSidecarCallback.onDeviceStateChanged(newState);
+ try {
+ mSidecarCallback.onDeviceStateChanged(newState);
+ } catch (AbstractMethodError e) {
+ Log.e(TAG, "App is using an outdated Window Jetpack library", e);
+ }
}
}
void updateWindowLayout(@NonNull IBinder windowToken,
@NonNull SidecarWindowLayoutInfo newLayout) {
if (this.mSidecarCallback != null) {
- mSidecarCallback.onWindowLayoutChanged(windowToken, newLayout);
+ try {
+ mSidecarCallback.onWindowLayoutChanged(windowToken, newLayout);
+ } catch (AbstractMethodError e) {
+ Log.e(TAG, "App is using an outdated Window Jetpack library", e);
+ }
}
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/Android.bp b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
index 4ddbd13978d5..61ea51a35f58 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/Android.bp
+++ b/libs/WindowManager/Jetpack/tests/unittest/Android.bp
@@ -23,6 +23,7 @@ package {
android_test {
name: "WMJetpackUnitTests",
+ team: "trendy_team_windowing_sdk",
// To make the test run via TEST_MAPPING
test_suites: ["device-tests"],
@@ -32,6 +33,7 @@ android_test {
static_libs: [
"androidx.window.extensions",
+ "androidx.window.extensions.core_core",
"junit",
"androidx.test.runner",
"androidx.test.rules",
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
index f471af052bf2..4267749dfa6b 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
@@ -16,12 +16,15 @@
package androidx.window.extensions;
-import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+import static androidx.window.extensions.WindowExtensionsImpl.EXTENSIONS_VERSION_CURRENT_PLATFORM;
import static com.google.common.truth.Truth.assertThat;
-import android.app.ActivityTaskManager;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
import android.platform.test.annotations.Presubmit;
+import android.view.WindowManager;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -42,25 +45,61 @@ import org.junit.runner.RunWith;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class WindowExtensionsTest {
+
private WindowExtensions mExtensions;
+ private int mVersion;
@Before
public void setUp() {
mExtensions = WindowExtensionsProvider.getWindowExtensions();
+ mVersion = mExtensions.getVendorApiLevel();
+ }
+
+ @Test
+ public void testGetVendorApiLevel_extensionsEnabled_matchesCurrentVersion() {
+ assumeTrue(WindowManager.hasWindowExtensionsEnabled());
+ assertThat(mVersion).isEqualTo(EXTENSIONS_VERSION_CURRENT_PLATFORM);
}
@Test
- public void testGetWindowLayoutComponent() {
+ public void testGetVendorApiLevel_extensionsDisabled_returnsZero() {
+ assumeFalse(WindowManager.hasWindowExtensionsEnabled());
+ assertThat(mVersion).isEqualTo(0);
+ }
+
+ @Test
+ public void testGetWindowLayoutComponent_extensionsEnabled_returnsImplementation() {
+ assumeTrue(WindowManager.hasWindowExtensionsEnabled());
assertThat(mExtensions.getWindowLayoutComponent()).isNotNull();
}
@Test
- public void testGetActivityEmbeddingComponent() {
- if (ActivityTaskManager.supportsMultiWindow(getInstrumentation().getContext())) {
- assertThat(mExtensions.getActivityEmbeddingComponent()).isNotNull();
- } else {
- assertThat(mExtensions.getActivityEmbeddingComponent()).isNull();
- }
+ public void testGetWindowLayoutComponent_extensionsDisabled_returnsNull() {
+ assumeFalse(WindowManager.hasWindowExtensionsEnabled());
+ assertThat(mExtensions.getWindowLayoutComponent()).isNull();
+ }
+ @Test
+ public void testGetActivityEmbeddingComponent_featureDisabled_returnsNull() {
+ assumeFalse(WindowExtensionsImpl.isActivityEmbeddingEnabled());
+ assertThat(mExtensions.getActivityEmbeddingComponent()).isNull();
+ }
+
+ @Test
+ public void testGetActivityEmbeddingComponent_featureEnabled_returnsImplementation() {
+ assumeTrue(WindowExtensionsImpl.isActivityEmbeddingEnabled());
+ assertThat(mExtensions.getActivityEmbeddingComponent()).isNotNull();
+ }
+
+ @Test
+ public void testGetWindowAreaComponent_extensionsEnabled_returnsImplementation() {
+ assumeTrue(WindowManager.hasWindowExtensionsEnabled());
+ assertThat(mExtensions.getWindowAreaComponent()).isNotNull();
+ }
+
+ @Test
+ public void testGetWindowAreaComponent_extensionsDisabled_returnsNull() {
+ assumeFalse(WindowManager.hasWindowExtensionsEnabled());
+ assertThat(mExtensions.getWindowAreaComponent()).isNull();
}
@Test
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
new file mode 100644
index 000000000000..4f51815ed05d
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
@@ -0,0 +1,941 @@
+/*
+ * 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.embedding;
+
+import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE;
+import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE;
+
+import static androidx.window.extensions.embedding.DividerPresenter.FLING_ANIMATION_DURATION;
+import static androidx.window.extensions.embedding.DividerPresenter.FLING_ANIMATION_INTERPOLATOR;
+import static androidx.window.extensions.embedding.DividerPresenter.MIN_DISMISS_VELOCITY_DP_PER_SECOND;
+import static androidx.window.extensions.embedding.DividerPresenter.MIN_FLING_VELOCITY_DP_PER_SECOND;
+import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider;
+import static androidx.window.extensions.embedding.DividerPresenter.getInitialDividerPosition;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT;
+import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.animation.ValueAnimator;
+import android.app.Activity;
+import android.content.res.Configuration;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
+import android.os.Binder;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.Window;
+import android.window.TaskFragmentOperation;
+import android.window.TaskFragmentParentInfo;
+import android.window.WindowContainerTransaction;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.window.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Test class for {@link DividerPresenter}.
+ *
+ * Build/Install/Run:
+ * atest WMJetpackUnitTests:DividerPresenterTest
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DividerPresenterTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
+
+ @Rule
+ public final SetFlagsRule mSetFlagRule = new SetFlagsRule();
+
+ private static final int MOCK_TASK_ID = 1234;
+
+ @Mock
+ private DividerPresenter.Renderer mRenderer;
+
+ @Mock
+ private WindowContainerTransaction mTransaction;
+
+ @Mock
+ private TaskFragmentParentInfo mParentInfo;
+
+ @Mock
+ private TaskContainer mTaskContainer;
+
+ @Mock
+ private DividerPresenter.DragEventCallback mDragEventCallback;
+
+ @Mock
+ private SplitContainer mSplitContainer;
+
+ @Mock
+ private SurfaceControl mSurfaceControl;
+
+ private DividerPresenter mDividerPresenter;
+
+ private final IBinder mPrimaryContainerToken = new Binder();
+
+ private final IBinder mSecondaryContainerToken = new Binder();
+
+ private final IBinder mAnotherContainerToken = new Binder();
+
+ private DividerPresenter.Properties mProperties;
+
+ private static final DividerAttributes DEFAULT_DIVIDER_ATTRIBUTES =
+ new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE).build();
+
+ private static final DividerAttributes ANOTHER_DIVIDER_ATTRIBUTES =
+ new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+ .setWidthDp(10).build();
+
+ @Before
+ public void setUp() {
+ mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG);
+
+ when(mTaskContainer.getTaskId()).thenReturn(MOCK_TASK_ID);
+
+ when(mParentInfo.getDisplayId()).thenReturn(Display.DEFAULT_DISPLAY);
+ when(mParentInfo.getConfiguration()).thenReturn(new Configuration());
+ when(mParentInfo.getDecorSurface()).thenReturn(mSurfaceControl);
+
+ when(mSplitContainer.getCurrentSplitAttributes()).thenReturn(
+ new SplitAttributes.Builder()
+ .setDividerAttributes(DEFAULT_DIVIDER_ATTRIBUTES)
+ .build());
+ final Rect mockTaskBounds = new Rect(0, 0, 2000, 1000);
+ final TaskFragmentContainer mockPrimaryContainer =
+ createMockTaskFragmentContainer(
+ mPrimaryContainerToken, new Rect(0, 0, 950, 1000));
+ final TaskFragmentContainer mockSecondaryContainer =
+ createMockTaskFragmentContainer(
+ mSecondaryContainerToken, new Rect(1000, 0, 2000, 1000));
+ when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer);
+ when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer);
+
+ mProperties = new DividerPresenter.Properties(
+ new Configuration(),
+ DEFAULT_DIVIDER_ATTRIBUTES,
+ mSurfaceControl,
+ getInitialDividerPosition(
+ mockPrimaryContainer, mockSecondaryContainer, mockTaskBounds,
+ 50 /* divideWidthPx */, false /* isDraggableExpandType */,
+ true /* isVerticalSplit */, false /* isReversedLayout */),
+ true /* isVerticalSplit */,
+ false /* isReversedLayout */,
+ Display.DEFAULT_DISPLAY,
+ false /* isDraggableExpandType */,
+ mockPrimaryContainer,
+ mockSecondaryContainer
+ );
+
+ mDividerPresenter = new DividerPresenter(
+ MOCK_TASK_ID, mDragEventCallback, mock(Executor.class));
+ mDividerPresenter.mProperties = mProperties;
+ mDividerPresenter.mRenderer = mRenderer;
+ mDividerPresenter.mDecorSurfaceOwner = mPrimaryContainerToken;
+ }
+
+ @Test
+ public void testUpdateDivider() {
+ when(mSplitContainer.getCurrentSplitAttributes()).thenReturn(
+ new SplitAttributes.Builder()
+ .setDividerAttributes(ANOTHER_DIVIDER_ATTRIBUTES)
+ .build());
+ mDividerPresenter.updateDivider(
+ mTransaction,
+ mParentInfo,
+ mSplitContainer);
+
+ assertNotEquals(mProperties, mDividerPresenter.mProperties);
+ verify(mRenderer).update();
+ verify(mTransaction, never()).addTaskFragmentOperation(any(), any());
+ }
+
+ @Test
+ public void testUpdateDivider_updateDecorSurfaceOwnerIfPrimaryContainerChanged() {
+ final TaskFragmentContainer mockPrimaryContainer =
+ createMockTaskFragmentContainer(
+ mAnotherContainerToken, new Rect(0, 0, 750, 1000));
+ final TaskFragmentContainer mockSecondaryContainer =
+ createMockTaskFragmentContainer(
+ mSecondaryContainerToken, new Rect(800, 0, 2000, 1000));
+ when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer);
+ when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer);
+ mDividerPresenter.updateDivider(
+ mTransaction,
+ mParentInfo,
+ mSplitContainer);
+
+ assertNotEquals(mProperties, mDividerPresenter.mProperties);
+ verify(mRenderer).update();
+ final TaskFragmentOperation operation = new TaskFragmentOperation.Builder(
+ OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE)
+ .build();
+ assertEquals(mAnotherContainerToken, mDividerPresenter.mDecorSurfaceOwner);
+ verify(mTransaction).addTaskFragmentOperation(mAnotherContainerToken, operation);
+ }
+
+ @Test
+ public void testUpdateDivider_noChangeIfPropertiesIdentical() {
+ mDividerPresenter.updateDivider(
+ mTransaction,
+ mParentInfo,
+ mSplitContainer);
+
+ assertEquals(mProperties, mDividerPresenter.mProperties);
+ verify(mRenderer, never()).update();
+ verify(mTransaction, never()).addTaskFragmentOperation(any(), any());
+ }
+
+ @Test
+ public void testUpdateDivider_dividerRemovedWhenSplitContainerIsNull() {
+ mDividerPresenter.updateDivider(
+ mTransaction,
+ mParentInfo,
+ null /* splitContainer */);
+ 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_dividerRemovedWhenDividerAttributesIsNull() {
+ when(mSplitContainer.getCurrentSplitAttributes()).thenReturn(
+ new SplitAttributes.Builder().setDividerAttributes(null).build());
+ mDividerPresenter.updateDivider(
+ mTransaction,
+ mParentInfo,
+ mSplitContainer);
+ 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 testSanitizeDividerAttributes_setDefaultValues() {
+ DividerAttributes attributes =
+ new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE).build();
+ DividerAttributes sanitized = DividerPresenter.sanitizeDividerAttributes(attributes);
+
+ assertEquals(DividerAttributes.DIVIDER_TYPE_DRAGGABLE, sanitized.getDividerType());
+ assertEquals(DividerPresenter.DEFAULT_DIVIDER_WIDTH_DP, sanitized.getWidthDp());
+ assertEquals(DividerPresenter.DEFAULT_MIN_RATIO, sanitized.getPrimaryMinRatio(),
+ 0.0f /* delta */);
+ assertEquals(DividerPresenter.DEFAULT_MAX_RATIO, sanitized.getPrimaryMaxRatio(),
+ 0.0f /* delta */);
+ }
+
+ @Test
+ public void testSanitizeDividerAttributes_setDefaultValues_fixedDivider() {
+ DividerAttributes attributes =
+ new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_FIXED).build();
+ DividerAttributes sanitized = DividerPresenter.sanitizeDividerAttributes(attributes);
+
+ assertEquals(DividerAttributes.DIVIDER_TYPE_FIXED, sanitized.getDividerType());
+ assertEquals(DividerPresenter.DEFAULT_DIVIDER_WIDTH_DP, sanitized.getWidthDp());
+
+ // The ratios should not be set for fixed divider
+ assertEquals(DividerAttributes.RATIO_SYSTEM_DEFAULT, sanitized.getPrimaryMinRatio(),
+ 0.0f /* delta */);
+ assertEquals(DividerAttributes.RATIO_SYSTEM_DEFAULT, sanitized.getPrimaryMaxRatio(),
+ 0.0f /* delta */);
+ }
+
+ @Test
+ public void testSanitizeDividerAttributes_notChangingValidValues() {
+ DividerAttributes attributes =
+ new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+ .setWidthDp(24)
+ .setPrimaryMinRatio(0.3f)
+ .setPrimaryMaxRatio(0.7f)
+ .build();
+ DividerAttributes sanitized = DividerPresenter.sanitizeDividerAttributes(attributes);
+
+ assertEquals(attributes, sanitized);
+ }
+
+ @Test
+ public void testGetBoundsOffsetForDivider_ratioSplitType() {
+ final int dividerWidthPx = 100;
+ final float splitRatio = 0.25f;
+ final SplitAttributes.SplitType splitType =
+ new SplitAttributes.SplitType.RatioSplitType(splitRatio);
+ final int expectedTopLeftOffset = 25;
+ final int expectedBottomRightOffset = 75;
+
+ assertDividerOffsetEquals(
+ dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset);
+ }
+
+ @Test
+ public void testGetBoundsOffsetForDivider_ratioSplitType_withRounding() {
+ final int dividerWidthPx = 101;
+ final float splitRatio = 0.25f;
+ final SplitAttributes.SplitType splitType =
+ new SplitAttributes.SplitType.RatioSplitType(splitRatio);
+ final int expectedTopLeftOffset = 25;
+ final int expectedBottomRightOffset = 76;
+
+ assertDividerOffsetEquals(
+ dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset);
+ }
+
+ @Test
+ public void testGetBoundsOffsetForDivider_hingeSplitType() {
+ final int dividerWidthPx = 100;
+ final SplitAttributes.SplitType splitType =
+ new SplitAttributes.SplitType.HingeSplitType(
+ new SplitAttributes.SplitType.RatioSplitType(0.5f));
+
+ final int expectedTopLeftOffset = 50;
+ final int expectedBottomRightOffset = 50;
+
+ assertDividerOffsetEquals(
+ dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset);
+ }
+
+ @Test
+ public void testGetBoundsOffsetForDivider_expandContainersSplitType() {
+ final int dividerWidthPx = 100;
+ final SplitAttributes.SplitType splitType =
+ new SplitAttributes.SplitType.ExpandContainersSplitType();
+ // Always return 0 for ExpandContainersSplitType as divider is not needed.
+ final int expectedTopLeftOffset = 0;
+ final int expectedBottomRightOffset = 0;
+
+ assertDividerOffsetEquals(
+ dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset);
+ }
+
+ @Test
+ public void testCalculateDividerPosition() {
+ final MotionEvent event = mock(MotionEvent.class);
+ final Rect taskBounds = new Rect(100, 200, 1000, 2000);
+ final int dividerWidthPx = 50;
+ final DividerAttributes dividerAttributes =
+ new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+ .setPrimaryMinRatio(0.3f)
+ .setPrimaryMaxRatio(0.8f)
+ .build();
+
+ // Left-to-right split
+ when(event.getRawX()).thenReturn(500f); // Touch event is in display space
+ assertEquals(
+ // Touch position is in task space is 400, then minus half of divider width.
+ 375,
+ DividerPresenter.calculateDividerPosition(
+ event,
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ true /* isVerticalSplit */,
+ 0 /* minPosition */,
+ 900 /* maxPosition */));
+
+ // Top-to-bottom split
+ when(event.getRawY()).thenReturn(1000f); // Touch event is in display space
+ assertEquals(
+ // Touch position is in task space is 800, then minus half of divider width.
+ 775,
+ DividerPresenter.calculateDividerPosition(
+ event,
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ false /* isVerticalSplit */,
+ 0 /* minPosition */,
+ 900 /* maxPosition */));
+ }
+
+ @Test
+ public void testCalculateMinPosition() {
+ final Rect taskBounds = new Rect(100, 200, 1000, 2000);
+ final int dividerWidthPx = 50;
+ final DividerAttributes dividerAttributes =
+ new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+ .setPrimaryMinRatio(0.3f)
+ .setPrimaryMaxRatio(0.8f)
+ .build();
+
+ // Left-to-right split
+ assertEquals(
+ 255, /* (1000 - 100 - 50) * 0.3 */
+ DividerPresenter.calculateMinPosition(
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ true /* isVerticalSplit */,
+ false /* isReversedLayout */));
+
+ // Top-to-bottom split
+ assertEquals(
+ 525, /* (2000 - 200 - 50) * 0.3 */
+ DividerPresenter.calculateMinPosition(
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ false /* isVerticalSplit */,
+ false /* isReversedLayout */));
+
+ // Right-to-left split
+ assertEquals(
+ 170, /* (1000 - 100 - 50) * (1 - 0.8) */
+ DividerPresenter.calculateMinPosition(
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ true /* isVerticalSplit */,
+ true /* isReversedLayout */));
+ }
+
+ @Test
+ public void testCalculateMaxPosition() {
+ final Rect taskBounds = new Rect(100, 200, 1000, 2000);
+ final int dividerWidthPx = 50;
+ final DividerAttributes dividerAttributes =
+ new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE)
+ .setPrimaryMinRatio(0.3f)
+ .setPrimaryMaxRatio(0.8f)
+ .build();
+
+ // Left-to-right split
+ assertEquals(
+ 680, /* (1000 - 100 - 50) * 0.8 */
+ DividerPresenter.calculateMaxPosition(
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ true /* isVerticalSplit */,
+ false /* isReversedLayout */));
+
+ // Top-to-bottom split
+ assertEquals(
+ 1400, /* (2000 - 200 - 50) * 0.8 */
+ DividerPresenter.calculateMaxPosition(
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ false /* isVerticalSplit */,
+ false /* isReversedLayout */));
+
+ // Right-to-left split
+ assertEquals(
+ 595, /* (1000 - 100 - 50) * (1 - 0.3) */
+ DividerPresenter.calculateMaxPosition(
+ taskBounds,
+ dividerWidthPx,
+ dividerAttributes,
+ true /* isVerticalSplit */,
+ true /* isReversedLayout */));
+ }
+
+ @Test
+ public void testCalculateNewSplitRatio_leftToRight() {
+ // primary=500px; secondary=500px; divider=100px; total=1100px.
+ final Rect taskBounds = new Rect(0, 0, 1100, 2000);
+ final Rect primaryBounds = new Rect(0, 0, 500, 2000);
+ final Rect secondaryBounds = new Rect(600, 0, 1100, 2000);
+ final int dividerWidthPx = 100;
+
+ final TaskFragmentContainer mockPrimaryContainer =
+ createMockTaskFragmentContainer(mPrimaryContainerToken, primaryBounds);
+ final TaskFragmentContainer mockSecondaryContainer =
+ createMockTaskFragmentContainer(mSecondaryContainerToken, secondaryBounds);
+ when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer);
+ when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer);
+
+ // Test the normal case
+ int dividerPosition = 300;
+ assertEquals(
+ 0.3f, // Primary is 300px after dragging.
+ DividerPresenter.calculateNewSplitRatio(
+ dividerPosition,
+ taskBounds,
+ dividerWidthPx,
+ true /* isVerticalSplit */,
+ false /* isReversedLayout */,
+ 200 /* minPosition */,
+ 1000 /* maxPosition */,
+ false /* isDraggingToFullscreenAllowed */),
+ 0.0001 /* delta */);
+
+ // Test the case when dragging to fullscreen is allowed and divider is dragged to the edge
+ dividerPosition = 0;
+ assertEquals(
+ DividerPresenter.RATIO_EXPANDED_SECONDARY,
+ DividerPresenter.calculateNewSplitRatio(
+ dividerPosition,
+ taskBounds,
+ dividerWidthPx,
+ true /* isVerticalSplit */,
+ false /* isReversedLayout */,
+ 200 /* minPosition */,
+ 1000 /* maxPosition */,
+ true /* isDraggingToFullscreenAllowed */),
+ 0.0001 /* delta */);
+
+ // Test the case when dragging to fullscreen is not allowed and divider is dragged to the
+ // edge.
+ dividerPosition = 0;
+ assertEquals(
+ 0.2f, // Adjusted to the minPosition 200
+ DividerPresenter.calculateNewSplitRatio(
+ dividerPosition,
+ taskBounds,
+ dividerWidthPx,
+ true /* isVerticalSplit */,
+ false /* isReversedLayout */,
+ 200 /* minPosition */,
+ 1000 /* maxPosition */,
+ false /* isDraggingToFullscreenAllowed */),
+ 0.0001 /* delta */);
+ }
+
+ @Test
+ public void testCalculateNewSplitRatio_bottomToTop() {
+ // Primary is at bottom. Secondary is at top.
+ // primary=500px; secondary=500px; divider=100px; total=1100px.
+ final Rect taskBounds = new Rect(0, 0, 2000, 1100);
+ final Rect primaryBounds = new Rect(0, 0, 2000, 1100);
+ final Rect secondaryBounds = new Rect(0, 0, 2000, 500);
+ final int dividerWidthPx = 100;
+
+ final TaskFragmentContainer mockPrimaryContainer =
+ createMockTaskFragmentContainer(mPrimaryContainerToken, primaryBounds);
+ final TaskFragmentContainer mockSecondaryContainer =
+ createMockTaskFragmentContainer(mSecondaryContainerToken, secondaryBounds);
+ when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer);
+ when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer);
+
+ // Test the normal case
+ int dividerPosition = 300;
+ assertEquals(
+ // After dragging, secondary is [0, 0, 2000, 300]. Primary is [0, 400, 2000, 1100].
+ 0.7f,
+ DividerPresenter.calculateNewSplitRatio(
+ dividerPosition,
+ taskBounds,
+ dividerWidthPx,
+ false /* isVerticalSplit */,
+ true /* isReversedLayout */,
+ 200 /* minPosition */,
+ 1000 /* maxPosition */,
+ true /* isDraggingToFullscreenAllowed */),
+ 0.0001 /* delta */);
+
+ // Test the case when dragging to fullscreen is not allowed and divider is dragged to the
+ // edge.
+ dividerPosition = 0;
+ assertEquals(
+ // The primary (bottom) container is expanded
+ DividerPresenter.RATIO_EXPANDED_PRIMARY,
+ DividerPresenter.calculateNewSplitRatio(
+ dividerPosition,
+ taskBounds,
+ dividerWidthPx,
+ false /* isVerticalSplit */,
+ true /* isReversedLayout */,
+ 200 /* minPosition */,
+ 1000 /* maxPosition */,
+ true /* isDraggingToFullscreenAllowed */),
+ 0.0001 /* delta */);
+
+ // Test the case when dragging to fullscreen is not allowed and divider is dragged to the
+ // edge.
+ dividerPosition = 0;
+ assertEquals(
+ // Adjusted to minPosition 200, so the primary (bottom) container is 800.
+ 0.8f,
+ DividerPresenter.calculateNewSplitRatio(
+ dividerPosition,
+ taskBounds,
+ dividerWidthPx,
+ false /* isVerticalSplit */,
+ true /* isReversedLayout */,
+ 200 /* minPosition */,
+ 1000 /* maxPosition */,
+ false /* isDraggingToFullscreenAllowed */),
+ 0.0001 /* delta */);
+ }
+
+ @Test
+ public void testGetContainerBackgroundColor() {
+ final Color defaultColor = Color.valueOf(Color.RED);
+ final Color activityBackgroundColor = Color.valueOf(Color.BLUE);
+ final TaskFragmentContainer container = mock(TaskFragmentContainer.class);
+ final Activity activity = mock(Activity.class);
+ final Window window = mock(Window.class);
+ final View decorView = mock(View.class);
+ final ColorDrawable backgroundDrawable =
+ new ColorDrawable(activityBackgroundColor.toArgb());
+ when(activity.getWindow()).thenReturn(window);
+ when(window.getDecorView()).thenReturn(decorView);
+ when(decorView.getBackground()).thenReturn(backgroundDrawable);
+
+ // When the top non-finishing activity returns null, the default color should be returned.
+ when(container.getTopNonFinishingActivity()).thenReturn(null);
+ assertEquals(defaultColor,
+ DividerPresenter.getContainerBackgroundColor(container, defaultColor));
+
+ // When the top non-finishing activity is non-null, its background color should be returned.
+ when(container.getTopNonFinishingActivity()).thenReturn(activity);
+ assertEquals(activityBackgroundColor,
+ DividerPresenter.getContainerBackgroundColor(container, defaultColor));
+ }
+
+ @Test
+ public void testGetValueAnimator() {
+ ValueAnimator animator =
+ DividerPresenter.getValueAnimator(
+ 375 /* prevDividerPosition */,
+ 500 /* snappedDividerPosition */);
+
+ assertEquals(animator.getDuration(), FLING_ANIMATION_DURATION);
+ assertEquals(animator.getInterpolator(), FLING_ANIMATION_INTERPOLATOR);
+ }
+
+ @Test
+ public void testDividerPositionWithDraggingToFullscreenAllowed() {
+ final float displayDensity = 600F;
+ final float dismissVelocity = MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity + 10f;
+ 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 less than minPosition and the velocity is enough to be dismissed
+ assertEquals(
+ 0, // Closed position
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 10 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ -dismissVelocity,
+ displayDensity,
+ true /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is greater than maxPosition and the velocity is enough to be dismissed
+ assertEquals(
+ 1200, // Fully expanded position
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 1000 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ dismissVelocity,
+ 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.dividerPositionWithPositionOptions(
+ 500 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ nonFlingVelocity,
+ 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.dividerPositionWithPositionOptions(
+ 950 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ nonFlingVelocity,
+ 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.dividerPositionWithPositionOptions(
+ 20 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ nonFlingVelocity,
+ displayDensity,
+ false /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is snapped when the velocity is not fast enough for fling and at the
+ // closed position
+ assertEquals(
+ 30, // Closest position is minPosition
+ DividerPresenter.dividerPositionWithPositionOptions(
+ 0 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ nonFlingVelocity,
+ displayDensity,
+ false /* isDraggingToFullscreenAllowed */));
+
+ // Divider position is snapped when the velocity is not fast enough for fling and at the
+ // fully expanded position
+ assertEquals(
+ 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,
+ false /* isDraggingToFullscreenAllowed */));
+ }
+
+ private TaskFragmentContainer createMockTaskFragmentContainer(
+ @NonNull IBinder token, @NonNull Rect bounds) {
+ final TaskFragmentContainer container = mock(TaskFragmentContainer.class);
+ when(container.getTaskFragmentToken()).thenReturn(token);
+ when(container.getLastRequestedBounds()).thenReturn(bounds);
+ return container;
+ }
+
+ private void assertDividerOffsetEquals(
+ int dividerWidthPx,
+ @NonNull SplitAttributes.SplitType splitType,
+ int expectedTopLeftOffset,
+ int expectedBottomRightOffset) {
+ int offset = getBoundsOffsetForDivider(
+ dividerWidthPx,
+ splitType,
+ CONTAINER_POSITION_LEFT
+ );
+ assertEquals(-expectedTopLeftOffset, offset);
+
+ offset = getBoundsOffsetForDivider(
+ dividerWidthPx,
+ splitType,
+ CONTAINER_POSITION_RIGHT
+ );
+ assertEquals(expectedBottomRightOffset, offset);
+
+ offset = getBoundsOffsetForDivider(
+ dividerWidthPx,
+ splitType,
+ CONTAINER_POSITION_TOP
+ );
+ assertEquals(-expectedTopLeftOffset, offset);
+
+ offset = getBoundsOffsetForDivider(
+ dividerWidthPx,
+ splitType,
+ CONTAINER_POSITION_BOTTOM
+ );
+ assertEquals(expectedBottomRightOffset, offset);
+ }
+}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
index a069ac7256d6..d649c6d57137 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
@@ -248,4 +248,17 @@ public class EmbeddingTestUtils {
return new SplitPlaceholderRule.Builder(placeholderIntent, activityPredicate,
intentPredicate, windowMetricsPredicate);
}
+
+ @NonNull
+ static TaskFragmentContainer createTfContainer(
+ @NonNull SplitController splitController, @NonNull Activity activity) {
+ return createTfContainer(splitController, TASK_ID, activity);
+ }
+
+ @NonNull
+ static TaskFragmentContainer createTfContainer(
+ @NonNull SplitController splitController, int taskId, @NonNull Activity activity) {
+ return new TaskFragmentContainer.Builder(splitController, taskId, activity)
+ .setPendingAppearedActivity(activity).build();
+ }
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
index dd087e8eb7c9..7b473b04548c 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
@@ -25,6 +25,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -42,10 +43,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
@@ -61,6 +64,9 @@ import java.util.ArrayList;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class JetpackTaskFragmentOrganizerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
+
@Mock
private WindowContainerTransaction mTransaction;
@Mock
@@ -73,7 +79,6 @@ public class JetpackTaskFragmentOrganizerTest {
@Before
public void setup() {
- MockitoAnnotations.initMocks(this);
mOrganizer = new JetpackTaskFragmentOrganizer(Runnable::run, mCallback);
mOrganizer.registerOrganizer();
spyOn(mOrganizer);
@@ -101,13 +106,16 @@ public class JetpackTaskFragmentOrganizerTest {
@Test
public void testExpandTaskFragment() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
- new Intent(), taskContainer, mSplitController, null /* pairedPrimaryContainer */);
+ doReturn(taskContainer).when(mSplitController).getTaskContainer(anyInt());
+ final TaskFragmentContainer container = new TaskFragmentContainer.Builder(mSplitController,
+ taskContainer.getTaskId(), null /* activityInTask */)
+ .setPendingAppearedIntent(new Intent())
+ .build();
final TaskFragmentInfo info = createMockInfo(container);
mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info);
container.setInfo(mTransaction, info);
- mOrganizer.expandTaskFragment(mTransaction, container.getTaskFragmentToken());
+ mOrganizer.expandTaskFragment(mTransaction, container);
verify(mTransaction).setWindowingMode(container.getInfo().getToken(),
WINDOWING_MODE_UNDEFINED);
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 28fbadbebe7f..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
@@ -21,11 +21,16 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
import static androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_ATTRIBUTES;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TEST_TAG;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTfContainer;
+import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds;
import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -39,6 +44,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -80,9 +86,11 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -97,6 +105,11 @@ import java.util.List;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class OverlayPresentationTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
+
+ private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent(
+ new ComponentName("test", "placeholder"));
@Rule
public final SetFlagsRule mSetFlagRule = new SetFlagsRule();
@@ -125,7 +138,6 @@ public class OverlayPresentationTest {
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent)
.getCurrentWindowLayoutInfo(anyInt(), any());
DeviceStateManagerFoldingFeatureProducer producer =
@@ -177,37 +189,85 @@ public class OverlayPresentationTest {
}
@Test
- public void testGetOverlayContainers() {
- assertThat(mSplitController.getAllOverlayTaskFragmentContainers()).isEmpty();
+ public void testSetIsolatedNavigation_overlayFeatureDisabled_earlyReturn() {
+ mSetFlagRule.disableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG);
+
+ final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, "test");
+
+ mSplitPresenter.setTaskFragmentIsolatedNavigation(mTransaction, container,
+ !container.isIsolatedNavigationEnabled());
+
+ verify(mSplitPresenter, never()).setTaskFragmentIsolatedNavigation(any(),
+ any(IBinder.class), anyBoolean());
+ }
+
+ @Test
+ public void testSetPinned_overlayFeatureDisabled_earlyReturn() {
+ mSetFlagRule.disableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_OVERLAY_PRESENTATION_FLAG);
+
+ final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, "test");
+
+ mSplitPresenter.setTaskFragmentPinned(mTransaction, container,
+ !container.isPinned());
+
+ verify(mSplitPresenter, never()).setTaskFragmentPinned(any(), any(IBinder.class),
+ anyBoolean());
+ }
+
+ @Test
+ public void testGetAllNonFinishingOverlayContainers() {
+ assertThat(mSplitController.getAllNonFinishingOverlayContainers()).isEmpty();
final TaskFragmentContainer overlayContainer1 =
createTestOverlayContainer(TASK_ID, "test1");
- assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
+ assertThat(mSplitController.getAllNonFinishingOverlayContainers())
.containsExactly(overlayContainer1);
- assertThrows(
- "The exception must throw if there are two overlay containers in the same task.",
- IllegalStateException.class,
- () -> createTestOverlayContainer(TASK_ID, "test2"));
+ final TaskFragmentContainer overlayContainer2 =
+ createTestOverlayContainer(TASK_ID, "test2");
+
+ assertThat(mSplitController.getAllNonFinishingOverlayContainers())
+ .containsExactly(overlayContainer1, overlayContainer2);
final TaskFragmentContainer overlayContainer3 =
createTestOverlayContainer(TASK_ID + 1, "test3");
- assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
- .containsExactly(overlayContainer1, overlayContainer3);
+ assertThat(mSplitController.getAllNonFinishingOverlayContainers())
+ .containsExactly(overlayContainer1, overlayContainer2, overlayContainer3);
+
+ final TaskFragmentContainer finishingOverlayContainer =
+ createTestOverlayContainer(TASK_ID, "test4");
+ spyOn(finishingOverlayContainer);
+ doReturn(true).when(finishingOverlayContainer).isFinished();
+
+ assertThat(mSplitController.getAllNonFinishingOverlayContainers())
+ .containsExactly(overlayContainer1, overlayContainer2, overlayContainer3);
}
@Test
- public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_anotherTagInTask_dismissOverlay() {
+ public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_anotherTagInTask() {
+ createExistingOverlayContainers(false /* visible */);
+ createMockTaskFragmentContainer(mActivity);
+
+ final TaskFragmentContainer overlayContainer =
+ createOrUpdateOverlayTaskFragmentIfNeeded("test3");
+
+ assertWithMessage("overlayContainer1 is still there since it's not visible.")
+ .that(mSplitController.getAllNonFinishingOverlayContainers())
+ .containsExactly(mOverlayContainer1, mOverlayContainer2, overlayContainer);
+ }
+
+ @Test
+ public void testCreateOrUpdateOverlay_visibleOverlaySameTagInTask_dismissOverlay() {
createExistingOverlayContainers();
final TaskFragmentContainer overlayContainer =
createOrUpdateOverlayTaskFragmentIfNeeded("test3");
- assertWithMessage("overlayContainer1 must be dismissed since the new overlay container"
- + " is launched to the same task")
- .that(mSplitController.getAllOverlayTaskFragmentContainers())
+ assertWithMessage("overlayContainer1 must be dismissed since it's visible"
+ + " in the same task.")
+ .that(mSplitController.getAllNonFinishingOverlayContainers())
.containsExactly(mOverlayContainer2, overlayContainer);
}
@@ -222,23 +282,23 @@ public class OverlayPresentationTest {
assertWithMessage("overlayContainer1 must be dismissed since the new overlay container"
+ " is launched with the same tag as an existing overlay container in a different "
+ "task")
- .that(mSplitController.getAllOverlayTaskFragmentContainers())
+ .that(mSplitController.getAllNonFinishingOverlayContainers())
.containsExactly(mOverlayContainer2, overlayContainer);
}
@Test
- public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_sameTagAndTask_updateOverlay() {
+ public void testCreateOrUpdateOverlay_sameTagTaskAndActivity_updateOverlay() {
createExistingOverlayContainers();
final Rect bounds = new Rect(0, 0, 100, 100);
mSplitController.setActivityStackAttributesCalculator(params ->
new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build());
final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
- "test1");
+ mOverlayContainer1.getOverlayTag());
assertWithMessage("overlayContainer1 must be updated since the new overlay container"
+ " is launched with the same tag and task")
- .that(mSplitController.getAllOverlayTaskFragmentContainers())
+ .that(mSplitController.getAllNonFinishingOverlayContainers())
.containsExactly(mOverlayContainer1, mOverlayContainer2);
assertThat(overlayContainer).isEqualTo(mOverlayContainer1);
@@ -247,6 +307,38 @@ public class OverlayPresentationTest {
}
@Test
+ public void testCreateOrUpdateOverlay_sameTagAndTaskButNotActivity_dismissOverlay() {
+ createExistingOverlayContainers();
+
+ final Rect bounds = new Rect(0, 0, 100, 100);
+ mSplitController.setActivityStackAttributesCalculator(params ->
+ new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build());
+ final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
+ mOverlayContainer1.getOverlayTag(), createMockActivity());
+
+ assertWithMessage("overlayContainer1 must be dismissed since the new overlay container"
+ + " is associated with different launching activity")
+ .that(mSplitController.getAllNonFinishingOverlayContainers())
+ .containsExactly(mOverlayContainer2, overlayContainer);
+ assertThat(overlayContainer).isNotEqualTo(mOverlayContainer1);
+ }
+
+ @Test
+ public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_dismissOverlay() {
+ createExistingOverlayContainers(false /* visible */);
+ createMockTaskFragmentContainer(mActivity);
+
+ final TaskFragmentContainer overlayContainer =
+ createOrUpdateOverlayTaskFragmentIfNeeded("test2");
+
+ // OverlayContainer2 is dismissed since new container is launched with the
+ // same tag in different task.
+ assertWithMessage("overlayContainer1 must be dismissed")
+ .that(mSplitController.getAllNonFinishingOverlayContainers())
+ .containsExactly(mOverlayContainer1, overlayContainer);
+ }
+
+ @Test
public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_dismissMultipleOverlays() {
createExistingOverlayContainers();
@@ -257,15 +349,39 @@ public class OverlayPresentationTest {
// different tag. OverlayContainer2 is dismissed since new container is launched with the
// same tag in different task.
assertWithMessage("overlayContainer1 and overlayContainer2 must be dismissed")
- .that(mSplitController.getAllOverlayTaskFragmentContainers())
+ .that(mSplitController.getAllNonFinishingOverlayContainers())
.containsExactly(overlayContainer);
}
+ @Test
+ public void testCreateOrUpdateOverlay_launchFromSplit_returnNull() {
+ final Activity primaryActivity = createMockActivity();
+ final Activity secondaryActivity = createMockActivity();
+ final TaskFragmentContainer primaryContainer =
+ createMockTaskFragmentContainer(primaryActivity);
+ final TaskFragmentContainer secondaryContainer =
+ createMockTaskFragmentContainer(secondaryActivity);
+ final SplitPairRule splitPairRule = createSplitPairRuleBuilder(
+ activityActivityPair -> true /* activityPairPredicate */,
+ activityIntentPair -> true /* activityIntentPairPredicate */,
+ parentWindowMetrics -> true /* parentWindowMetricsPredicate */).build();
+ mSplitController.registerSplit(mTransaction, primaryContainer, primaryActivity,
+ secondaryContainer, splitPairRule, splitPairRule.getDefaultSplitAttributes());
+
+ assertThat(createOrUpdateOverlayTaskFragmentIfNeeded("test", primaryActivity)).isNull();
+ assertThat(createOrUpdateOverlayTaskFragmentIfNeeded("test", secondaryActivity)).isNull();
+ }
+
private void createExistingOverlayContainers() {
- mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1");
- mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2");
+ createExistingOverlayContainers(true /* visible */);
+ }
+
+ private void createExistingOverlayContainers(boolean visible) {
+ mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1", visible,
+ true /* associatedLaunchingActivity */, mActivity);
+ mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2", visible);
List<TaskFragmentContainer> overlayContainers = mSplitController
- .getAllOverlayTaskFragmentContainers();
+ .getAllNonFinishingOverlayContainers();
assertThat(overlayContainers).containsExactly(mOverlayContainer1, mOverlayContainer2);
}
@@ -274,17 +390,38 @@ public class OverlayPresentationTest {
mIntent.setComponent(new ComponentName(ApplicationProvider.getApplicationContext(),
MinimumDimensionActivity.class));
final Rect bounds = new Rect(0, 0, 100, 100);
+ final TaskFragmentContainer overlayContainer =
+ createTestOverlayContainer(TASK_ID, "test1");
- SplitPresenter.sanitizeBounds(bounds, SplitPresenter.getMinDimensions(mIntent),
- TASK_BOUNDS);
+ assertThat(sanitizeBounds(bounds, SplitPresenter.getMinDimensions(mIntent),
+ overlayContainer).isEmpty()).isTrue();
}
@Test
public void testSanitizeBounds_notInTaskBounds_expandOverlay() {
final Rect bounds = new Rect(TASK_BOUNDS);
bounds.offset(10, 10);
+ final TaskFragmentContainer overlayContainer =
+ createTestOverlayContainer(TASK_ID, "test1");
- SplitPresenter.sanitizeBounds(bounds, null, TASK_BOUNDS);
+ assertThat(sanitizeBounds(bounds, null, overlayContainer)
+ .isEmpty()).isTrue();
+ }
+
+ @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
@@ -294,9 +431,9 @@ public class OverlayPresentationTest {
new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build());
final TaskFragmentContainer overlayContainer =
createOrUpdateOverlayTaskFragmentIfNeeded("test");
- setupTaskFragmentInfo(overlayContainer, mActivity);
+ setupTaskFragmentInfo(overlayContainer, mActivity, true /* isVisible */);
- assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
+ assertThat(mSplitController.getAllNonFinishingOverlayContainers())
.containsExactly(overlayContainer);
assertThat(overlayContainer.getTaskId()).isEqualTo(TASK_ID);
assertThat(overlayContainer.areLastRequestedBoundsEqual(bounds)).isTrue();
@@ -304,14 +441,16 @@ public class OverlayPresentationTest {
}
@Test
- public void testGetTopNonFishingTaskFragmentContainerWithOverlay() {
- final TaskFragmentContainer overlayContainer =
- createTestOverlayContainer(TASK_ID, "test1");
-
- // Add a SplitPinContainer, the overlay should be on top
+ public void testGetTopNonFishingTaskFragmentContainerWithoutAssociatedOverlay() {
final Activity primaryActivity = createMockActivity();
final Activity secondaryActivity = createMockActivity();
-
+ final Rect bounds = new Rect(0, 0, 100, 100);
+ mSplitController.setActivityStackAttributesCalculator(params ->
+ new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build());
+ final TaskFragmentContainer overlayContainer =
+ createTestOverlayContainer(TASK_ID, "test1", true /* isVisible */,
+ false /* shouldAssociateWithActivity */);
+ overlayContainer.setIsolatedNavigationEnabled(true);
final TaskFragmentContainer primaryContainer =
createMockTaskFragmentContainer(primaryActivity);
final TaskFragmentContainer secondaryContainer =
@@ -353,10 +492,10 @@ public class OverlayPresentationTest {
@Test
public void testGetTopNonFinishingActivityWithOverlay() {
- TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test1");
-
final Activity activity = createMockActivity();
final TaskFragmentContainer container = createMockTaskFragmentContainer(activity);
+ final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID,
+ "test1");
final TaskContainer task = container.getTaskContainer();
assertThat(task.getTopNonFinishingActivity(true /* includeOverlay */))
@@ -373,8 +512,9 @@ public class OverlayPresentationTest {
}
@Test
- public void testUpdateOverlayContainer_dismissOverlayIfNeeded() {
- TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test");
+ public void testUpdateOverlayContainer_dismissNonAssociatedOverlayIfNeeded() {
+ TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test",
+ true /* isVisible */, false /* associatedLaunchingActivity */);
mSplitController.updateOverlayContainer(mTransaction, overlayContainer);
@@ -407,8 +547,8 @@ public class OverlayPresentationTest {
@Test
public void testUpdateActivityStackAttributes_nullContainer_earlyReturn() {
- final TaskFragmentContainer container = mSplitController.newContainer(mActivity,
- mActivity.getTaskId());
+ final TaskFragmentContainer container = createTfContainer(mSplitController,
+ mActivity.getTaskId(), mActivity);
mSplitController.updateActivityStackAttributes(
ActivityStack.Token.createFromBinder(container.getTaskFragmentToken()),
new ActivityStackAttributes.Builder().build());
@@ -443,11 +583,10 @@ public class OverlayPresentationTest {
@Test
public void testOnTaskFragmentParentInfoChanged_positionOnlyChange_earlyReturn() {
- final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test");
+ final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test",
+ true /* isVisible */, false /* associatedLaunchingActivity */);
final TaskContainer taskContainer = overlayContainer.getTaskContainer();
- assertThat(taskContainer.getOverlayContainer()).isEqualTo(overlayContainer);
-
spyOn(taskContainer);
final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties();
final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(
@@ -463,21 +602,20 @@ public class OverlayPresentationTest {
assertWithMessage("The overlay container must still be dismissed even if "
+ "#updateContainer is not called")
- .that(taskContainer.getOverlayContainer()).isNull();
+ .that(taskContainer.getTaskFragmentContainers()).isEmpty();
}
@Test
- public void testOnTaskFragmentParentInfoChanged_invisibleTask_callDismissOverlayContainer() {
- final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test");
+ public void testOnTaskFragmentParentInfoChanged_invisibleTask_callDismissNonAssocOverlay() {
+ final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test",
+ true /* isVisible */, false /* associatedLaunchingActivity */);
final TaskContainer taskContainer = overlayContainer.getTaskContainer();
- assertThat(taskContainer.getOverlayContainer()).isEqualTo(overlayContainer);
-
spyOn(taskContainer);
final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties();
final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(
new Configuration(taskProperties.getConfiguration()), taskProperties.getDisplayId(),
- false /* visible */, false /* hasDirectActivity */, null /* decorSurface */);
+ true /* visible */, false /* hasDirectActivity */, null /* decorSurface */);
mSplitController.onTaskFragmentParentInfoChanged(mTransaction, TASK_ID, parentInfo);
@@ -487,7 +625,7 @@ public class OverlayPresentationTest {
assertWithMessage("The overlay container must still be dismissed even if "
+ "#updateContainer is not called")
- .that(taskContainer.getOverlayContainer()).isNull();
+ .that(taskContainer.getTaskFragmentContainers()).isEmpty();
}
@Test
@@ -505,12 +643,15 @@ public class OverlayPresentationTest {
WINDOWING_MODE_UNDEFINED);
verify(mSplitPresenter).updateAnimationParams(mTransaction, token,
TaskFragmentAnimationParams.DEFAULT);
- verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, false);
verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, false);
+ verify(mSplitPresenter, never()).setTaskFragmentPinned(any(),
+ any(TaskFragmentContainer.class), anyBoolean());
+ verify(mSplitPresenter, never()).setTaskFragmentIsolatedNavigation(any(),
+ any(TaskFragmentContainer.class), anyBoolean());
}
@Test
- public void testApplyActivityStackAttributesForOverlayContainer() {
+ public void testApplyActivityStackAttributesForOverlayContainerAssociatedWithActivity() {
final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, TEST_TAG);
final IBinder token = container.getTaskFragmentToken();
final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder()
@@ -528,6 +669,33 @@ public class OverlayPresentationTest {
verify(mSplitPresenter).updateAnimationParams(mTransaction, token,
TaskFragmentAnimationParams.DEFAULT);
verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, true);
+ verify(mSplitPresenter, never()).setTaskFragmentPinned(any(),
+ any(TaskFragmentContainer.class), anyBoolean());
+ verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, true);
+ }
+
+ @Test
+ public void testApplyActivityStackAttributesForOverlayContainerWithoutAssociatedActivity() {
+ final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, TEST_TAG,
+ true, /* isVisible */ false /* associatedWithLaunchingActivity */);
+ final IBinder token = container.getTaskFragmentToken();
+ final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder()
+ .setRelativeBounds(new Rect(0, 0, 200, 200))
+ .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK))
+ .build();
+
+ mSplitPresenter.applyActivityStackAttributes(mTransaction, container,
+ attributes, null /* minDimensions */);
+
+ verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container,
+ attributes.getRelativeBounds());
+ verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction,
+ container, WINDOWING_MODE_MULTI_WINDOW);
+ verify(mSplitPresenter).updateAnimationParams(mTransaction, token,
+ TaskFragmentAnimationParams.DEFAULT);
+ verify(mSplitPresenter, never()).setTaskFragmentIsolatedNavigation(any(),
+ any(TaskFragmentContainer.class), anyBoolean());
+ verify(mSplitPresenter).setTaskFragmentPinned(mTransaction, container, true);
verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, true);
}
@@ -547,6 +715,8 @@ public class OverlayPresentationTest {
verify(mSplitPresenter).updateAnimationParams(mTransaction, token,
TaskFragmentAnimationParams.DEFAULT);
verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, false);
+ verify(mSplitPresenter, never()).setTaskFragmentPinned(any(),
+ any(TaskFragmentContainer.class), anyBoolean());
verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, false);
}
@@ -563,8 +733,7 @@ public class OverlayPresentationTest {
mSplitPresenter.applyActivityStackAttributes(mTransaction, container, attributes,
new Size(relativeBounds.width() + 1, relativeBounds.height()));
- verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container,
- new Rect());
+ verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container, new Rect());
verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container,
WINDOWING_MODE_UNDEFINED);
verify(mSplitPresenter).updateAnimationParams(mTransaction, token,
@@ -573,40 +742,215 @@ public class OverlayPresentationTest {
verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, false);
}
+ @Test
+ public void testFinishSelfWithActivityIfNeeded() {
+ TaskFragmentContainer container = createMockTaskFragmentContainer(mActivity);
+
+ container.finishSelfWithActivityIfNeeded(mTransaction, mActivity.getActivityToken());
+
+ verify(mSplitPresenter, never()).cleanupContainer(any(), any(), anyBoolean());
+
+ TaskFragmentContainer overlayWithoutAssociation = createTestOverlayContainer(TASK_ID,
+ "test", false /* associateLaunchingActivity */);
+
+ overlayWithoutAssociation.finishSelfWithActivityIfNeeded(mTransaction,
+ mActivity.getActivityToken());
+
+ verify(mSplitPresenter, never()).cleanupContainer(any(), any(), anyBoolean());
+ assertThat(mSplitController.getAllNonFinishingOverlayContainers())
+ .contains(overlayWithoutAssociation);
+
+ TaskFragmentContainer overlayWithAssociation =
+ createOrUpdateOverlayTaskFragmentIfNeeded("test");
+ overlayWithAssociation.setInfo(mTransaction, createMockTaskFragmentInfo(
+ overlayWithAssociation, mActivity, true /* isVisible */));
+ assertThat(mSplitController.getAllNonFinishingOverlayContainers())
+ .contains(overlayWithAssociation);
+ clearInvocations(mSplitPresenter);
+
+ overlayWithAssociation.finishSelfWithActivityIfNeeded(mTransaction, new Binder());
+
+ verify(mSplitPresenter, never()).cleanupContainer(any(), any(), anyBoolean());
+
+ overlayWithAssociation.finishSelfWithActivityIfNeeded(mTransaction,
+ mActivity.getActivityToken());
+
+ verify(mSplitPresenter).cleanupContainer(mTransaction, overlayWithAssociation, false);
+
+ assertThat(mSplitController.getAllNonFinishingOverlayContainers())
+ .doesNotContain(overlayWithAssociation);
+ }
+
+ @Test
+ public void testLaunchPlaceholderIfNecessary_skipIfActivityAssociateOverlay() {
+ setupPlaceholderRule(mActivity);
+ createTestOverlayContainer(TASK_ID, "test", true /* isVisible */,
+ true /* associateLaunchingActivity */, mActivity);
+
+
+ mSplitController.mTransactionManager.startNewTransaction();
+ assertThat(mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity,
+ false /* isOnCreated */)).isFalse();
+
+ verify(mTransaction, never()).startActivityInTaskFragment(any(), any(), any(), any());
+ }
+
+ @Test
+ public void testLaunchPlaceholderIfNecessary_skipIfActivityInOverlay() {
+ setupPlaceholderRule(mActivity);
+ createOrUpdateOverlayTaskFragmentIfNeeded("test1", mActivity);
+
+ mSplitController.mTransactionManager.startNewTransaction();
+ assertThat(mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity,
+ false /* isOnCreated */)).isFalse();
+
+ verify(mTransaction, never()).startActivityInTaskFragment(any(), any(), any(), any());
+ }
+
+ /** Setups a rule to launch placeholder for the given activity. */
+ private void setupPlaceholderRule(@NonNull Activity primaryActivity) {
+ final SplitRule placeholderRule = createSplitPlaceholderRuleBuilder(PLACEHOLDER_INTENT,
+ primaryActivity::equals, i -> false, w -> true)
+ .setDefaultSplitAttributes(SPLIT_ATTRIBUTES)
+ .build();
+ mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule));
+ }
+
+ @Test
+ public void testResolveStartActivityIntent_skipIfAssociateOverlay() {
+ final Intent intent = new Intent();
+ mSplitController.setEmbeddingRules(Collections.singleton(
+ createSplitRule(mActivity, intent)));
+ createTestOverlayContainer(TASK_ID, "test", true /* isVisible */,
+ true /* associateLaunchingActivity */, mActivity);
+ final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(
+ mTransaction, TASK_ID, intent, mActivity);
+
+ assertThat(container).isNull();
+ verify(mSplitController, never()).resolveStartActivityIntentByRule(any(), anyInt(), any(),
+ any());
+ }
+
+ @Test
+ public void testResolveStartActivityIntent_skipIfLaunchingActivityInOverlay() {
+ final Intent intent = new Intent();
+ mSplitController.setEmbeddingRules(Collections.singleton(
+ createSplitRule(mActivity, intent)));
+ createOrUpdateOverlayTaskFragmentIfNeeded("test1", mActivity);
+ final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(
+ mTransaction, TASK_ID, intent, mActivity);
+
+ assertThat(container).isNull();
+ verify(mSplitController, never()).resolveStartActivityIntentByRule(any(), anyInt(), any(),
+ any());
+ }
+
+ @Test
+ public void testOnActivityReparentedToTask_overlayRestoration() {
+ mSetFlagRule.enableFlags(Flags.FLAG_FIX_PIP_RESTORE_TO_OVERLAY);
+
+ // Prepares and mock the data necessary for the test.
+ final IBinder activityToken = mActivity.getActivityToken();
+ final Intent intent = new Intent();
+ final IBinder fillTaskActivityToken = new Binder();
+ final IBinder lastOverlayToken = new Binder();
+ final TaskFragmentContainer overlayContainer =
+ new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity)
+ .setPendingAppearedIntent(intent).build();
+ final TaskFragmentContainer.OverlayContainerRestoreParams params = mock(
+ TaskFragmentContainer.OverlayContainerRestoreParams.class);
+ doReturn(params).when(mSplitController).getOverlayContainerRestoreParams(any(), any());
+ doReturn(overlayContainer).when(mSplitController).createOrUpdateOverlayTaskFragmentIfNeeded(
+ any(), any(), any(), any());
+
+ // Verify the activity should be reparented to the overlay container.
+ mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, intent, activityToken,
+ fillTaskActivityToken, lastOverlayToken);
+ verify(mTransaction).reparentActivityToTaskFragment(
+ eq(overlayContainer.getTaskFragmentToken()), eq(activityToken));
+ }
+
/**
- * A simplified version of {@link SplitController.ActivityStartMonitor
- * #createOrUpdateOverlayTaskFragmentIfNeeded}
+ * A simplified version of {@link SplitController#createOrUpdateOverlayTaskFragmentIfNeeded}
*/
@Nullable
private TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded(@NonNull String tag) {
final Bundle launchOptions = new Bundle();
launchOptions.putString(KEY_OVERLAY_TAG, tag);
+ return createOrUpdateOverlayTaskFragmentIfNeeded(tag, mActivity);
+ }
+
+ /**
+ * A simplified version of {@link SplitController#createOrUpdateOverlayTaskFragmentIfNeeded}
+ */
+ @Nullable
+ private TaskFragmentContainer createOrUpdateOverlayTaskFragmentIfNeeded(
+ @NonNull String tag, @NonNull Activity activity) {
+ final Bundle launchOptions = new Bundle();
+ launchOptions.putString(KEY_OVERLAY_TAG, tag);
return mSplitController.createOrUpdateOverlayTaskFragmentIfNeeded(mTransaction,
- launchOptions, mIntent, mActivity);
+ launchOptions, mIntent, activity);
}
/** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
@NonNull
private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) {
- final TaskFragmentContainer container = mSplitController.newContainer(activity,
- activity.getTaskId());
- setupTaskFragmentInfo(container, activity);
+ return createMockTaskFragmentContainer(activity, false /* isVisible */);
+ }
+
+ /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
+ @NonNull
+ private TaskFragmentContainer createMockTaskFragmentContainer(
+ @NonNull Activity activity, boolean isVisible) {
+ final TaskFragmentContainer container = createTfContainer(mSplitController,
+ activity.getTaskId(), activity);
+ setupTaskFragmentInfo(container, activity, isVisible);
return container;
}
@NonNull
private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag) {
- Activity activity = createMockActivity();
- TaskFragmentContainer overlayContainer = mSplitController.newContainer(
- null /* pendingAppearedActivity */, mIntent, activity, taskId,
- null /* pairedPrimaryContainer */, tag, Bundle.EMPTY);
- setupTaskFragmentInfo(overlayContainer, activity);
+ return createTestOverlayContainer(taskId, tag, false /* isVisible */,
+ true /* associateLaunchingActivity */);
+ }
+
+ @NonNull
+ private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag,
+ boolean isVisible) {
+ return createTestOverlayContainer(taskId, tag, isVisible,
+ true /* associateLaunchingActivity */);
+ }
+
+ @NonNull
+ private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag,
+ boolean isVisible, boolean associateLaunchingActivity) {
+ return createTestOverlayContainer(taskId, tag, isVisible, associateLaunchingActivity,
+ null /* launchingActivity */);
+ }
+
+ // TODO(b/243518738): add more test coverage on overlay container without activity association
+ // once we have use cases.
+ @NonNull
+ private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag,
+ boolean isVisible, boolean associateLaunchingActivity,
+ @Nullable Activity launchingActivity) {
+ final Activity activity = launchingActivity != null
+ ? launchingActivity : createMockActivity();
+ TaskFragmentContainer overlayContainer =
+ new TaskFragmentContainer.Builder(mSplitController, taskId, activity)
+ .setPendingAppearedIntent(mIntent)
+ .setOverlayTag(tag)
+ .setLaunchOptions(Bundle.EMPTY)
+ .setAssociatedActivity(associateLaunchingActivity ? activity : null)
+ .build();
+ setupTaskFragmentInfo(overlayContainer, createMockActivity(), isVisible);
return overlayContainer;
}
private void setupTaskFragmentInfo(@NonNull TaskFragmentContainer container,
- @NonNull Activity activity) {
- final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity);
+ @NonNull Activity activity,
+ boolean isVisible) {
+ final TaskFragmentInfo info = createMockTaskFragmentInfo(container, activity, isVisible);
container.setInfo(mTransaction, info);
mSplitPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), info);
}
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 00f8b5925d66..640b1fced455 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
@@ -41,6 +41,7 @@ import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSpli
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTestTaskContainer;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTfContainer;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
import static androidx.window.extensions.embedding.SplitRule.FINISH_ALWAYS;
@@ -59,7 +60,6 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.clearInvocations;
@@ -72,6 +72,8 @@ import static org.mockito.Mockito.times;
import android.annotation.NonNull;
import android.app.Activity;
import android.app.ActivityOptions;
+import android.app.ActivityThread;
+import android.app.servertransaction.ClientTransactionListenerController;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -83,9 +85,11 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArraySet;
import android.view.WindowInsets;
import android.view.WindowMetrics;
+import android.window.ActivityWindowInfo;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentOrganizer;
import android.window.TaskFragmentParentInfo;
@@ -99,17 +103,23 @@ import androidx.window.common.DeviceStateManagerFoldingFeatureProducer;
import androidx.window.extensions.layout.WindowLayoutComponentImpl;
import androidx.window.extensions.layout.WindowLayoutInfo;
+import com.android.window.flags.Flags;
+
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.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
@@ -127,6 +137,12 @@ public class SplitControllerTest {
private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent(
new ComponentName("test", "placeholder"));
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
+
+ @Rule
+ public final SetFlagsRule mSetFlagRule = new SetFlagsRule();
+
private Activity mActivity;
@Mock
private Resources mActivityResources;
@@ -138,6 +154,13 @@ public class SplitControllerTest {
private Handler mHandler;
@Mock
private WindowLayoutComponentImpl mWindowLayoutComponent;
+ @Mock
+ private ActivityWindowInfo mActivityWindowInfo;
+ @Mock
+ private BiConsumer<IBinder, ActivityWindowInfo> mActivityWindowInfoListener;
+ @Mock
+ private androidx.window.extensions.core.util.function.Consumer<EmbeddedActivityWindowInfo>
+ mEmbeddedActivityWindowInfoCallback;
private SplitController mSplitController;
private SplitPresenter mSplitPresenter;
@@ -147,7 +170,6 @@ public class SplitControllerTest {
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent)
.getCurrentWindowLayoutInfo(anyInt(), any());
DeviceStateManagerFoldingFeatureProducer producer =
@@ -176,7 +198,7 @@ public class SplitControllerTest {
@Test
public void testOnTaskFragmentVanished() {
- final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity);
doReturn(tf.getTaskFragmentToken()).when(mInfo).getFragmentToken();
// The TaskFragment has been removed in the server, we only need to cleanup the reference.
@@ -191,7 +213,7 @@ public class SplitControllerTest {
public void testOnTaskFragmentAppearEmptyTimeout() {
// Setup to make sure a transaction record is started.
mTransactionManager.startNewTransaction();
- final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity);
doCallRealMethod().when(mSplitController).onTaskFragmentAppearEmptyTimeout(any(), any());
mSplitController.onTaskFragmentAppearEmptyTimeout(mTransaction, tf);
@@ -202,19 +224,19 @@ public class SplitControllerTest {
@Test
public void testOnActivityDestroyed() {
doReturn(new Binder()).when(mActivity).getActivityToken();
- final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity);
assertTrue(tf.hasActivity(mActivity.getActivityToken()));
// When the activity is not finishing, do not clear the record.
doReturn(false).when(mActivity).isFinishing();
- mSplitController.onActivityDestroyed(mActivity);
+ mSplitController.onActivityDestroyed(mTransaction, mActivity);
assertTrue(tf.hasActivity(mActivity.getActivityToken()));
// Clear the record when the activity is finishing and destroyed.
doReturn(true).when(mActivity).isFinishing();
- mSplitController.onActivityDestroyed(mActivity);
+ mSplitController.onActivityDestroyed(mTransaction, mActivity);
assertFalse(tf.hasActivity(mActivity.getActivityToken()));
}
@@ -223,12 +245,9 @@ public class SplitControllerTest {
public void testNewContainer() {
// Must pass in a valid activity.
assertThrows(IllegalArgumentException.class, () ->
- mSplitController.newContainer(null /* activity */, TASK_ID));
- assertThrows(IllegalArgumentException.class, () ->
- mSplitController.newContainer(mActivity, null /* launchingActivity */, TASK_ID));
+ createTfContainer(mSplitController, null /* activity */));
- final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, mActivity,
- TASK_ID);
+ final TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity);
final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
assertNotNull(tf);
@@ -241,7 +260,7 @@ public class SplitControllerTest {
public void testUpdateContainer() {
// Make SplitController#launchPlaceholderIfNecessary(TaskFragmentContainer) return true
// and verify if shouldContainerBeExpanded() not called.
- final TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity);
spyOn(tf);
doReturn(mActivity).when(tf).getTopNonFinishingActivity();
doReturn(true).when(tf).isEmpty();
@@ -275,7 +294,10 @@ public class SplitControllerTest {
doReturn(tf).when(splitContainer).getPrimaryContainer();
doReturn(tf).when(splitContainer).getSecondaryContainer();
doReturn(createTestTaskContainer()).when(splitContainer).getTaskContainer();
- doReturn(createSplitRule(mActivity, mActivity)).when(splitContainer).getSplitRule();
+ final SplitRule splitRule = createSplitRule(mActivity, mActivity);
+ doReturn(splitRule).when(splitContainer).getSplitRule();
+ doReturn(splitRule.getDefaultSplitAttributes())
+ .when(splitContainer).getDefaultSplitAttributes();
taskContainer = mSplitController.getTaskContainer(TASK_ID);
taskContainer.addSplitContainer(splitContainer);
// Add a mock SplitContainer on top of splitContainer
@@ -344,8 +366,12 @@ public class SplitControllerTest {
public void testOnStartActivityResultError() {
final Intent intent = new Intent();
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
- intent, taskContainer, mSplitController, null /* pairedPrimaryContainer */);
+ final int taskId = taskContainer.getTaskId();
+ mSplitController.addTaskContainer(taskId, taskContainer);
+ final TaskFragmentContainer container = new TaskFragmentContainer.Builder(mSplitController,
+ taskId, null /* activityInTask */)
+ .setPendingAppearedIntent(intent)
+ .build();
final SplitController.ActivityStartMonitor monitor =
mSplitController.getActivityStartMonitor();
@@ -372,7 +398,8 @@ public class SplitControllerTest {
@Test
public void testOnActivityReparentedToTask_sameProcess() {
mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, new Intent(),
- mActivity.getActivityToken());
+ mActivity.getActivityToken(), null /* fillTaskActivityToken */,
+ null /* lastOverlayToken */);
// Treated as on activity created, but allow to split as primary.
verify(mSplitController).resolveActivityToContainer(mTransaction,
@@ -384,11 +411,13 @@ public class SplitControllerTest {
@Test
public void testOnActivityReparentedToTask_diffProcess() {
// Create an empty TaskFragment to initialize for the Task.
- mSplitController.newContainer(new Intent(), mActivity, TASK_ID);
+ new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity)
+ .setPendingAppearedIntent(new Intent()).build();
final IBinder activityToken = new Binder();
final Intent intent = new Intent();
- mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, intent, activityToken);
+ mSplitController.onActivityReparentedToTask(mTransaction, TASK_ID, intent, activityToken,
+ null /* fillTaskActivityToken */, null /* lastOverlayToken */);
// Treated as starting new intent
verify(mSplitController, never()).resolveActivityToContainer(any(), any(), anyBoolean());
@@ -568,8 +597,9 @@ public class SplitControllerTest {
verify(mTransaction, never()).reparentActivityToTaskFragment(any(), any());
// Place in the top container if there is no other rule matched.
- final TaskFragmentContainer topContainer = mSplitController
- .newContainer(new Intent(), mActivity, TASK_ID);
+ final TaskFragmentContainer topContainer =
+ new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity)
+ .setPendingAppearedIntent(new Intent()).build();
mSplitController.placeActivityInTopContainer(mTransaction, mActivity);
verify(mTransaction).reparentActivityToTaskFragment(topContainer.getTaskFragmentToken(),
@@ -577,7 +607,7 @@ public class SplitControllerTest {
// Not reparent if activity is in a TaskFragment.
clearInvocations(mTransaction);
- mSplitController.newContainer(mActivity, TASK_ID);
+ createTfContainer(mSplitController, mActivity);
mSplitController.placeActivityInTopContainer(mTransaction, mActivity);
verify(mTransaction, never()).reparentActivityToTaskFragment(any(), any());
@@ -589,8 +619,7 @@ public class SplitControllerTest {
false /* isOnReparent */);
assertFalse(result);
- verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(),
- anyString(), any());
+ verify(mSplitController, never()).addTaskContainer(anyInt(), any());
}
@Test
@@ -605,7 +634,6 @@ public class SplitControllerTest {
assertTrue(result);
assertNotNull(container);
- verify(mSplitController).newContainer(mActivity, TASK_ID);
verify(mSplitPresenter).expandActivity(mTransaction, container.getTaskFragmentToken(),
mActivity);
}
@@ -615,12 +643,12 @@ public class SplitControllerTest {
setupExpandRule(mActivity);
// When the activity is not in any TaskFragment, create a new expanded TaskFragment for it.
- final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container = createTfContainer(mSplitController, mActivity);
final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
assertTrue(result);
- verify(mSplitPresenter).expandTaskFragment(mTransaction, container.getTaskFragmentToken());
+ verify(mSplitPresenter).expandTaskFragment(mTransaction, container);
}
@Test
@@ -665,8 +693,8 @@ public class SplitControllerTest {
// Don't launch placeholder if the activity is not in the topmost active TaskFragment.
final Activity activity = createMockActivity();
- mSplitController.newContainer(mActivity, TASK_ID);
- mSplitController.newContainer(activity, TASK_ID);
+ createTfContainer(mSplitController, mActivity);
+ createTfContainer(mSplitController, activity);
final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
@@ -684,7 +712,7 @@ public class SplitControllerTest {
(SplitPlaceholderRule) mSplitController.getSplitRules().get(0);
// Launch placeholder if the activity is in the topmost expanded TaskFragment.
- mSplitController.newContainer(mActivity, TASK_ID);
+ createTfContainer(mSplitController, mActivity);
final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
@@ -736,10 +764,11 @@ public class SplitControllerTest {
final SplitPairRule splitRule = (SplitPairRule) mSplitController.getSplitRules().get(0);
// Activity is already in primary split, no need to create new split.
- final TaskFragmentContainer primaryContainer = mSplitController.newContainer(mActivity,
- TASK_ID);
- final TaskFragmentContainer secondaryContainer = mSplitController.newContainer(
- secondaryIntent, mActivity, TASK_ID);
+ final TaskFragmentContainer primaryContainer =
+ createTfContainer(mSplitController, mActivity);
+ final TaskFragmentContainer secondaryContainer =
+ new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity)
+ .setPendingAppearedIntent(secondaryIntent).build();
mSplitController.registerSplit(
mTransaction,
primaryContainer,
@@ -752,8 +781,6 @@ public class SplitControllerTest {
false /* isOnReparent */);
assertTrue(result);
- verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(),
- anyString(), any());
verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any());
}
@@ -765,10 +792,11 @@ public class SplitControllerTest {
// The new launched activity is in primary split, but there is no rule for it to split with
// the secondary, so return false.
- final TaskFragmentContainer primaryContainer = mSplitController.newContainer(mActivity,
- TASK_ID);
- final TaskFragmentContainer secondaryContainer = mSplitController.newContainer(
- secondaryIntent, mActivity, TASK_ID);
+ final TaskFragmentContainer primaryContainer =
+ createTfContainer(mSplitController, mActivity);
+ final TaskFragmentContainer secondaryContainer =
+ new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity)
+ .setPendingAppearedIntent(secondaryIntent).build();
mSplitController.registerSplit(
mTransaction,
primaryContainer,
@@ -795,8 +823,6 @@ public class SplitControllerTest {
false /* isOnReparent */);
assertTrue(result);
- verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(),
- anyString(), any());
verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any());
}
@@ -825,10 +851,10 @@ public class SplitControllerTest {
doReturn(PLACEHOLDER_INTENT).when(mActivity).getIntent();
// Activity is a placeholder.
- final TaskFragmentContainer primaryContainer = mSplitController.newContainer(
- primaryActivity, TASK_ID);
- final TaskFragmentContainer secondaryContainer = mSplitController.newContainer(mActivity,
- TASK_ID);
+ final TaskFragmentContainer primaryContainer =
+ createTfContainer(mSplitController, primaryActivity);
+ final TaskFragmentContainer secondaryContainer =
+ createTfContainer(mSplitController, mActivity);
mSplitController.registerSplit(
mTransaction,
primaryContainer,
@@ -847,8 +873,7 @@ public class SplitControllerTest {
final Activity activityBelow = createMockActivity();
setupSplitRule(activityBelow, mActivity);
- final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
- TASK_ID);
+ final TaskFragmentContainer container = createTfContainer(mSplitController, activityBelow);
container.addPendingAppearedActivity(mActivity);
final boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
@@ -863,8 +888,7 @@ public class SplitControllerTest {
setupSplitRule(mActivity, activityBelow);
// Disallow to split as primary.
- final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
- TASK_ID);
+ final TaskFragmentContainer container = createTfContainer(mSplitController, activityBelow);
container.addPendingAppearedActivity(mActivity);
boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
false /* isOnReparent */);
@@ -934,8 +958,7 @@ public class SplitControllerTest {
doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo();
- final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
- TASK_ID);
+ final TaskFragmentContainer container = createTfContainer(mSplitController, activityBelow);
container.addPendingAppearedActivity(mActivity);
// Allow to split as primary.
@@ -953,8 +976,7 @@ public class SplitControllerTest {
doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo();
- final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
- TASK_ID);
+ final TaskFragmentContainer container = createTfContainer(mSplitController, activityBelow);
container.addPendingAppearedActivity(mActivity);
boolean result = mSplitController.resolveActivityToContainer(mTransaction, mActivity,
@@ -1017,8 +1039,8 @@ public class SplitControllerTest {
public void testResolveActivityToContainer_skipIfNonTopOrPinned() {
final TaskFragmentContainer container = createMockTaskFragmentContainer(mActivity);
final Activity pinnedActivity = createMockActivity();
- final TaskFragmentContainer topContainer = mSplitController.newContainer(pinnedActivity,
- TASK_ID);
+ final TaskFragmentContainer topContainer =
+ createTfContainer(mSplitController, pinnedActivity);
final TaskContainer taskContainer = container.getTaskContainer();
spyOn(taskContainer);
doReturn(container).when(taskContainer).getTopNonFinishingTaskFragmentContainer(false);
@@ -1185,7 +1207,7 @@ public class SplitControllerTest {
mSplitController.onTransactionReady(transaction);
verify(mSplitController).onActivityReparentedToTask(any(), eq(TASK_ID), eq(intent),
- eq(activityToken));
+ eq(activityToken), any(), any());
verify(mSplitPresenter).onTransactionHandled(eq(transaction.getTransactionToken()), any(),
anyInt(), anyBoolean());
}
@@ -1324,7 +1346,7 @@ public class SplitControllerTest {
// Launch placeholder for activity in top TaskFragment.
setupPlaceholderRule(mActivity);
mTransactionManager.startNewTransaction();
- final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container = createTfContainer(mSplitController, mActivity);
mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity,
true /* isOnCreated */);
@@ -1338,9 +1360,10 @@ public class SplitControllerTest {
// Do not launch placeholder for invisible activity below the top TaskFragment.
setupPlaceholderRule(mActivity);
mTransactionManager.startNewTransaction();
- final TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID);
- final TaskFragmentContainer topTf = mSplitController.newContainer(new Intent(), mActivity,
- TASK_ID);
+ final TaskFragmentContainer bottomTf = createTfContainer(mSplitController, mActivity);
+ final TaskFragmentContainer topTf =
+ new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity)
+ .setPendingAppearedIntent(new Intent()).build();
bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity,
false /* isVisible */));
topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity()));
@@ -1356,9 +1379,10 @@ public class SplitControllerTest {
// Launch placeholder for visible activity below the top TaskFragment.
setupPlaceholderRule(mActivity);
mTransactionManager.startNewTransaction();
- final TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID);
- final TaskFragmentContainer topTf = mSplitController.newContainer(new Intent(), mActivity,
- TASK_ID);
+ final TaskFragmentContainer bottomTf = createTfContainer(mSplitController, mActivity);
+ final TaskFragmentContainer topTf =
+ new TaskFragmentContainer.Builder(mSplitController, TASK_ID, mActivity)
+ .setPendingAppearedIntent(new Intent()).build();
bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity,
true /* isVisible */));
topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity()));
@@ -1385,7 +1409,7 @@ public class SplitControllerTest {
@Test
public void testFinishActivityStacks_finishSingleActivityStack() {
- TaskFragmentContainer tf = mSplitController.newContainer(mActivity, TASK_ID);
+ TaskFragmentContainer tf = createTfContainer(mSplitController, mActivity);
tf.setInfo(mTransaction, createMockTaskFragmentInfo(tf, mActivity));
final TaskContainer taskContainer = mSplitController.mTaskContainers.get(TASK_ID);
@@ -1399,8 +1423,8 @@ public class SplitControllerTest {
@Test
public void testFinishActivityStacks_finishActivityStacksInOrder() {
- TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID);
- TaskFragmentContainer topTf = mSplitController.newContainer(mActivity, TASK_ID);
+ TaskFragmentContainer bottomTf = createTfContainer(mSplitController, mActivity);
+ TaskFragmentContainer topTf = createTfContainer(mSplitController, mActivity);
bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity));
topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity()));
@@ -1529,6 +1553,113 @@ public class SplitControllerTest {
.getTopNonFinishingActivity(), secondaryActivity);
}
+ @Test
+ public void testIsActivityEmbedded() {
+ mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
+
+ assertFalse(mSplitController.isActivityEmbedded(mActivity));
+
+ doReturn(true).when(mActivityWindowInfo).isEmbedded();
+
+ assertTrue(mSplitController.isActivityEmbedded(mActivity));
+ }
+
+ @Test
+ public void testGetEmbeddedActivityWindowInfo() {
+ mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
+
+ final boolean isEmbedded = true;
+ final Rect taskBounds = new Rect(0, 0, 1000, 2000);
+ final Rect activityStackBounds = new Rect(0, 0, 500, 2000);
+ doReturn(isEmbedded).when(mActivityWindowInfo).isEmbedded();
+ doReturn(taskBounds).when(mActivityWindowInfo).getTaskBounds();
+ doReturn(activityStackBounds).when(mActivityWindowInfo).getTaskFragmentBounds();
+
+ final EmbeddedActivityWindowInfo expected = new EmbeddedActivityWindowInfo(mActivity,
+ isEmbedded, taskBounds, activityStackBounds);
+ assertEquals(expected, mSplitController.getEmbeddedActivityWindowInfo(mActivity));
+ }
+
+ @Test
+ public void testSetEmbeddedActivityWindowInfoCallback() {
+ mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG);
+
+ final ClientTransactionListenerController controller = ClientTransactionListenerController
+ .getInstance();
+ spyOn(controller);
+ doNothing().when(controller).registerActivityWindowInfoChangedListener(any());
+ doReturn(mActivityWindowInfoListener).when(mSplitController)
+ .getActivityWindowInfoListener();
+ final Executor executor = Runnable::run;
+
+ // Register to ClientTransactionListenerController
+ mSplitController.setEmbeddedActivityWindowInfoCallback(executor,
+ mEmbeddedActivityWindowInfoCallback);
+
+ verify(controller).registerActivityWindowInfoChangedListener(mActivityWindowInfoListener);
+ verify(mEmbeddedActivityWindowInfoCallback, never()).accept(any());
+
+ // Test onActivityWindowInfoChanged triggered.
+ mSplitController.onActivityWindowInfoChanged(mActivity.getActivityToken(),
+ mActivityWindowInfo);
+
+ verify(mEmbeddedActivityWindowInfoCallback).accept(any());
+
+ // Unregister to ClientTransactionListenerController
+ mSplitController.clearEmbeddedActivityWindowInfoCallback();
+
+ verify(controller).unregisterActivityWindowInfoChangedListener(mActivityWindowInfoListener);
+
+ // Test onActivityWindowInfoChanged triggered as no-op after clear callback.
+ clearInvocations(mEmbeddedActivityWindowInfoCallback);
+ mSplitController.onActivityWindowInfoChanged(mActivity.getActivityToken(),
+ mActivityWindowInfo);
+
+ verify(mEmbeddedActivityWindowInfoCallback, never()).accept(any());
+ }
+
+ @Test
+ public void testTaskFragmentParentInfoChanged() {
+ // Making a split
+ final Activity secondaryActivity = createMockActivity();
+ addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */);
+
+ // Updates the parent info.
+ final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID);
+ final Configuration configuration = new Configuration();
+ final TaskFragmentParentInfo originalInfo = new TaskFragmentParentInfo(configuration,
+ DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */,
+ null /* decorSurface */);
+ mSplitController.onTaskFragmentParentInfoChanged(mock(WindowContainerTransaction.class),
+ TASK_ID, originalInfo);
+ assertTrue(taskContainer.isVisible());
+
+ // Making a public configuration change while the Task is invisible.
+ configuration.densityDpi += 100;
+ final TaskFragmentParentInfo invisibleInfo = new TaskFragmentParentInfo(configuration,
+ DEFAULT_DISPLAY, false /* visible */, false /* hasDirectActivity */,
+ null /* decorSurface */);
+ mSplitController.onTaskFragmentParentInfoChanged(mock(WindowContainerTransaction.class),
+ TASK_ID, invisibleInfo);
+
+ // Ensure the TaskContainer is inivisible, but the configuration is not updated.
+ assertFalse(taskContainer.isVisible());
+ assertTrue(taskContainer.getTaskFragmentParentInfo().getConfiguration().diffPublicOnly(
+ configuration) > 0);
+
+ // Updates when Task to become visible
+ final TaskFragmentParentInfo visibleInfo = new TaskFragmentParentInfo(configuration,
+ DEFAULT_DISPLAY, true /* visible */, false /* hasDirectActivity */,
+ null /* decorSurface */);
+ mSplitController.onTaskFragmentParentInfoChanged(mock(WindowContainerTransaction.class),
+ TASK_ID, visibleInfo);
+
+ // Ensure the Task is visible and configuration is updated.
+ assertTrue(taskContainer.isVisible());
+ assertFalse(taskContainer.getTaskFragmentParentInfo().getConfiguration().diffPublicOnly(
+ configuration) > 0);
+ }
+
/** Creates a mock activity in the organizer process. */
private Activity createMockActivity() {
return createMockActivity(TASK_ID);
@@ -1537,20 +1668,24 @@ public class SplitControllerTest {
/** Creates a mock activity in the organizer process. */
private Activity createMockActivity(int taskId) {
final Activity activity = mock(Activity.class);
+ final ActivityThread.ActivityClientRecord activityClientRecord =
+ mock(ActivityThread.ActivityClientRecord.class);
doReturn(mActivityResources).when(activity).getResources();
final IBinder activityToken = new Binder();
doReturn(activityToken).when(activity).getActivityToken();
doReturn(activity).when(mSplitController).getActivity(activityToken);
+ doReturn(activityClientRecord).when(mSplitController).getActivityClientRecord(activity);
doReturn(taskId).when(activity).getTaskId();
doReturn(new ActivityInfo()).when(activity).getActivityInfo();
doReturn(DEFAULT_DISPLAY).when(activity).getDisplayId();
+ doReturn(mActivityWindowInfo).when(activityClientRecord).getActivityWindowInfo();
return activity;
}
/** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) {
- final TaskFragmentContainer container = mSplitController.newContainer(activity,
- activity.getTaskId());
+ final TaskFragmentContainer container = createTfContainer(mSplitController,
+ activity.getTaskId(), activity);
setupTaskFragmentInfo(container, activity);
return container;
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index 941b4e1c3e41..816e2dae1e5b 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS;
+import static android.window.TaskFragmentOperation.OP_TYPE_SET_PINNED;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY;
@@ -30,6 +31,7 @@ import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActi
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTfContainer;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createWindowLayoutInfo;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
import static androidx.window.extensions.embedding.SplitPresenter.EXPAND_CONTAINERS_ATTRIBUTES;
@@ -85,10 +87,12 @@ import androidx.window.extensions.layout.WindowLayoutComponentImpl;
import androidx.window.extensions.layout.WindowLayoutInfo;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
@@ -106,6 +110,10 @@ import java.util.ArrayList;
public class SplitPresenterTest {
private Activity mActivity;
+
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
+
@Mock
private Resources mActivityResources;
@Mock
@@ -119,7 +127,6 @@ public class SplitPresenterTest {
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent)
.getCurrentWindowLayoutInfo(anyInt(), any());
DeviceStateManagerFoldingFeatureProducer producer =
@@ -133,7 +140,7 @@ public class SplitPresenterTest {
@Test
public void testCreateTaskFragment() {
- final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container = createTfContainer(mController, mActivity);
mPresenter.createTaskFragment(mTransaction, container.getTaskFragmentToken(),
mActivity.getActivityToken(), TASK_BOUNDS, WINDOWING_MODE_MULTI_WINDOW);
@@ -144,7 +151,7 @@ public class SplitPresenterTest {
@Test
public void testResizeTaskFragment() {
- final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container = createTfContainer(mController, mActivity);
mPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), mTaskFragmentInfo);
mPresenter.resizeTaskFragment(mTransaction, container.getTaskFragmentToken(), TASK_BOUNDS);
@@ -160,7 +167,7 @@ public class SplitPresenterTest {
@Test
public void testUpdateWindowingMode() {
- final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container = createTfContainer(mController, mActivity);
mPresenter.mFragmentInfos.put(container.getTaskFragmentToken(), mTaskFragmentInfo);
mPresenter.updateWindowingMode(mTransaction, container.getTaskFragmentToken(),
WINDOWING_MODE_MULTI_WINDOW);
@@ -178,8 +185,8 @@ public class SplitPresenterTest {
@Test
public void testSetAdjacentTaskFragments() {
- final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID);
- final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container0 = createTfContainer(mController, mActivity);
+ final TaskFragmentContainer container1 = createTfContainer(mController, mActivity);
mPresenter.setAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken(),
container1.getTaskFragmentToken(), null /* adjacentParams */);
@@ -196,8 +203,8 @@ public class SplitPresenterTest {
@Test
public void testClearAdjacentTaskFragments() {
- final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID);
- final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container0 = createTfContainer(mController, mActivity);
+ final TaskFragmentContainer container1 = createTfContainer(mController, mActivity);
// No request to clear as it is not set by default.
mPresenter.clearAdjacentTaskFragments(mTransaction, container0.getTaskFragmentToken());
@@ -218,8 +225,8 @@ public class SplitPresenterTest {
@Test
public void testSetCompanionTaskFragment() {
- final TaskFragmentContainer container0 = mController.newContainer(mActivity, TASK_ID);
- final TaskFragmentContainer container1 = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container0 = createTfContainer(mController, mActivity);
+ final TaskFragmentContainer container1 = createTfContainer(mController, mActivity);
mPresenter.setCompanionTaskFragment(mTransaction, container0.getTaskFragmentToken(),
container1.getTaskFragmentToken());
@@ -236,7 +243,7 @@ public class SplitPresenterTest {
@Test
public void testSetTaskFragmentDimOnTask() {
- final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container = createTfContainer(mController, mActivity);
mPresenter.setTaskFragmentDimOnTask(mTransaction, container.getTaskFragmentToken(), true);
verify(mTransaction).addTaskFragmentOperation(eq(container.getTaskFragmentToken()), any());
@@ -249,7 +256,7 @@ public class SplitPresenterTest {
@Test
public void testUpdateAnimationParams() {
- final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer container = createTfContainer(mController, mActivity);
// Verify the default.
assertTrue(container.areLastRequestedAnimationParamsEqual(
@@ -280,6 +287,28 @@ public class SplitPresenterTest {
}
@Test
+ public void testSetTaskFragmentPinned() {
+ final TaskFragmentContainer container = createTfContainer(mController, mActivity);
+
+ // Verify the default.
+ assertFalse(container.isPinned());
+
+ mPresenter.setTaskFragmentPinned(mTransaction, container, true);
+
+ final TaskFragmentOperation expectedOperation = new TaskFragmentOperation.Builder(
+ OP_TYPE_SET_PINNED).setBooleanValue(true).build();
+ verify(mTransaction).addTaskFragmentOperation(container.getTaskFragmentToken(),
+ expectedOperation);
+ assertTrue(container.isPinned());
+
+ // No request to set the same animation params.
+ clearInvocations(mTransaction);
+ mPresenter.setTaskFragmentPinned(mTransaction, container, true);
+
+ verify(mTransaction, never()).addTaskFragmentOperation(any(), any());
+ }
+
+ @Test
public void testGetMinDimensionsForIntent() {
final Intent intent = new Intent(ApplicationProvider.getApplicationContext(),
MinimumDimensionActivity.class);
@@ -639,8 +668,8 @@ public class SplitPresenterTest {
public void testExpandSplitContainerIfNeeded() {
Activity secondaryActivity = createMockActivity();
SplitRule splitRule = createSplitRule(mActivity, secondaryActivity);
- TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID);
- TaskFragmentContainer secondaryTf = mController.newContainer(secondaryActivity, TASK_ID);
+ TaskFragmentContainer primaryTf = createTfContainer(mController, mActivity);
+ TaskFragmentContainer secondaryTf = createTfContainer(mController, secondaryActivity);
SplitContainer splitContainer = new SplitContainer(primaryTf, secondaryActivity,
secondaryTf, splitRule, SPLIT_ATTRIBUTES);
@@ -665,8 +694,8 @@ public class SplitPresenterTest {
assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
- verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken());
- verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken());
+ verify(mPresenter).expandTaskFragment(mTransaction, primaryTf);
+ verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf);
splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES);
clearInvocations(mPresenter);
@@ -675,15 +704,15 @@ public class SplitPresenterTest {
splitContainer, mActivity, null /* secondaryActivity */,
new Intent(ApplicationProvider.getApplicationContext(),
MinimumDimensionActivity.class)));
- verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken());
- verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken());
+ verify(mPresenter).expandTaskFragment(mTransaction, primaryTf);
+ verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf);
}
@Test
public void testCreateNewSplitContainer_secondaryAbovePrimary() {
final Activity secondaryActivity = createMockActivity();
- final TaskFragmentContainer bottomTf = mController.newContainer(secondaryActivity, TASK_ID);
- final TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer bottomTf = createTfContainer(mController, secondaryActivity);
+ final TaskFragmentContainer primaryTf = createTfContainer(mController, mActivity);
final SplitPairRule rule = createSplitPairRuleBuilder(pair ->
pair.first == mActivity && pair.second == secondaryActivity, pair -> false,
metrics -> true)
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
index a5995a3027ac..284723279b80 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java
@@ -29,6 +29,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -42,11 +43,12 @@ import android.window.TaskFragmentParentInfo;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
-import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
import java.util.List;
@@ -56,18 +58,19 @@ import java.util.List;
* Build/Install/Run:
* atest WMJetpackUnitTests:TaskContainerTest
*/
+
+// Suppress GuardedBy warning on unit tests
+@SuppressWarnings("GuardedBy")
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
public class TaskContainerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
+
@Mock
private SplitController mController;
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
- }
-
@Test
public void testGetWindowingModeForSplitTaskFragment() {
final TaskContainer taskContainer = createTestTaskContainer();
@@ -127,8 +130,11 @@ public class TaskContainerTest {
assertTrue(taskContainer.isEmpty());
- final TaskFragmentContainer tf = new TaskFragmentContainer(null /* activity */,
- new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */);
+ doReturn(taskContainer).when(mController).getTaskContainer(anyInt());
+ final TaskFragmentContainer tf = new TaskFragmentContainer.Builder(mController,
+ taskContainer.getTaskId(), null /* activityInTask */)
+ .setPendingAppearedIntent(new Intent())
+ .build();
assertFalse(taskContainer.isEmpty());
@@ -143,12 +149,17 @@ public class TaskContainerTest {
final TaskContainer taskContainer = createTestTaskContainer();
assertNull(taskContainer.getTopNonFinishingTaskFragmentContainer());
- final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */,
- new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */);
+ doReturn(taskContainer).when(mController).getTaskContainer(anyInt());
+ final TaskFragmentContainer tf0 = new TaskFragmentContainer.Builder(mController,
+ taskContainer.getTaskId(), null /* activityInTask */)
+ .setPendingAppearedIntent(new Intent())
+ .build();
assertEquals(tf0, taskContainer.getTopNonFinishingTaskFragmentContainer());
- final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */,
- new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer tf1 = new TaskFragmentContainer.Builder(mController,
+ taskContainer.getTaskId(), null /* activityInTask */)
+ .setPendingAppearedIntent(new Intent())
+ .build();
assertEquals(tf1, taskContainer.getTopNonFinishingTaskFragmentContainer());
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java
index 379ea0c534ba..a1e9f08585f6 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentAnimationControllerTest.java
@@ -27,10 +27,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
/**
* Test class for {@link TaskFragmentAnimationController}.
@@ -42,13 +44,15 @@ import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class TaskFragmentAnimationControllerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
+
@Mock
private TaskFragmentOrganizer mOrganizer;
private TaskFragmentAnimationController mAnimationController;
@Before
public void setup() {
- MockitoAnnotations.initMocks(this);
mAnimationController = new TaskFragmentAnimationController(mOrganizer);
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index cc00a49604ee..7fab371cb790 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -51,10 +51,12 @@ import androidx.window.extensions.layout.WindowLayoutComponentImpl;
import com.google.android.collect.Lists;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
import java.util.List;
@@ -71,6 +73,9 @@ import java.util.List;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class TaskFragmentContainerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
+
@Mock
private SplitPresenter mPresenter;
private SplitController mController;
@@ -83,7 +88,6 @@ public class TaskFragmentContainerTest {
@Before
public void setup() {
- MockitoAnnotations.initMocks(this);
DeviceStateManagerFoldingFeatureProducer producer =
mock(DeviceStateManagerFoldingFeatureProducer.class);
WindowLayoutComponentImpl component = mock(WindowLayoutComponentImpl.class);
@@ -96,24 +100,27 @@ public class TaskFragmentContainerTest {
@Test
public void testNewContainer() {
final TaskContainer taskContainer = createTestTaskContainer();
+ mController.addTaskContainer(taskContainer.getTaskId(), taskContainer);
// One of the activity and the intent must be non-null
assertThrows(IllegalArgumentException.class,
- () -> new TaskFragmentContainer(null, null, taskContainer, mController,
- null /* pairedPrimaryContainer */));
+ () -> new TaskFragmentContainer.Builder(mController, taskContainer.getTaskId(),
+ null /* activityInTask */).build());
// One of the activity and the intent must be null.
assertThrows(IllegalArgumentException.class,
- () -> new TaskFragmentContainer(mActivity, mIntent, taskContainer, mController,
- null /* pairedPrimaryContainer */));
+ () -> new TaskFragmentContainer.Builder(mController, taskContainer.getTaskId(),
+ null /* activityInTask */)
+ .setPendingAppearedActivity(createMockActivity())
+ .setPendingAppearedIntent(mIntent)
+ .build());
}
@Test
public void testFinish() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container = new TaskFragmentContainer(mActivity,
- null /* pendingAppearedIntent */, taskContainer, mController,
- null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container = createTaskFragmentContainer(taskContainer,
+ mActivity, null /* pendingAppearedIntent */);
doReturn(container).when(mController).getContainerWithActivity(mActivity);
// Only remove the activity, but not clear the reference until appeared.
@@ -144,15 +151,13 @@ public class TaskFragmentContainerTest {
@Test
public void testFinish_notFinishActivityThatIsReparenting() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity,
- null /* pendingAppearedIntent */, taskContainer, mController,
- null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container0 = createTaskFragmentContainer(taskContainer,
+ mActivity, null /* pendingAppearedIntent */);
final TaskFragmentInfo info = createMockTaskFragmentInfo(container0, mActivity);
container0.setInfo(mTransaction, info);
// Request to reparent the activity to a new TaskFragment.
- final TaskFragmentContainer container1 = new TaskFragmentContainer(mActivity,
- null /* pendingAppearedIntent */, taskContainer, mController,
- null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container1 = createTaskFragmentContainer(taskContainer,
+ mActivity, null /* pendingAppearedIntent */);
doReturn(container1).when(mController).getContainerWithActivity(mActivity);
// The activity is requested to be reparented, so don't finish it.
@@ -167,15 +172,13 @@ public class TaskFragmentContainerTest {
public void testFinish_alwaysFinishPlaceholder() {
// Register container1 as a placeholder
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container0 = new TaskFragmentContainer(mActivity,
- null /* pendingAppearedIntent */, taskContainer, mController,
- null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container0 = createTaskFragmentContainer(taskContainer,
+ mActivity, null /* pendingAppearedIntent */);
final TaskFragmentInfo info0 = createMockTaskFragmentInfo(container0, mActivity);
container0.setInfo(mTransaction, info0);
final Activity placeholderActivity = createMockActivity();
- final TaskFragmentContainer container1 = new TaskFragmentContainer(placeholderActivity,
- null /* pendingAppearedIntent */, taskContainer, mController,
- null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container1 = createTaskFragmentContainer(taskContainer,
+ placeholderActivity, null /* pendingAppearedIntent */);
final TaskFragmentInfo info1 = createMockTaskFragmentInfo(container1, placeholderActivity);
container1.setInfo(mTransaction, info1);
final SplitAttributes splitAttributes = new SplitAttributes.Builder().build();
@@ -203,9 +206,8 @@ public class TaskFragmentContainerTest {
public void testSetInfo() {
final TaskContainer taskContainer = createTestTaskContainer();
// Pending activity should be cleared when it has appeared on server side.
- final TaskFragmentContainer pendingActivityContainer = new TaskFragmentContainer(mActivity,
- null /* pendingAppearedIntent */, taskContainer, mController,
- null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer pendingActivityContainer = createTaskFragmentContainer(
+ taskContainer, mActivity, null /* pendingAppearedIntent */);
assertTrue(pendingActivityContainer.mPendingAppearedActivities.contains(
mActivity.getActivityToken()));
@@ -217,9 +219,8 @@ public class TaskFragmentContainerTest {
assertTrue(pendingActivityContainer.mPendingAppearedActivities.isEmpty());
// Pending intent should be cleared when the container becomes non-empty.
- final TaskFragmentContainer pendingIntentContainer = new TaskFragmentContainer(
- null /* pendingAppearedActivity */, mIntent, taskContainer, mController,
- null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer pendingIntentContainer = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
assertEquals(mIntent, pendingIntentContainer.getPendingAppearedIntent());
@@ -233,8 +234,8 @@ public class TaskFragmentContainerTest {
@Test
public void testIsWaitingActivityAppear() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
assertTrue(container.isWaitingActivityAppear());
@@ -255,8 +256,8 @@ public class TaskFragmentContainerTest {
public void testAppearEmptyTimeout() {
doNothing().when(mController).onTaskFragmentAppearEmptyTimeout(any(), any());
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
assertNull(container.mAppearEmptyTimeout);
@@ -295,8 +296,8 @@ public class TaskFragmentContainerTest {
@Test
public void testCollectNonFinishingActivities() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
List<Activity> activities = container.collectNonFinishingActivities();
assertTrue(activities.isEmpty());
@@ -323,8 +324,8 @@ public class TaskFragmentContainerTest {
@Test
public void testCollectNonFinishingActivities_checkIfStable() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
// In case mInfo is null, collectNonFinishingActivities(true) should return null.
List<Activity> activities =
@@ -349,8 +350,8 @@ public class TaskFragmentContainerTest {
@Test
public void testAddPendingActivity() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
container.addPendingAppearedActivity(mActivity);
assertEquals(1, container.collectNonFinishingActivities().size());
@@ -363,10 +364,10 @@ public class TaskFragmentContainerTest {
@Test
public void testIsAbove() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container0 = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
- final TaskFragmentContainer container1 = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container0 = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
+ final TaskFragmentContainer container1 = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
assertTrue(container1.isAbove(container0));
assertFalse(container0.isAbove(container1));
@@ -375,8 +376,8 @@ public class TaskFragmentContainerTest {
@Test
public void testGetBottomMostActivity() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
container.addPendingAppearedActivity(mActivity);
assertEquals(mActivity, container.getBottomMostActivity());
@@ -392,8 +393,8 @@ public class TaskFragmentContainerTest {
@Test
public void testOnActivityDestroyed() {
final TaskContainer taskContainer = createTestTaskContainer(mController);
- final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
container.addPendingAppearedActivity(mActivity);
final List<IBinder> activities = new ArrayList<>();
activities.add(mActivity.getActivityToken());
@@ -402,7 +403,7 @@ public class TaskFragmentContainerTest {
assertTrue(container.hasActivity(mActivity.getActivityToken()));
- taskContainer.onActivityDestroyed(mActivity.getActivityToken());
+ taskContainer.onActivityDestroyed(mTransaction, mActivity.getActivityToken());
// It should not contain the destroyed Activity.
assertFalse(container.hasActivity(mActivity.getActivityToken()));
@@ -412,8 +413,8 @@ public class TaskFragmentContainerTest {
public void testIsInIntermediateState() {
// True if no info set.
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
spyOn(taskContainer);
doReturn(true).when(taskContainer).isVisible();
@@ -475,8 +476,8 @@ public class TaskFragmentContainerTest {
@Test
public void testHasAppearedActivity() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
container.addPendingAppearedActivity(mActivity);
assertFalse(container.hasAppearedActivity(mActivity.getActivityToken()));
@@ -492,8 +493,8 @@ public class TaskFragmentContainerTest {
@Test
public void testHasPendingAppearedActivity() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
container.addPendingAppearedActivity(mActivity);
assertTrue(container.hasPendingAppearedActivity(mActivity.getActivityToken()));
@@ -509,10 +510,10 @@ public class TaskFragmentContainerTest {
@Test
public void testHasActivity() {
final TaskContainer taskContainer = createTestTaskContainer(mController);
- final TaskFragmentContainer container1 = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
- final TaskFragmentContainer container2 = new TaskFragmentContainer(null /* activity */,
- mIntent, taskContainer, mController, null /* pairedPrimaryContainer */);
+ final TaskFragmentContainer container1 = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
+ final TaskFragmentContainer container2 = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, mIntent);
// Activity is pending appeared on container2.
container2.addPendingAppearedActivity(mActivity);
@@ -534,9 +535,7 @@ public class TaskFragmentContainerTest {
// container1.
container2.setInfo(mTransaction, mInfo);
- assertTrue(container1.hasActivity(mActivity.getActivityToken()));
- assertFalse(container2.hasActivity(mActivity.getActivityToken()));
-
+ assertTrue(container2.hasActivity(mActivity.getActivityToken()));
// When the pending appeared record is removed from container1, we respect the appeared
// record in container2.
container1.removePendingAppearedActivity(mActivity.getActivityToken());
@@ -548,17 +547,19 @@ public class TaskFragmentContainerTest {
@Test
public void testNewContainerWithPairedPrimaryContainer() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer tf0 = new TaskFragmentContainer(
- null /* pendingAppearedActivity */, new Intent(), taskContainer, mController,
- null /* pairedPrimaryTaskFragment */);
- final TaskFragmentContainer tf1 = new TaskFragmentContainer(
- null /* pendingAppearedActivity */, new Intent(), taskContainer, mController,
- null /* pairedPrimaryTaskFragment */);
+ mController.addTaskContainer(taskContainer.getTaskId(), taskContainer);
+ final TaskFragmentContainer tf0 = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, new Intent());
+ final TaskFragmentContainer tf1 = createTaskFragmentContainer(
+ taskContainer, null /* pendingAppearedActivity */, new Intent());
// When tf2 is created with using tf0 as pairedPrimaryContainer, tf2 should be inserted
// right above tf0.
- final TaskFragmentContainer tf2 = new TaskFragmentContainer(
- null /* pendingAppearedActivity */, new Intent(), taskContainer, mController, tf0);
+ final TaskFragmentContainer tf2 = new TaskFragmentContainer.Builder(mController,
+ taskContainer.getTaskId(), null /* activityInTask */)
+ .setPendingAppearedIntent(new Intent())
+ .setPairedPrimaryContainer(tf0)
+ .build();
assertEquals(0, taskContainer.indexOf(tf0));
assertEquals(1, taskContainer.indexOf(tf2));
assertEquals(2, taskContainer.indexOf(tf1));
@@ -567,18 +568,15 @@ public class TaskFragmentContainerTest {
@Test
public void testNewContainerWithPairedPendingAppearedActivity() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer tf0 = new TaskFragmentContainer(
- createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController,
- null /* pairedPrimaryTaskFragment */);
- final TaskFragmentContainer tf1 = new TaskFragmentContainer(
- null /* pendingAppearedActivity */, new Intent(), taskContainer, mController,
- null /* pairedPrimaryTaskFragment */);
+ final TaskFragmentContainer tf0 = createTaskFragmentContainer(taskContainer,
+ createMockActivity(), null /* pendingAppearedIntent */);
+ final TaskFragmentContainer tf1 = createTaskFragmentContainer(taskContainer,
+ null /* pendingAppearedActivity */, new Intent());
// When tf2 is created with pendingAppearedActivity, tf2 should be inserted below any
// TaskFragment without any Activity.
- final TaskFragmentContainer tf2 = new TaskFragmentContainer(
- createMockActivity(), null /* pendingAppearedIntent */, taskContainer, mController,
- null /* pairedPrimaryTaskFragment */);
+ final TaskFragmentContainer tf2 = createTaskFragmentContainer(taskContainer,
+ createMockActivity(), null /* pendingAppearedIntent */);
assertEquals(0, taskContainer.indexOf(tf0));
assertEquals(1, taskContainer.indexOf(tf2));
assertEquals(2, taskContainer.indexOf(tf1));
@@ -587,9 +585,8 @@ public class TaskFragmentContainerTest {
@Test
public void testIsVisible() {
final TaskContainer taskContainer = createTestTaskContainer();
- final TaskFragmentContainer container = new TaskFragmentContainer(
- null /* pendingAppearedActivity */, new Intent(), taskContainer, mController,
- null /* pairedPrimaryTaskFragment */);
+ final TaskFragmentContainer container = createTaskFragmentContainer(taskContainer,
+ null /* pendingAppearedActivity */, new Intent());
// Not visible when there is not appeared.
assertFalse(container.isVisible());
@@ -615,4 +612,14 @@ public class TaskFragmentContainerTest {
doReturn(activity).when(mController).getActivity(activityToken);
return activity;
}
+
+ private TaskFragmentContainer createTaskFragmentContainer(TaskContainer taskContainer,
+ Activity pendingAppearedActivity, Intent pendingAppearedIntent) {
+ final int taskId = taskContainer.getTaskId();
+ mController.addTaskContainer(taskId, taskContainer);
+ return new TaskFragmentContainer.Builder(mController, taskId, pendingAppearedActivity)
+ .setPendingAppearedActivity(pendingAppearedActivity)
+ .setPendingAppearedIntent(pendingAppearedIntent)
+ .build();
+ }
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java
index 459b6d2c31f9..2598dd63bbde 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TransactionManagerTest.java
@@ -41,10 +41,12 @@ import androidx.test.filters.SmallTest;
import androidx.window.extensions.embedding.TransactionManager.TransactionRecord;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
/**
* Test class for {@link TransactionManager}.
@@ -56,6 +58,8 @@ import org.mockito.MockitoAnnotations;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class TransactionManagerTest {
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
@Mock
private TaskFragmentOrganizer mOrganizer;
@@ -63,7 +67,6 @@ public class TransactionManagerTest {
@Before
public void setup() {
- MockitoAnnotations.initMocks(this);
mTransactionManager = new TransactionManager(mOrganizer);
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/util/DeduplicateConsumerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/util/DeduplicateConsumerTest.java
new file mode 100644
index 000000000000..4e9b4a02e1f8
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/util/DeduplicateConsumerTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import androidx.window.extensions.core.util.function.Consumer;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class to validate {@link DeduplicateConsumer}.
+ */
+public class DeduplicateConsumerTest {
+
+ @Test
+ public void test_duplicate_value_is_filtered() {
+ String value = "test_value";
+ List<String> expected = new ArrayList<>();
+ expected.add(value);
+ RecordingConsumer recordingConsumer = new RecordingConsumer();
+ DeduplicateConsumer<String> deduplicateConsumer =
+ new DeduplicateConsumer<>(recordingConsumer);
+
+ deduplicateConsumer.accept(value);
+ deduplicateConsumer.accept(value);
+
+ assertEquals(expected, recordingConsumer.getValues());
+ }
+
+ @Test
+ public void test_different_value_is_filtered() {
+ String value = "test_value";
+ String newValue = "test_value_new";
+ List<String> expected = new ArrayList<>();
+ expected.add(value);
+ expected.add(newValue);
+ RecordingConsumer recordingConsumer = new RecordingConsumer();
+ DeduplicateConsumer<String> deduplicateConsumer =
+ new DeduplicateConsumer<>(recordingConsumer);
+
+ deduplicateConsumer.accept(value);
+ deduplicateConsumer.accept(value);
+ deduplicateConsumer.accept(newValue);
+
+ assertEquals(expected, recordingConsumer.getValues());
+ }
+
+ @Test
+ public void test_match_against_consumer_property_returns_true() {
+ RecordingConsumer recordingConsumer = new RecordingConsumer();
+ DeduplicateConsumer<String> deduplicateConsumer =
+ new DeduplicateConsumer<>(recordingConsumer);
+
+ assertTrue(deduplicateConsumer.matchesConsumer(recordingConsumer));
+ }
+
+ @Test
+ public void test_match_against_self_returns_true() {
+ RecordingConsumer recordingConsumer = new RecordingConsumer();
+ DeduplicateConsumer<String> deduplicateConsumer =
+ new DeduplicateConsumer<>(recordingConsumer);
+
+ assertTrue(deduplicateConsumer.matchesConsumer(deduplicateConsumer));
+ }
+
+ private static final class RecordingConsumer implements Consumer<String> {
+
+ private final List<String> mValues = new ArrayList<>();
+
+ @Override
+ public void accept(String s) {
+ mValues.add(s);
+ }
+
+ public List<String> getValues() {
+ return mValues;
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 8829d1b9e0e1..25d3067a34bc 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -45,13 +45,13 @@ filegroup {
name: "wm_shell_util-sources",
srcs: [
"src/com/android/wm/shell/animation/Interpolators.java",
- "src/com/android/wm/shell/animation/PhysicsAnimator.kt",
"src/com/android/wm/shell/common/bubbles/*.kt",
"src/com/android/wm/shell/common/bubbles/*.java",
"src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt",
"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",
@@ -166,10 +166,28 @@ java_library {
},
}
+filegroup {
+ name: "wm_shell-shared-aidls",
+
+ srcs: [
+ "shared/**/*.aidl",
+ ],
+
+ path: "shared/src",
+}
+
java_library {
name: "WindowManager-Shell-shared",
- srcs: ["shared/**/*.java"],
+ srcs: [
+ "shared/**/*.java",
+ "shared/**/*.kt",
+ ":wm_shell-shared-aidls",
+ ],
+ static_libs: [
+ "androidx.dynamicanimation_dynamicanimation",
+ "jsr330",
+ ],
}
android_library {
@@ -188,12 +206,14 @@ 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",
"kotlinx-coroutines-android",
"kotlinx-coroutines-core",
- "iconloader_base",
+ "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib",
+ "//frameworks/libs/systemui:iconloader_base",
"com_android_wm_shell_flags_lib",
"com.android.window.flags.window-aconfig-java",
"WindowManager-Shell-proto",
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index 36d3313a9f3b..bcb1d292fce2 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -23,4 +23,36 @@
<uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" />
<uses-permission android:name="android.permission.WAKEUP_SURFACE_FLINGER" />
<uses-permission android:name="android.permission.READ_FRAME_BUFFER" />
+
+ <application>
+ <activity
+ android:name=".desktopmode.DesktopWallpaperActivity"
+ android:excludeFromRecents="true"
+ android:launchMode="singleInstance"
+ 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/Android.bp b/libs/WindowManager/Shell/aconfig/Android.bp
index 3891eebbc01f..7f8f57b172ff 100644
--- a/libs/WindowManager/Shell/aconfig/Android.bp
+++ b/libs/WindowManager/Shell/aconfig/Android.bp
@@ -10,4 +10,4 @@ aconfig_declarations {
java_aconfig_library {
name: "com_android_wm_shell_flags_lib",
aconfig_declarations: "com_android_wm_shell_flags",
-}
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index 7ff204c695f8..112eb617e7a6 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -1,3 +1,5 @@
+# proto-file: build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto
+
package: "com.android.wm.shell"
container: "system"
@@ -64,3 +66,58 @@ flag {
description: "Enables long-press action for nav handle when a bubble is expanded"
bug: "324910035"
}
+
+flag {
+ name: "enable_optional_bubble_overflow"
+ namespace: "multitasking"
+ description: "Hides the bubble overflow if there aren't any overflowed bubbles"
+ bug: "334175587"
+}
+
+flag {
+ name: "enable_retrievable_bubbles"
+ namespace: "multitasking"
+ description: "Allow opening bubbles overflow UI without bubbles being visible"
+ bug: "340337839"
+}
+
+flag {
+ name: "enable_bubble_stashing"
+ namespace: "multitasking"
+ description: "Allow the floating bubble stack to stash on the edge of the screen"
+ bug: "341361249"
+}
+
+flag {
+ name: "enable_tiny_taskbar"
+ namespace: "multitasking"
+ description: "Enables Taskbar on phones"
+ bug: "341784466"
+}
+
+flag {
+ name: "enable_bubble_anything"
+ namespace: "multitasking"
+ description: "Enable UI affordances to put other content into a bubble"
+ bug: "342245211"
+}
+
+flag {
+ name: "only_reuse_bubbled_task_when_launched_from_bubble"
+ namespace: "multitasking"
+ description: "Allow reusing bubbled tasks for new activities only when launching from bubbles"
+ bug: "328229865"
+ metadata {
+ 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
+ }
+}
diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp
index 1686d0d54dc4..1ad19c9f3033 100644
--- a/libs/WindowManager/Shell/multivalentTests/Android.bp
+++ b/libs/WindowManager/Shell/multivalentTests/Android.bp
@@ -46,6 +46,7 @@ android_robolectric_test {
exclude_srcs: ["src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt"],
static_libs: [
"junit",
+ "androidx.core_core-animation-testing",
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.ext.junit",
@@ -64,6 +65,7 @@ android_test {
static_libs: [
"WindowManager-Shell",
"junit",
+ "androidx.core_core-animation-testing",
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.ext.junit",
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
index 9cd14fca6a9d..9e1440d5716b 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
@@ -18,6 +18,7 @@ package com.android.wm.shell.bubbles
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
+import android.content.res.Resources
import android.graphics.Insets
import android.graphics.PointF
import android.graphics.Rect
@@ -26,8 +27,10 @@ import android.view.WindowManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.internal.protolog.common.ProtoLog
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT
+import com.android.wm.shell.common.bubbles.BubbleBarLocation
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
import org.junit.Before
@@ -41,6 +44,9 @@ class BubblePositionerTest {
private lateinit var positioner: BubblePositioner
private val context = ApplicationProvider.getApplicationContext<Context>()
+ private val resources: Resources
+ get() = context.resources
+
private val defaultDeviceConfig =
DeviceConfig(
windowBounds = Rect(0, 0, 1000, 2000),
@@ -53,6 +59,7 @@ class BubblePositionerTest {
@Before
fun setUp() {
+ ProtoLog.REQUIRE_PROTOLOGTOOL = false
val windowManager = context.getSystemService(WindowManager::class.java)
positioner = BubblePositioner(context, windowManager)
}
@@ -166,8 +173,9 @@ class BubblePositionerTest {
@Test
fun testGetRestingPosition_afterBoundsChange() {
- positioner.update(defaultDeviceConfig.copy(isLargeScreen = true,
- windowBounds = Rect(0, 0, 2000, 1600)))
+ positioner.update(
+ defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600))
+ )
// Set the resting position to the right side
var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
@@ -175,8 +183,9 @@ class BubblePositionerTest {
positioner.restingPosition = restingPosition
// Now make the device smaller
- positioner.update(defaultDeviceConfig.copy(isLargeScreen = false,
- windowBounds = Rect(0, 0, 1000, 1600)))
+ positioner.update(
+ defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600))
+ )
// Check the resting position is on the correct side
allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
@@ -200,6 +209,56 @@ class BubblePositionerTest {
}
@Test
+ fun testBubbleBarExpandedViewHeightAndWidth() {
+ val deviceConfig =
+ defaultDeviceConfig.copy(
+ // portrait orientation
+ isLandscape = false,
+ isLargeScreen = true,
+ insets = Insets.of(10, 20, 5, 15),
+ windowBounds = Rect(0, 0, 1800, 2600)
+ )
+
+ positioner.setShowingInBubbleBar(true)
+ positioner.update(deviceConfig)
+ positioner.bubbleBarTopOnScreen = 2500
+
+ val spaceBetweenTopInsetAndBubbleBarInLandscape = 1680
+ val expandedViewVerticalSpacing =
+ resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
+ val expectedHeight =
+ spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewVerticalSpacing
+ val expectedWidth = resources.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width)
+
+ assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth)
+ assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight)
+ }
+
+ @Test
+ fun testBubbleBarExpandedViewHeightAndWidth_screenWidthTooSmall() {
+ val screenWidth = 300
+ val deviceConfig =
+ defaultDeviceConfig.copy(
+ // portrait orientation
+ isLandscape = false,
+ isLargeScreen = true,
+ insets = Insets.of(10, 20, 5, 15),
+ windowBounds = Rect(0, 0, screenWidth, 2600)
+ )
+ positioner.setShowingInBubbleBar(true)
+ positioner.update(deviceConfig)
+ positioner.bubbleBarTopOnScreen = 2500
+
+ val spaceBetweenTopInsetAndBubbleBarInLandscape = 180
+ val expandedViewSpacing =
+ resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
+ val expectedHeight = spaceBetweenTopInsetAndBubbleBarInLandscape - 2 * expandedViewSpacing
+ val expectedWidth = screenWidth - 15 /* horizontal insets */ - 2 * expandedViewSpacing
+ assertThat(positioner.getExpandedViewWidthForBubbleBar(false)).isEqualTo(expectedWidth)
+ assertThat(positioner.getExpandedViewHeightForBubbleBar(false)).isEqualTo(expectedHeight)
+ }
+
+ @Test
fun testGetExpandedViewHeight_max() {
val deviceConfig =
defaultDeviceConfig.copy(
@@ -235,7 +294,8 @@ class BubblePositionerTest {
0 /* taskId */,
null /* locus */,
true /* isDismissable */,
- directExecutor()) {}
+ directExecutor()
+ ) {}
// Ensure the height is the same as the desired value
assertThat(positioner.getExpandedViewHeight(bubble))
@@ -262,7 +322,8 @@ class BubblePositionerTest {
0 /* taskId */,
null /* locus */,
true /* isDismissable */,
- directExecutor()) {}
+ directExecutor()
+ ) {}
// Ensure the height is the same as the desired value
val minHeight =
@@ -470,20 +531,109 @@ class BubblePositionerTest {
fun testGetTaskViewContentWidth_onLeft() {
positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */)
- val paddings = positioner.getExpandedViewContainerPadding(true /* onLeft */,
- false /* isOverflow */)
- assertThat(taskViewWidth).isEqualTo(
- positioner.screenRect.width() - paddings[0] - paddings[2])
+ val paddings =
+ positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */)
+ assertThat(taskViewWidth)
+ .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
}
@Test
fun testGetTaskViewContentWidth_onRight() {
positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */)
- val paddings = positioner.getExpandedViewContainerPadding(false /* onLeft */,
- false /* isOverflow */)
- assertThat(taskViewWidth).isEqualTo(
- positioner.screenRect.width() - paddings[0] - paddings[2])
+ val paddings =
+ positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */)
+ assertThat(taskViewWidth)
+ .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
+ }
+
+ @Test
+ fun testIsBubbleBarOnLeft_defaultsToRight() {
+ positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT
+ assertThat(positioner.isBubbleBarOnLeft).isFalse()
+
+ // Check that left and right return expected position
+ positioner.bubbleBarLocation = BubbleBarLocation.LEFT
+ assertThat(positioner.isBubbleBarOnLeft).isTrue()
+ positioner.bubbleBarLocation = BubbleBarLocation.RIGHT
+ assertThat(positioner.isBubbleBarOnLeft).isFalse()
+ }
+
+ @Test
+ fun testIsBubbleBarOnLeft_rtlEnabled_defaultsToLeft() {
+ positioner.update(defaultDeviceConfig.copy(isRtl = true))
+
+ positioner.bubbleBarLocation = BubbleBarLocation.DEFAULT
+ assertThat(positioner.isBubbleBarOnLeft).isTrue()
+
+ // Check that left and right return expected position
+ positioner.bubbleBarLocation = BubbleBarLocation.LEFT
+ assertThat(positioner.isBubbleBarOnLeft).isTrue()
+ positioner.bubbleBarLocation = BubbleBarLocation.RIGHT
+ assertThat(positioner.isBubbleBarOnLeft).isFalse()
+ }
+
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_onLeft() {
+ testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false)
+ }
+
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_onRight() {
+ testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false)
+ }
+
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() {
+ testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true)
+ }
+
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() {
+ testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true)
+ }
+
+ private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) {
+ positioner.setShowingInBubbleBar(true)
+ val windowBounds = Rect(0, 0, 2000, 2600)
+ val insets = Insets.of(10, 20, 5, 15)
+ val deviceConfig =
+ defaultDeviceConfig.copy(
+ isLargeScreen = true,
+ isLandscape = true,
+ insets = insets,
+ windowBounds = windowBounds
+ )
+ positioner.update(deviceConfig)
+
+ val bubbleBarHeight = 100
+ positioner.bubbleBarTopOnScreen = windowBounds.bottom - insets.bottom - bubbleBarHeight
+
+ val expandedViewPadding =
+ context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
+
+ val left: Int
+ val right: Int
+ if (onLeft) {
+ // Pin to the left, calculate right
+ left = deviceConfig.insets.left + expandedViewPadding
+ right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow)
+ } else {
+ // Pin to the right, calculate left
+ right =
+ deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding
+ left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow)
+ }
+ // Above the bubble bar
+ val bottom = positioner.bubbleBarTopOnScreen - expandedViewPadding
+ // Calculate right and top based on size
+ val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow)
+ val expectedBounds = Rect(left, top, right, bottom)
+
+ val bounds = Rect()
+ positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds)
+
+ assertThat(bounds).isEqualTo(expectedBounds)
}
private val defaultYPosition: Float
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 8989fc543044..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
@@ -18,40 +18,52 @@ package com.android.wm.shell.bubbles
import android.content.Context
import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.content.res.Resources
import android.graphics.Color
import android.graphics.drawable.Icon
import android.os.UserHandle
+import android.platform.test.flag.junit.SetFlagsRule
import android.view.IWindowManager
import android.view.WindowManager
import android.view.WindowManagerGlobal
-import androidx.test.annotation.UiThreadTest
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.internal.protolog.common.ProtoLog
import com.android.launcher3.icons.BubbleIconFactory
+import com.android.wm.shell.Flags
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
+import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix
import com.android.wm.shell.common.FloatingContentCoordinator
import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
import com.android.wm.shell.taskview.TaskView
import com.android.wm.shell.taskview.TaskViewTaskController
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.MoreExecutors.directExecutor
-import java.util.concurrent.Semaphore
-import java.util.concurrent.TimeUnit
-import java.util.function.Consumer
+import org.junit.After
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
/** Unit tests for [BubbleStackView]. */
@SmallTest
@RunWith(AndroidJUnit4::class)
class BubbleStackViewTest {
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
private val context = ApplicationProvider.getApplicationContext<Context>()
private lateinit var positioner: BubblePositioner
private lateinit var iconFactory: BubbleIconFactory
@@ -61,9 +73,12 @@ class BubbleStackViewTest {
private lateinit var windowManager: IWindowManager
private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory
private lateinit var bubbleData: BubbleData
+ private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager
+ private var sysuiProxy = mock<SysuiProxy>()
@Before
fun setUp() {
+ PhysicsAnimatorTestUtils.prepareForTest()
// Disable protolog tool when running the tests from studio
ProtoLog.REQUIRE_PROTOLOGTOOL = false
windowManager = WindowManagerGlobal.getWindowManagerService()!!
@@ -80,7 +95,6 @@ class BubbleStackViewTest {
)
)
positioner = BubblePositioner(context, windowManager)
- val bubbleStackViewManager = FakeBubbleStackViewManager()
bubbleData =
BubbleData(
context,
@@ -89,8 +103,7 @@ class BubbleStackViewTest {
BubbleEducationController(context),
shellExecutor
)
-
- val sysuiProxy = mock<SysuiProxy>()
+ bubbleStackViewManager = FakeBubbleStackViewManager()
expandedViewManager = FakeBubbleExpandedViewManager()
bubbleTaskViewFactory = FakeBubbleTaskViewFactory()
bubbleStackView =
@@ -104,34 +117,267 @@ class BubbleStackViewTest {
{ sysuiProxy },
shellExecutor
)
+
+ context
+ .getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
+ .edit()
+ .putBoolean(StackEducationView.PREF_STACK_EDUCATION, true)
+ .apply()
+ }
+
+ @After
+ fun tearDown() {
+ PhysicsAnimatorTestUtils.tearDown()
}
- @UiThreadTest
@Test
fun addBubble() {
val bubble = createAndInflateBubble()
- bubbleStackView.addBubble(bubble)
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleStackView.addBubble(bubble)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
assertThat(bubbleStackView.bubbleCount).isEqualTo(1)
}
- @UiThreadTest
@Test
fun tapBubbleToExpand() {
val bubble = createAndInflateBubble()
- bubbleStackView.addBubble(bubble)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleStackView.addBubble(bubble)
+ }
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
assertThat(bubbleStackView.bubbleCount).isEqualTo(1)
+ var lastUpdate: BubbleData.Update? = null
+ val semaphore = Semaphore(0)
+ val listener =
+ BubbleData.Listener { update ->
+ lastUpdate = update
+ semaphore.release()
+ }
+ bubbleData.setListener(listener)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubble.iconView!!.performClick()
+ // we're checking the expanded state in BubbleData because that's the source of truth.
+ // This will eventually propagate an update back to the stack view, but setting the
+ // entire pipeline is outside the scope of a unit test.
+ assertThat(bubbleData.isExpanded).isTrue()
+ }
+
+ assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+ assertThat(lastUpdate).isNotNull()
+ assertThat(lastUpdate!!.expandedChanged).isTrue()
+ assertThat(lastUpdate!!.expanded).isTrue()
+ }
+
+ @Test
+ fun tapDifferentBubble_shouldReorder() {
+ val bubble1 = createAndInflateChatBubble(key = "bubble1")
+ val bubble2 = createAndInflateChatBubble(key = "bubble2")
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleStackView.addBubble(bubble1)
+ bubbleStackView.addBubble(bubble2)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+ assertThat(bubbleStackView.bubbleCount).isEqualTo(2)
+ assertThat(bubbleData.bubbles).hasSize(2)
+ assertThat(bubbleData.selectedBubble).isEqualTo(bubble2)
+ assertThat(bubble2.iconView).isNotNull()
+
+ var lastUpdate: BubbleData.Update? = null
+ val semaphore = Semaphore(0)
+ val listener =
+ BubbleData.Listener { update ->
+ lastUpdate = update
+ semaphore.release()
+ }
+ bubbleData.setListener(listener)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubble2.iconView!!.performClick()
+ assertThat(bubbleData.isExpanded).isTrue()
+
+ bubbleStackView.setSelectedBubble(bubble2)
+ bubbleStackView.isExpanded = true
+ }
+
+ assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+ assertThat(lastUpdate!!.expanded).isTrue()
+ assertThat(lastUpdate!!.bubbles.map { it.key })
+ .containsExactly("bubble2", "bubble1")
+ .inOrder()
+
+ // wait for idle to allow the animation to start
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ // wait for the expansion animation to complete before interacting with the bubbles
+ PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(
+ AnimatableScaleMatrix.SCALE_X, AnimatableScaleMatrix.SCALE_Y)
+
+ // tap on bubble1 to select it
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubble1.iconView!!.performClick()
+ }
+ assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+ assertThat(bubbleData.selectedBubble).isEqualTo(bubble1)
+
+ // tap on bubble1 again to collapse the stack
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ // we have to set the selected bubble in the stack view manually because we don't have a
+ // listener wired up.
+ bubbleStackView.setSelectedBubble(bubble1)
+ bubble1.iconView!!.performClick()
+ }
+
+ assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+ assertThat(bubbleData.selectedBubble).isEqualTo(bubble1)
+ assertThat(bubbleData.isExpanded).isFalse()
+ assertThat(lastUpdate!!.orderChanged).isTrue()
+ assertThat(lastUpdate!!.bubbles.map { it.key })
+ .containsExactly("bubble1", "bubble2")
+ .inOrder()
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+ @Test
+ fun testCreateStackView_noOverflowContents_noOverflow() {
+ bubbleStackView =
+ BubbleStackView(
+ context,
+ bubbleStackViewManager,
+ positioner,
+ bubbleData,
+ null,
+ FloatingContentCoordinator(),
+ { sysuiProxy },
+ shellExecutor
+ )
- bubble.iconView!!.performClick()
- // we're checking the expanded state in BubbleData because that's the source of truth. This
- // will eventually propagate an update back to the stack view, but setting the entire
- // pipeline is outside the scope of a unit test.
- assertThat(bubbleData.isExpanded).isTrue()
+ assertThat(bubbleData.overflowBubbles).isEmpty()
+ val bubbleOverflow = bubbleData.overflow
+ // Overflow shouldn't be attached
+ assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1)
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+ @Test
+ fun testCreateStackView_hasOverflowContents_hasOverflow() {
+ // Add a bubble to the overflow
+ val bubble1 = createAndInflateChatBubble(key = "bubble1")
+ bubbleData.notificationEntryUpdated(bubble1, false, false)
+ bubbleData.dismissBubbleWithKey(bubble1.key, Bubbles.DISMISS_USER_GESTURE)
+ assertThat(bubbleData.overflowBubbles).isNotEmpty()
+
+ bubbleStackView =
+ BubbleStackView(
+ context,
+ bubbleStackViewManager,
+ positioner,
+ bubbleData,
+ null,
+ FloatingContentCoordinator(),
+ { sysuiProxy },
+ shellExecutor
+ )
+ val bubbleOverflow = bubbleData.overflow
+ assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
+ }
+
+ @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+ @Test
+ fun testCreateStackView_noOverflowContents_hasOverflow() {
+ bubbleStackView =
+ BubbleStackView(
+ context,
+ bubbleStackViewManager,
+ positioner,
+ bubbleData,
+ null,
+ FloatingContentCoordinator(),
+ { sysuiProxy },
+ shellExecutor
+ )
+
+ assertThat(bubbleData.overflowBubbles).isEmpty()
+ val bubbleOverflow = bubbleData.overflow
+ assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+ @Test
+ fun showOverflow_true() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleStackView.showOverflow(true)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+ val bubbleOverflow = bubbleData.overflow
+ assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
+ }
+
+ @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+ @Test
+ fun showOverflow_false() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleStackView.showOverflow(true)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ val bubbleOverflow = bubbleData.overflow
+ assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleStackView.showOverflow(false)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+ // The overflow should've been removed
+ assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isEqualTo(-1)
+ }
+
+ @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
+ @Test
+ fun showOverflow_ignored() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ bubbleStackView.showOverflow(false)
+ }
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+
+ // showOverflow should've been ignored, so the overflow would be attached
+ val bubbleOverflow = bubbleData.overflow
+ assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1)
+ }
+
+ private fun createAndInflateChatBubble(key: String): Bubble {
+ val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button)
+ val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build()
+ val bubble =
+ Bubble(
+ key,
+ shortcutInfo,
+ /* desiredHeight= */ 6,
+ Resources.ID_NULL,
+ "title",
+ /* taskId= */ 0,
+ "locus",
+ /* isDismissable= */ true,
+ directExecutor()
+ ) {}
+ inflateBubble(bubble)
+ return bubble
}
private fun createAndInflateBubble(): Bubble {
val intent = Intent(Intent.ACTION_VIEW).setPackage(context.packageName)
val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button)
val bubble = Bubble.createAppBubble(intent, UserHandle(1), icon, directExecutor())
+ inflateBubble(bubble)
+ return bubble
+ }
+
+ private fun inflateBubble(bubble: Bubble) {
bubble.setInflateSynchronously(true)
bubbleData.notificationEntryUpdated(bubble, true, false)
@@ -152,7 +398,6 @@ class BubbleStackViewTest {
assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
assertThat(bubble.isInflated).isTrue()
- return bubble
}
private class FakeBubbleStackViewManager : BubbleStackViewManager {
@@ -176,7 +421,7 @@ class BubbleStackViewTest {
r.run()
}
- override fun removeCallbacks(r: Runnable) {}
+ override fun removeCallbacks(r: Runnable?) {}
override fun hasCallback(r: Runnable): Boolean = false
}
@@ -211,5 +456,7 @@ class BubbleStackViewTest {
override fun isStackExpanded(): Boolean = false
override fun isShowingAsBubbleBar(): Boolean = false
+
+ override fun hideCurrentInputMethod() {}
}
}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
new file mode 100644
index 000000000000..ace2c131050c
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
@@ -0,0 +1,459 @@
+/*
+ * 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.bar
+
+import android.content.Context
+import android.graphics.Insets
+import android.graphics.PointF
+import android.graphics.Rect
+import android.view.View
+import android.view.WindowManager
+import android.widget.FrameLayout
+import androidx.core.animation.AnimatorTestRule
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.internal.protolog.common.ProtoLog
+import com.android.wm.shell.R
+import com.android.wm.shell.bubbles.BubblePositioner
+import com.android.wm.shell.bubbles.DeviceConfig
+import com.android.wm.shell.common.bubbles.BaseBubblePinController
+import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION
+import com.android.wm.shell.common.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION
+import com.android.wm.shell.common.bubbles.BubbleBarLocation
+import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT
+import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.ClassRule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests for [BubbleExpandedViewPinController] */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleExpandedViewPinControllerTest {
+
+ companion object {
+ @JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule()
+
+ const val SCREEN_WIDTH = 2000
+ const val SCREEN_HEIGHT = 1000
+
+ const val BUBBLE_BAR_HEIGHT = 50
+ }
+
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+ private lateinit var positioner: BubblePositioner
+ private lateinit var container: FrameLayout
+
+ private lateinit var controller: BubbleExpandedViewPinController
+ private lateinit var testListener: TestLocationChangeListener
+
+ private val dropTargetView: View?
+ get() = container.findViewById(R.id.bubble_bar_drop_target)
+
+ private val pointOnLeft = PointF(100f, 100f)
+ private val pointOnRight = PointF(1900f, 500f)
+
+ @Before
+ fun setUp() {
+ ProtoLog.REQUIRE_PROTOLOGTOOL = false
+ container = FrameLayout(context)
+ val windowManager = context.getSystemService(WindowManager::class.java)
+ positioner = BubblePositioner(context, windowManager)
+ positioner.setShowingInBubbleBar(true)
+ val deviceConfig =
+ DeviceConfig(
+ windowBounds = Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT),
+ isLargeScreen = true,
+ isSmallTablet = false,
+ isLandscape = true,
+ isRtl = false,
+ insets = Insets.of(10, 20, 30, 40)
+ )
+ positioner.update(deviceConfig)
+ positioner.bubbleBarTopOnScreen =
+ SCREEN_HEIGHT - deviceConfig.insets.bottom - BUBBLE_BAR_HEIGHT
+ controller = BubbleExpandedViewPinController(context, container, positioner)
+ testListener = TestLocationChangeListener()
+ controller.setListener(testListener)
+ }
+
+ @After
+ fun tearDown() {
+ getInstrumentation().runOnMainSync { controller.onDragEnd() }
+ waitForAnimateOut()
+ }
+
+ /** Dragging on same side should not show drop target or trigger location changes */
+ @Test
+ fun drag_stayOnRightSide() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = false)
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ controller.onDragEnd()
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNull()
+ assertThat(testListener.locationChanges).isEmpty()
+ assertThat(testListener.locationReleases).containsExactly(RIGHT)
+ }
+
+ /** Dragging on same side should not show drop target or trigger location changes */
+ @Test
+ fun drag_stayOnLeftSide() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = true)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ controller.onDragEnd()
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNull()
+ assertThat(testListener.locationChanges).isEmpty()
+ assertThat(testListener.locationReleases).containsExactly(LEFT)
+ }
+
+ /** Drag crosses to the other side. Show drop target and trigger a location change. */
+ @Test
+ fun drag_rightToLeft() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = false)
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ }
+ waitForAnimateIn()
+
+ assertThat(dropTargetView).isNotNull()
+ assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+ assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft())
+ assertThat(testListener.locationChanges).containsExactly(LEFT)
+ assertThat(testListener.locationReleases).isEmpty()
+ }
+
+ /** Drag crosses to the other side. Show drop target and trigger a location change. */
+ @Test
+ fun drag_leftToRight() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = true)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ }
+ waitForAnimateIn()
+
+ assertThat(dropTargetView).isNotNull()
+ assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+ assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight())
+ assertThat(testListener.locationChanges).containsExactly(RIGHT)
+ assertThat(testListener.locationReleases).isEmpty()
+ }
+
+ /**
+ * Drop target does not initially show on the side that the drag starts. Check that it shows up
+ * after the dragging the view to other side and back to the initial side.
+ */
+ @Test
+ fun drag_rightToLeftToRight() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = false)
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNull()
+
+ getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNotNull()
+
+ getInstrumentation().runOnMainSync {
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ }
+ waitForAnimateOut()
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNotNull()
+ assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+ assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight())
+ assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder()
+ assertThat(testListener.locationReleases).isEmpty()
+ }
+
+ /**
+ * Drop target does not initially show on the side that the drag starts. Check that it shows up
+ * after the dragging the view to other side and back to the initial side.
+ */
+ @Test
+ fun drag_leftToRightToLeft() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = true)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNull()
+
+ getInstrumentation().runOnMainSync {
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNotNull()
+
+ getInstrumentation().runOnMainSync { controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y) }
+ waitForAnimateOut()
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNotNull()
+ assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+ assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft())
+ assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder()
+ assertThat(testListener.locationReleases).isEmpty()
+ }
+
+ /**
+ * Drag from right to left, but stay in exclusion rect around the dismiss view. Drop target
+ * should not show and location change should not trigger.
+ */
+ @Test
+ fun drag_rightToLeft_inExclusionRect() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = false)
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ // Exclusion rect is around the bottom center area of the screen
+ controller.onDragUpdate(SCREEN_WIDTH / 2f - 50, SCREEN_HEIGHT - 100f)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNull()
+ assertThat(testListener.locationChanges).isEmpty()
+ assertThat(testListener.locationReleases).isEmpty()
+ }
+
+ /**
+ * Drag from left to right, but stay in exclusion rect around the dismiss view. Drop target
+ * should not show and location change should not trigger.
+ */
+ @Test
+ fun drag_leftToRight_inExclusionRect() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = true)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ // Exclusion rect is around the bottom center area of the screen
+ controller.onDragUpdate(SCREEN_WIDTH / 2f + 50, SCREEN_HEIGHT - 100f)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNull()
+ assertThat(testListener.locationChanges).isEmpty()
+ assertThat(testListener.locationReleases).isEmpty()
+ }
+
+ /**
+ * Drag to dismiss target and back to the same side should not cause the drop target to show.
+ */
+ @Test
+ fun drag_rightToDismissToRight() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = false)
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ controller.onStuckToDismissTarget()
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNull()
+ assertThat(testListener.locationChanges).isEmpty()
+ assertThat(testListener.locationReleases).isEmpty()
+ }
+
+ /**
+ * Drag to dismiss target and back to the same side should not cause the drop target to show.
+ */
+ @Test
+ fun drag_leftToDismissToLeft() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = true)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ controller.onStuckToDismissTarget()
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNull()
+ assertThat(testListener.locationChanges).isEmpty()
+ assertThat(testListener.locationReleases).isEmpty()
+ }
+
+ /** Drag to dismiss target and other side should show drop target on the other side. */
+ @Test
+ fun drag_rightToDismissToLeft() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = false)
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ controller.onStuckToDismissTarget()
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNotNull()
+ assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+ assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnLeft())
+
+ assertThat(testListener.locationChanges).containsExactly(LEFT)
+ assertThat(testListener.locationReleases).isEmpty()
+ }
+
+ /** Drag to dismiss target and other side should show drop target on the other side. */
+ @Test
+ fun drag_leftToDismissToRight() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = true)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ controller.onStuckToDismissTarget()
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNotNull()
+ assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+ assertThat(dropTargetView!!.bounds()).isEqualTo(getExpectedDropTargetBoundsOnRight())
+
+ assertThat(testListener.locationChanges).containsExactly(RIGHT)
+ assertThat(testListener.locationReleases).isEmpty()
+ }
+
+ /**
+ * Drag to dismiss should trigger a location change to the initial location, if the current
+ * location is different. And hide the drop target.
+ */
+ @Test
+ fun drag_rightToLeftToDismiss() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = false)
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNotNull()
+ assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+
+ getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() }
+ waitForAnimateOut()
+ assertThat(dropTargetView!!.alpha).isEqualTo(0f)
+
+ assertThat(testListener.locationChanges).containsExactly(LEFT, RIGHT).inOrder()
+ assertThat(testListener.locationReleases).isEmpty()
+ }
+
+ /**
+ * Drag to dismiss should trigger a location change to the initial location, if the current
+ * location is different. And hide the drop target.
+ */
+ @Test
+ fun drag_leftToRightToDismiss() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = true)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNotNull()
+ assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+ getInstrumentation().runOnMainSync { controller.onStuckToDismissTarget() }
+ waitForAnimateOut()
+ assertThat(dropTargetView!!.alpha).isEqualTo(0f)
+ assertThat(testListener.locationChanges).containsExactly(RIGHT, LEFT).inOrder()
+ assertThat(testListener.locationReleases).isEmpty()
+ }
+
+ /** Finishing drag should remove drop target and send location update. */
+ @Test
+ fun drag_rightToLeftRelease() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = false)
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNotNull()
+
+ getInstrumentation().runOnMainSync { controller.onDragEnd() }
+ waitForAnimateOut()
+ assertThat(dropTargetView).isNull()
+ assertThat(testListener.locationChanges).containsExactly(LEFT)
+ assertThat(testListener.locationReleases).containsExactly(LEFT)
+ }
+
+ /** Finishing drag should remove drop target and send location update. */
+ @Test
+ fun drag_leftToRightRelease() {
+ getInstrumentation().runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = true)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ controller.onDragUpdate(pointOnRight.x, pointOnRight.y)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNotNull()
+
+ getInstrumentation().runOnMainSync { controller.onDragEnd() }
+ waitForAnimateOut()
+ assertThat(dropTargetView).isNull()
+ assertThat(testListener.locationChanges).containsExactly(RIGHT)
+ assertThat(testListener.locationReleases).containsExactly(RIGHT)
+ }
+
+ private fun getExpectedDropTargetBoundsOnLeft(): Rect =
+ Rect().also {
+ positioner.getBubbleBarExpandedViewBounds(
+ true /* onLeft */,
+ false /* isOverflowExpanded */,
+ it
+ )
+ }
+
+ private fun getExpectedDropTargetBoundsOnRight(): Rect =
+ Rect().also {
+ positioner.getBubbleBarExpandedViewBounds(
+ false /* onLeft */,
+ false /* isOverflowExpanded */,
+ it
+ )
+ }
+
+ private fun waitForAnimateIn() {
+ // Advance animator for on-device test
+ getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION)
+ }
+ }
+
+ private fun waitForAnimateOut() {
+ // Advance animator for on-device test
+ getInstrumentation().runOnMainSync {
+ animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION)
+ }
+ }
+
+ private fun View.bounds(): Rect {
+ return Rect(0, 0, layoutParams.width, layoutParams.height).also { rect ->
+ rect.offsetTo(x.toInt(), y.toInt())
+ }
+ }
+
+ internal class TestLocationChangeListener : BaseBubblePinController.LocationChangeListener {
+ val locationChanges = mutableListOf<BubbleBarLocation>()
+ val locationReleases = mutableListOf<BubbleBarLocation>()
+ override fun onChange(location: BubbleBarLocation) {
+ locationChanges.add(location)
+ }
+
+ override fun onRelease(location: BubbleBarLocation) {
+ locationReleases.add(location)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml b/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml
index 1f3e3a4c5b22..ab1ab984fd5f 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_resize_veil_background.xml
+++ b/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
+<?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.
@@ -14,7 +13,8 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<shape android:shape="rectangle"
- xmlns:android="http://schemas.android.com/apk/res/android">
- <solid android:color="@android:color/white" />
-</shape>
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:alpha="0.35" android:color="?androidprv:attr/materialColorPrimaryContainer" />
+</selector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_color_selector.xml b/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_color_selector.xml
index 65f5239737b2..640d184e641c 100644
--- a/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_color_selector.xml
+++ b/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_color_selector.xml
@@ -14,15 +14,13 @@
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
<item android:state_pressed="true"
- android:color="@color/desktop_mode_maximize_menu_button_on_hover"/>
- <item android:state_hovered="true"
- android:color="@color/desktop_mode_maximize_menu_button_on_hover"/>
+ android:color="?androidprv:attr/colorAccentPrimary"/>
<item android:state_focused="true"
- android:color="@color/desktop_mode_maximize_menu_button_on_hover"/>
+ android:color="?androidprv:attr/colorAccentPrimary"/>
<item android:state_selected="true"
- android:color="@color/desktop_mode_maximize_menu_button_on_hover"/>
- <item android:color="@color/desktop_mode_maximize_menu_button"/>
+ android:color="?androidprv:attr/colorAccentPrimary"/>
+ <item android:color="?androidprv:attr/materialColorOutlineVariant"/>
</selector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_outline_color_selector.xml b/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_outline_color_selector.xml
deleted file mode 100644
index 86679af5428b..000000000000
--- a/libs/WindowManager/Shell/res/color/desktop_mode_maximize_menu_button_outline_color_selector.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License
--->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_pressed="true"
- android:color="@color/desktop_mode_maximize_menu_button_outline_on_hover"/>
- <item android:state_hovered="true"
- android:color="@color/desktop_mode_maximize_menu_button_outline_on_hover"/>
- <item android:state_focused="true"
- android:color="@color/desktop_mode_maximize_menu_button_outline_on_hover"/>
- <item android:state_selected="true"
- android:color="@color/desktop_mode_maximize_menu_button_outline_on_hover"/>
- <item android:color="@color/desktop_mode_maximize_menu_button_outline"/>
-</selector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
new file mode 100644
index 000000000000..b928a0b20764
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml
@@ -0,0 +1,26 @@
+<?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.
+ -->
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:inset="@dimen/bubble_bar_expanded_view_drop_target_padding">
+ <shape android:shape="rectangle">
+ <corners android:radius="@dimen/bubble_bar_expanded_view_drop_target_corner" />
+ <solid android:color="@color/bubble_drop_target_background_color" />
+ <stroke
+ android:width="1dp"
+ android:color="?androidprv:attr/materialColorPrimaryContainer" />
+ </shape>
+</inset>
diff --git a/libs/WindowManager/Shell/res/drawable/circular_progress.xml b/libs/WindowManager/Shell/res/drawable/circular_progress.xml
index 948264579e1d..0d64527b6c13 100644
--- a/libs/WindowManager/Shell/res/drawable/circular_progress.xml
+++ b/libs/WindowManager/Shell/res/drawable/circular_progress.xml
@@ -24,8 +24,8 @@
android:toDegrees="275">
<shape
android:shape="ring"
- android:thickness="3dp"
- android:innerRadius="17dp"
+ android:thickness="2dp"
+ android:innerRadius="14dp"
android:useLevel="true">
</shape>
</rotate>
diff --git a/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_maximize_button_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_maximize_button_dark.xml
index 02b707568cd0..e5fe1b5431eb 100644
--- a/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_maximize_button_dark.xml
+++ b/libs/WindowManager/Shell/res/drawable/decor_desktop_mode_maximize_button_dark.xml
@@ -15,12 +15,12 @@
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="48dp"
- android:height="48dp"
+ android:width="24dp"
+ android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportHeight="960"
android:viewportWidth="960">
<path
- android:fillColor="@android:color/white"
- android:pathData="M180,840Q156,840 138,822Q120,804 120,780L120,180Q120,156 138,138Q156,120 180,120L780,120Q804,120 822,138Q840,156 840,180L840,780Q840,804 822,822Q804,840 780,840L180,840ZM180,780L780,780Q780,780 780,780Q780,780 780,780L780,277L180,277L180,780Q180,780 180,780Q180,780 180,780Z" />
+ android:fillColor="@android:color/black"
+ android:pathData="M160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720L800,720Q800,720 800,720Q800,720 800,720L800,320L160,320L160,720Q160,720 160,720Q160,720 160,720Z"/>
</vector> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml
deleted file mode 100644
index 7c4f49979455..000000000000
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_select.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="20dp"
- android:height="20dp"
- android:viewportWidth="20"
- android:viewportHeight="20">
- <path
- android:pathData="M15.701,14.583L18.567,17.5L17.425,18.733L14.525,15.833L12.442,17.917V12.5H17.917L15.701,14.583ZM15.833,5.833H17.5V7.5H15.833V5.833ZM17.5,4.167H15.833V2.567C16.75,2.567 17.5,3.333 17.5,4.167ZM12.5,2.5H14.167V4.167H12.5V2.5ZM15.833,9.167H17.5V10.833H15.833V9.167ZM7.5,17.5H5.833V15.833H7.5V17.5ZM4.167,7.5H2.5V5.833H4.167V7.5ZM4.167,2.567V4.167H2.5C2.5,3.333 3.333,2.567 4.167,2.567ZM4.167,14.167H2.5V12.5H4.167V14.167ZM7.5,4.167H5.833V2.5H7.5V4.167ZM10.833,4.167H9.167V2.5H10.833V4.167ZM10.833,17.5H9.167V15.833H10.833V17.5ZM4.167,10.833H2.5V9.167H4.167V10.833ZM4.167,17.567C3.25,17.567 2.5,16.667 2.5,15.833H4.167V17.567Z"
- android:fillColor="#1C1C14"/>
-</vector>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_background.xml
index 5d9fe67e8bee..9566f2f140c7 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_background.xml
@@ -15,7 +15,8 @@
~ limitations under the License.
-->
<shape android:shape="rectangle"
- xmlns:android="http://schemas.android.com/apk/res/android">
- <solid android:color="@android:color/white" />
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <solid android:color="?androidprv:attr/materialColorSurfaceContainerLow" />
<corners android:radius="@dimen/desktop_mode_maximize_menu_corner_radius" />
</shape>
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_maximize_button_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_button_background.xml
index bfb0dd7f3100..ed51498dfe24 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_maximize_button_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_button_background.xml
@@ -19,6 +19,5 @@
android:shape="rectangle">
<solid android:color="@color/desktop_mode_maximize_menu_button_color_selector"/>
<corners
- android:radius="@dimen/desktop_mode_maximize_menu_buttons_large_corner_radius"/>
- <stroke android:width="1dp" android:color="@color/desktop_mode_maximize_menu_button_outline_color_selector"/>
+ android:radius="@dimen/desktop_mode_maximize_menu_buttons_radius"/>
</shape> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_snap_right_button_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml
index 7bd6e9981c12..a30cfb74bf4a 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_snap_right_button_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background.xml
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
-
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
@@ -17,12 +16,10 @@
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
- <solid android:color="@color/desktop_mode_maximize_menu_button_color_selector"/>
<corners
- android:topLeftRadius="@dimen/desktop_mode_maximize_menu_buttons_small_corner_radius"
- android:topRightRadius="@dimen/desktop_mode_maximize_menu_buttons_large_corner_radius"
- android:bottomLeftRadius="@dimen/desktop_mode_maximize_menu_buttons_small_corner_radius"
- android:bottomRightRadius="@dimen/desktop_mode_maximize_menu_buttons_large_corner_radius"/>
- <stroke android:width="1dp" android:color="@color/desktop_mode_maximize_menu_button_outline_color_selector"/>
+ 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_maximize_menu_snap_left_button_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background_on_hover.xml
index 6630fcab4794..86da9feacc49 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_snap_left_button_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_maximize_menu_layout_background_on_hover.xml
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
-
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
@@ -17,12 +16,9 @@
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:shape="rectangle">
- <solid android:color="@color/desktop_mode_maximize_menu_button_color_selector"/>
<corners
- android:topLeftRadius="@dimen/desktop_mode_maximize_menu_buttons_large_corner_radius"
- android:topRightRadius="@dimen/desktop_mode_maximize_menu_buttons_small_corner_radius"
- android:bottomLeftRadius="@dimen/desktop_mode_maximize_menu_buttons_large_corner_radius"
- android:bottomRightRadius="@dimen/desktop_mode_maximize_menu_buttons_small_corner_radius"/>
- <stroke android:width="1dp" android:color="@color/desktop_mode_maximize_menu_button_outline_color_selector"/>
+ android:radius="@dimen/desktop_mode_maximize_menu_buttons_outline_radius"/>
+ <stroke android:width="1dp" android:color="?androidprv:attr/colorAccentPrimary"/>
</shape> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml
index 022594982ca3..4e673e65e053 100644
--- a/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml
+++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_transition_background.xml
@@ -14,9 +14,19 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<shape android:shape="rectangle"
- xmlns:android="http://schemas.android.com/apk/res/android">
- <solid android:color="#bf309fb5" />
- <corners android:radius="20dp" />
- <stroke android:width="1dp" color="#A00080FF"/>
-</shape>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item android:id="@+id/indicator_solid">
+ <shape android:shape="rectangle">
+ <solid android:color="?androidprv:attr/materialColorPrimaryContainer" />
+ <corners android:radius="28dp" />
+ </shape>
+ </item>
+ <item android:id="@+id/indicator_stroke">
+ <shape android:shape="rectangle">
+ <corners android:radius="28dp" />
+ <stroke android:width="1dp"
+ android:color="?androidprv:attr/materialColorPrimaryContainer"/>
+ </shape>
+ </item>
+</layer-list>
diff --git a/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget.xml b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget.xml
new file mode 100644
index 000000000000..b208f2fea7b2
--- /dev/null
+++ b/libs/WindowManager/Shell/res/drawable/ic_bubbles_shortcut_widget.xml
@@ -0,0 +1,19 @@
+<?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.
+ -->
+<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_drop_target.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_drop_target.xml
new file mode 100644
index 000000000000..9d29f7da8797
--- /dev/null
+++ b/libs/WindowManager/Shell/res/layout/bubble_bar_drop_target.xml
@@ -0,0 +1,22 @@
+<?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.
+ -->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/bubble_bar_drop_target"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="@drawable/bubble_drop_target_background"
+ android:elevation="@dimen/bubble_elevation"
+ android:importantForAccessibility="no" />
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 ef7478c04dda..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
@@ -22,14 +22,15 @@
android:layout_height="wrap_content"
android:gravity="center_horizontal">
- <ImageButton
+ <com.android.wm.shell.windowdecor.HandleImageButton
android:id="@+id/caption_handle"
android:layout_width="@dimen/desktop_mode_fullscreen_decor_caption_width"
android:layout_height="@dimen/desktop_mode_fullscreen_decor_caption_height"
android:paddingVertical="16dp"
+ android:paddingHorizontal="10dp"
android:contentDescription="@string/handle_text"
android:src="@drawable/decor_handle_dark"
tools:tint="@color/desktop_mode_caption_handle_bar_dark"
android:scaleType="fitXY"
- android:background="?android:selectableItemBackground"/>
+ android:background="@android:color/transparent"/>
</com.android.wm.shell.windowdecor.WindowDecorLinearLayout> \ No newline at end of file
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 a5605a7ff50a..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
@@ -27,13 +27,11 @@
<LinearLayout
android:id="@+id/open_menu_button"
android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:tint="?androidprv:attr/materialColorOnSurface"
- android:background="?android:selectableItemBackground"
+ android:layout_height="40dp"
android:orientation="horizontal"
android:clickable="true"
android:focusable="true"
- android:paddingStart="12dp">
+ android:layout_marginStart="12dp">
<ImageView
android:id="@+id/application_icon"
android:layout_width="@dimen/desktop_mode_caption_icon_radius"
@@ -80,11 +78,12 @@
<com.android.wm.shell.windowdecor.MaximizeButtonView
android:id="@+id/maximize_button_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_width="44dp"
+ android:layout_height="40dp"
android:layout_gravity="end"
+ android:layout_marginHorizontal="8dp"
android:clickable="true"
- android:focusable="true" />
+ android:focusable="true"/>
<ImageButton
android:id="@+id/close_window"
@@ -93,8 +92,6 @@
android:paddingHorizontal="10dp"
android:paddingVertical="8dp"
android:layout_marginEnd="8dp"
- android:tint="?androidprv:attr/materialColorOnSurface"
- android:background="?android:selectableItemBackgroundBorderless"
android:contentDescription="@string/close_button_text"
android:src="@drawable/desktop_mode_header_ic_close"
android:scaleType="centerCrop"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml
index a4bbd8998cc5..147f99144b1d 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_resize_veil.xml
@@ -16,13 +16,12 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@drawable/desktop_mode_resize_veil_background">
+ android:layout_height="match_parent">
<ImageView
android:id="@+id/veil_application_icon"
- android:layout_width="96dp"
- android:layout_height="96dp"
+ android:layout_width="@dimen/desktop_mode_resize_veil_icon_size"
+ android:layout_height="@dimen/desktop_mode_resize_veil_icon_size"
android:layout_gravity="center"
android:contentDescription="@string/app_icon_text" />
</FrameLayout> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
index c6f85a0b4ed4..d5724cc6a420 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml
@@ -52,7 +52,7 @@
android:textStyle="normal"
android:layout_weight="1"/>
- <ImageButton
+ <com.android.wm.shell.windowdecor.HandleMenuImageButton
android:id="@+id/collapse_menu_button"
android:layout_width="32dp"
android:layout_height="32dp"
@@ -134,15 +134,6 @@
android:drawableStart="@drawable/desktop_mode_ic_handle_menu_screenshot"
android:drawableTint="?androidprv:attr/materialColorOnSurface"
style="@style/DesktopModeHandleMenuActionButton"/>
-
- <Button
- android:id="@+id/select_button"
- android:contentDescription="@string/select_text"
- android:text="@string/select_text"
- android:drawableStart="@drawable/desktop_mode_ic_handle_menu_select"
- android:drawableTint="?androidprv:attr/materialColorOnSurface"
- style="@style/DesktopModeHandleMenuActionButton"/>
-
</LinearLayout>
</LinearLayout>
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 dbfd6e5d8d94..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
@@ -15,41 +15,87 @@
~ limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/maximize_menu"
style="?android:attr/buttonBarStyle"
android:layout_width="@dimen/desktop_mode_maximize_menu_width"
android:layout_height="@dimen/desktop_mode_maximize_menu_height"
android:orientation="horizontal"
android:gravity="center"
- android:background="@drawable/desktop_mode_maximize_menu_background">
+ android:padding="16dp"
+ android:background="@drawable/desktop_mode_maximize_menu_background"
+ android:elevation="1dp">
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
- <Button
- android:id="@+id/maximize_menu_maximize_button"
- style="?android:attr/buttonBarButtonStyle"
- android:layout_width="120dp"
- android:layout_height="80dp"
- android:layout_marginRight="15dp"
- android:color="@color/desktop_mode_maximize_menu_button"
- android:background="@drawable/desktop_mode_maximize_menu_maximize_button_background"
- android:stateListAnimator="@null"/>
+ <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_snap_left_button"
- style="?android:attr/buttonBarButtonStyle"
- android:layout_width="58dp"
- android:layout_height="80dp"
- android:layout_marginRight="6dp"
- android:color="@color/desktop_mode_maximize_menu_button"
- android:background="@drawable/desktop_mode_maximize_menu_snap_left_button_background"
- android:stateListAnimator="@null"/>
+ <TextView
+ android:id="@+id/maximize_menu_maximize_window_text"
+ android:layout_width="94dp"
+ android:layout_height="18dp"
+ android:textSize="11sp"
+ android:layout_marginBottom="76dp"
+ android:gravity="center"
+ android:fontFamily="google-sans-text"
+ android:text="@string/desktop_mode_maximize_menu_maximize_text"
+ android:textColor="?androidprv:attr/materialColorOnSurface"
+ android:alpha="0"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <LinearLayout
+ android:id="@+id/maximize_menu_snap_menu_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:padding="4dp"
+ android:background="@drawable/desktop_mode_maximize_menu_layout_background"
+ android:layout_marginBottom="4dp"
+ android:alpha="0">
+ <Button
+ android:id="@+id/maximize_menu_snap_left_button"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="41dp"
+ android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
+ android:layout_marginRight="4dp"
+ android:background="@drawable/desktop_mode_maximize_menu_button_background"
+ android:stateListAnimator="@null"/>
+
+ <Button
+ android:id="@+id/maximize_menu_snap_right_button"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="41dp"
+ android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
+ android:background="@drawable/desktop_mode_maximize_menu_button_background"
+ android:stateListAnimator="@null"/>
+ </LinearLayout>
+ <TextView
+ android:id="@+id/maximize_menu_snap_window_text"
+ android:layout_width="94dp"
+ android:layout_height="18dp"
+ android:textSize="11sp"
+ android:layout_marginBottom="76dp"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:fontFamily="google-sans-text"
+ android:text="@string/desktop_mode_maximize_menu_snap_text"
+ android:textColor="?androidprv:attr/materialColorOnSurface"
+ android:alpha="0"/>
+ </LinearLayout>
+</LinearLayout>
- <Button
- android:id="@+id/maximize_menu_snap_right_button"
- style="?android:attr/buttonBarButtonStyle"
- android:layout_width="58dp"
- android:layout_height="80dp"
- android:color="@color/desktop_mode_maximize_menu_button"
- android:background="@drawable/desktop_mode_maximize_menu_snap_right_button_background"
- android:stateListAnimator="@null"/>
-</LinearLayout> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
index e0057fe64fd2..cf1b8947467e 100644
--- a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
+++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
@@ -16,23 +16,29 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
- <ProgressBar
- android:id="@+id/progress_bar"
- style="?android:attr/progressBarStyleHorizontal"
- android:progressDrawable="@drawable/circular_progress"
- android:layout_width="40dp"
- android:layout_height="40dp"
- android:indeterminate="false"
- android:visibility="invisible"/>
+
+ <FrameLayout
+ android:layout_width="44dp"
+ android:layout_height="40dp">
+ <ProgressBar
+ android:id="@+id/progress_bar"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:progressDrawable="@drawable/circular_progress"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:indeterminate="false"
+ android:layout_marginHorizontal="6dp"
+ android:layout_marginVertical="4dp"
+ android:visibility="invisible"/>
+ </FrameLayout>
<ImageButton
android:id="@+id/maximize_window"
- android:layout_width="40dp"
+ android:layout_width="44dp"
android:layout_height="40dp"
- android:padding="9dp"
+ android:paddingHorizontal="10dp"
+ android:paddingVertical="8dp"
android:contentDescription="@string/maximize_button_text"
- android:tint="?androidprv:attr/materialColorOnSurface"
- android:background="?android:selectableItemBackgroundBorderless"
android:src="@drawable/decor_desktop_mode_maximize_button_dark"
android:scaleType="fitCenter" />
</merge> \ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml
index dd6f8455f82a..1c8f5e60c5c9 100644
--- a/libs/WindowManager/Shell/res/values-af/strings.xml
+++ b/libs/WindowManager/Shell/res/values-af/strings.xml
@@ -53,7 +53,7 @@
<string name="accessibility_split_top" msgid="2789329702027147146">"Verdeel bo"</string>
<string name="accessibility_split_bottom" msgid="8694551025220868191">"Verdeel onder"</string>
<string name="one_handed_tutorial_title" msgid="4583241688067426350">"Gebruik eenhandmodus"</string>
- <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Swiep van die onderkant van die skerm af op of tik enige plek bo die program om uit te gaan"</string>
+ <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Swiep van die onderkant van die skerm af op of tik enige plek bo die app om uit te gaan"</string>
<string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Begin eenhandmodus"</string>
<string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Verlaat eenhandmodus"</string>
<string name="bubbles_settings_button_description" msgid="1301286017420516912">"Instellings vir <xliff:g id="APP_NAME">%1$s</xliff:g>-borrels"</string>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Maak toe"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Maak kieslys toe"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Maak kieslys oop"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimeer skerm"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Gryp skerm vas"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml
index 18a4ccf5c16d..81ab3ab15aad 100644
--- a/libs/WindowManager/Shell/res/values-am/strings.xml
+++ b/libs/WindowManager/Shell/res/values-am/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"ዝጋ"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"ምናሌ ዝጋ"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"ምናሌን ክፈት"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"የማያ ገጹ መጠን አሳድግ"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ማያ ገጹን አሳድግ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml
index 7ca335e4a655..3974c39d4803 100644
--- a/libs/WindowManager/Shell/res/values-ar/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ar/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"إغلاق"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"إغلاق القائمة"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"فتح القائمة"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"تكبير الشاشة إلى أقصى حدّ"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"التقاط صورة للشاشة"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml
index 944c4f25bf2a..a1ce1b3b9513 100644
--- a/libs/WindowManager/Shell/res/values-as/strings.xml
+++ b/libs/WindowManager/Shell/res/values-as/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"বন্ধ কৰক"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"মেনু বন্ধ কৰক"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"মেনু খোলক"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"স্ক্ৰীন মেক্সিমাইজ কৰক"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"স্ক্ৰীন স্নেপ কৰক"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml
index c320e415d604..71dfe5ac6bed 100644
--- a/libs/WindowManager/Shell/res/values-az/strings.xml
+++ b/libs/WindowManager/Shell/res/values-az/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Bağlayın"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Menyunu bağlayın"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Menyunu açın"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ekranı maksimum böyüdün"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ekranı çəkin"</string>
</resources>
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 19ca4d300b7e..f48360991d49 100644
--- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Zatvorite"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zatvorite meni"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Otvorite meni"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Povećaj ekran"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Uklopi ekran"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml
index 74ae1d7637d0..532ecc64358f 100644
--- a/libs/WindowManager/Shell/res/values-be/strings.xml
+++ b/libs/WindowManager/Shell/res/values-be/strings.xml
@@ -56,7 +56,7 @@
<string name="one_handed_tutorial_description" msgid="3486582858591353067">"Каб выйсці, правядзіце па экране пальцам знізу ўверх або націсніце ў любым месцы над праграмай"</string>
<string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Запусціць рэжым кіравання адной рукой"</string>
<string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Выйсці з рэжыму кіравання адной рукой"</string>
- <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Налады ўсплывальных апавяшчэнняў у праграме \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
+ <string name="bubbles_settings_button_description" msgid="1301286017420516912">"Налады ўсплывальных чатаў у праграме \"<xliff:g id="APP_NAME">%1$s</xliff:g>\""</string>
<string name="bubble_overflow_button_content_description" msgid="8160974472718594382">"Дадатковае меню"</string>
<string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Зноў дадаць у стос"</string>
<string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> ад праграмы \"<xliff:g id="APP_NAME">%2$s</xliff:g>\""</string>
@@ -70,16 +70,16 @@
<string name="bubbles_app_settings" msgid="3617224938701566416">"Налады \"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>\""</string>
<string name="bubble_dismiss_text" msgid="8816558050659478158">"Адхіліць апавяшчэнне"</string>
<string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Не паказваць размову ў выглядзе ўсплывальных апавяшчэнняў"</string>
- <string name="bubbles_user_education_title" msgid="2112319053732691899">"Усплывальныя апавяшчэнні"</string>
- <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новыя размовы будуць паказвацца як рухомыя значкі ці ўсплывальныя апавяшчэнні. Націсніце, каб адкрыць усплывальнае апавяшчэнне. Перацягніце яго, каб перамясціць."</string>
- <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Кіруйце ўсплывальнымі апавяшчэннямі ў любы час"</string>
- <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Каб выключыць усплывальныя апавяшчэнні з гэтай праграмы, націсніце \"Кіраваць\""</string>
+ <string name="bubbles_user_education_title" msgid="2112319053732691899">"Усплывальныя чаты"</string>
+ <string name="bubbles_user_education_description" msgid="4215862563054175407">"Новыя размовы будуць паказвацца як рухомыя значкі ці ўсплывальныя чаты. Націсніце, каб адкрыць усплывальны чат. Перацягніце яго, каб перамясціць."</string>
+ <string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Кіруйце ўсплывальнымі чатамі"</string>
+ <string name="bubbles_user_education_manage" msgid="3460756219946517198">"Каб выключыць усплывальныя чаты з гэтай праграмы, націсніце \"Кіраваць\""</string>
<string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Зразумела"</string>
- <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Няма нядаўніх усплывальных апавяшчэнняў"</string>
- <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Нядаўнія і адхіленыя ўсплывальныя апавяшчэнні будуць паказаны тут"</string>
- <string name="bubble_bar_education_stack_title" msgid="2486903590422497245">"Чат з выкарыстаннем усплывальных апавяшчэнняў"</string>
+ <string name="bubble_overflow_empty_title" msgid="2397251267073294968">"Няма нядаўніх усплывальных чатаў"</string>
+ <string name="bubble_overflow_empty_subtitle" msgid="2627417924958633713">"Нядаўнія і адхіленыя ўсплывальныя чаты будуць паказаны тут"</string>
+ <string name="bubble_bar_education_stack_title" msgid="2486903590422497245">"Усплывальныя чаты"</string>
<string name="bubble_bar_education_stack_text" msgid="2446934610817409820">"Новыя размовы паказваюцца ў выглядзе значкоў у ніжнім вугле экрана. Націсніце на іх, каб разгарнуць. Перацягніце іх, калі хочаце закрыць."</string>
- <string name="bubble_bar_education_manage_title" msgid="6148404487810835924">"Кіруйце наладамі ўсплывальных апавяшчэнняў у любы час"</string>
+ <string name="bubble_bar_education_manage_title" msgid="6148404487810835924">"Кіруйце наладамі ўсплывальных чатаў у любы час"</string>
<string name="bubble_bar_education_manage_text" msgid="3199732148641842038">"Каб кіраваць усплывальнымі апавяшчэннямі для праграм і размоў, націсніце тут"</string>
<string name="notification_bubble_title" msgid="6082910224488253378">"Усплывальнае апавяшчэнне"</string>
<string name="manage_bubbles_text" msgid="7730624269650594419">"Кіраваць"</string>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Закрыць"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Закрыць меню"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Адкрыць меню"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Разгарнуць на ўвесь экран"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Размясціць на палавіне экрана"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml
index 1b753f5359ba..8f828badcf47 100644
--- a/libs/WindowManager/Shell/res/values-bg/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bg/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Затваряне"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Затваряне на менюто"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Отваряне на менюто"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Увеличаване на екрана"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Прилепване на екрана"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml
index 2ea22cc1455a..e0a2ea824be0 100644
--- a/libs/WindowManager/Shell/res/values-bn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bn/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"বন্ধ করুন"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"\'মেনু\' বন্ধ করুন"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"মেনু খুলুন"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"স্ক্রিন বড় করুন"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"স্ক্রিনে অ্যাপ মানানসই হিসেবে ছোট বড় করুন"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml
index 13655b3c5c85..41c72c1d3a03 100644
--- a/libs/WindowManager/Shell/res/values-bs/strings.xml
+++ b/libs/WindowManager/Shell/res/values-bs/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Zatvaranje"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zatvaranje menija"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Otvaranje menija"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimiziraj ekran"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snimi ekran"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml
index cb897c5b612d..679227248ea5 100644
--- a/libs/WindowManager/Shell/res/values-ca/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ca/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Tanca"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Tanca el menú"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Obre el menú"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximitza la pantalla"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajusta la pantalla"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml
index ded2707afd1d..150a6e642529 100644
--- a/libs/WindowManager/Shell/res/values-cs/strings.xml
+++ b/libs/WindowManager/Shell/res/values-cs/strings.xml
@@ -84,7 +84,7 @@
<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>
+ <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>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Zavřít"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zavřít nabídku"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Otevřít nabídku"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximalizovat obrazovku"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Rozpůlit obrazovku"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml
index 2bdb29d67447..8878910a4d2c 100644
--- a/libs/WindowManager/Shell/res/values-da/strings.xml
+++ b/libs/WindowManager/Shell/res/values-da/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Luk"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Luk menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Åbn menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimér skærm"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Tilpas skærm"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml
index e99d9d0c269a..7b91559e9179 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>
@@ -62,7 +62,7 @@
<string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> von <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
<string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> aus <xliff:g id="APP_NAME">%2$s</xliff:g> und <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> weiteren"</string>
<string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Nach oben links verschieben"</string>
- <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Nach rechts oben verschieben"</string>
+ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Nach oben rechts verschieben"</string>
<string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Nach unten links verschieben"</string>
<string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Nach unten rechts verschieben"</string>
<string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> maximieren"</string>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Schließen"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Menü schließen"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Menü öffnen"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Bildschirm maximieren"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Bildschirm teilen"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml
index d8bb740535fc..14e5e2f87ab8 100644
--- a/libs/WindowManager/Shell/res/values-el/strings.xml
+++ b/libs/WindowManager/Shell/res/values-el/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Κλείσιμο"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Κλείσιμο μενού"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Άνοιγμα μενού"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Μεγιστοποίηση οθόνης"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Προβολή στο μισό της οθόνης"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
index 5e1b274705dd..7427b62679be 100644
--- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Close"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
index 2525b321d9d7..cb9ee4f6b6b3 100644
--- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Close"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Close Menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Open Menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximize Screen"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap Screen"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
index 5e1b274705dd..7427b62679be 100644
--- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Close"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
index 5e1b274705dd..7427b62679be 100644
--- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Close"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Close menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Open menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
index 0623bef925e2..8498807f9fdb 100644
--- a/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
+++ b/libs/WindowManager/Shell/res/values-en-rXC/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‎‏‎‎‏‏‎‎‏‏‏‎‏‎‏‏‎‎‏‎‎‎‏‏‎‎‏‏‎‏‏‏‎‏‏‏‎‎‎‎‏‎‎‏‏‏‎‏‏‎‎‎‏‏‎‎‎‎‎Close‎‏‎‎‏‎"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‎‎‏‎‎‏‎‏‎‏‎‏‎‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‏‎‏‏‎‏‏‎‏‎‎‏‏‏‏‏‏‎‎‎‎‏‎‎‎‏‏‎‏‎Close Menu‎‏‎‎‏‎"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‏‎‏‏‎‎‏‎‏‏‏‏‎‎‏‏‏‏‎‏‎‎‏‏‏‏‏‎‎‏‎‏‎‎‎‎‏‏‎‎‏‏‎‎‎‎‎‏‏‎‎‏‏‎‎‎‎‎Open Menu‎‏‎‎‏‎"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‏‎‏‎‏‏‏‎‏‎‏‏‎‏‎‏‏‏‏‏‎‎‎‎‎‎‏‏‏‎‏‎‏‏‎‏‏‎‎‏‎‏‎‎‎‎‏‎‏‏‏‏‎‏‎‏‎‏‏‎Maximize Screen‎‏‎‎‏‎"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‏‏‎‎‏‎‏‎‏‎‎‏‎‎‏‏‏‏‏‎‏‏‎‏‏‏‎‎‏‏‏‏‎‎‎‏‎‎‎‎‏‎‏‏‏‎‎‏‏‎‏‏‏‏‎‏‏‎‏‎‎Snap Screen‎‏‎‎‏‎"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
index 9fe77ddf7e28..406c1f37c455 100644
--- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Cerrar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Abrir el menú"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar pantalla"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml
index b88f215eb54e..0583d79da127 100644
--- a/libs/WindowManager/Shell/res/values-es/strings.xml
+++ b/libs/WindowManager/Shell/res/values-es/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Cerrar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Cerrar menú"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Abrir menú"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar pantalla"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml
index 529b6d10b3c6..70547f566ea6 100644
--- a/libs/WindowManager/Shell/res/values-et/strings.xml
+++ b/libs/WindowManager/Shell/res/values-et/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Sule"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Sule menüü"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Ava menüü"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Kuva täisekraanil"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Kuva poolel ekraanil"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml
index 7438f4240eae..4be35eac6c1f 100644
--- a/libs/WindowManager/Shell/res/values-eu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-eu/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Itxi"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Itxi menua"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Ireki menua"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Handitu pantaila"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Zatitu pantaila"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml
index f7fcb2162603..32d5f5f34fb8 100644
--- a/libs/WindowManager/Shell/res/values-fa/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fa/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"بستن"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"بستن منو"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"باز کردن منو"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"بزرگ کردن صفحه"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"بزرگ کردن صفحه"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml
index 400107317637..6f03545e5542 100644
--- a/libs/WindowManager/Shell/res/values-fi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fi/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Sulje"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Sulje valikko"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Avaa valikko"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Suurenna näyttö"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Jaa näyttö"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
index b54f9cf2f15d..3492f136c4f9 100644
--- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
@@ -53,7 +53,7 @@
<string name="accessibility_split_top" msgid="2789329702027147146">"Diviser dans la partie supérieure"</string>
<string name="accessibility_split_bottom" msgid="8694551025220868191">"Diviser dans la partie inférieure"</string>
<string name="one_handed_tutorial_title" msgid="4583241688067426350">"Utiliser le mode Une main"</string>
- <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pour quitter, balayez l\'écran du bas vers le haut, ou touchez n\'importe où sur l\'écran en haut de l\'application"</string>
+ <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pour quitter, balayez l\'écran du bas vers le haut, ou touchez n\'importe où sur l\'écran en haut de l\'appli"</string>
<string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Démarrer le mode Une main"</string>
<string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Quitter le mode Une main"</string>
<string name="bubbles_settings_button_description" msgid="1301286017420516912">"Paramètres pour les bulles de l\'application <xliff:g id="APP_NAME">%1$s</xliff:g>"</string>
@@ -62,9 +62,9 @@
<string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
<string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g> et <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> autres"</string>
<string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Déplacer dans coin sup. gauche"</string>
- <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Déplacer dans coin sup. droit"</string>
- <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Déplacer dans coin inf. gauche"</string>
- <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Déplacer dans coin inf. droit"</string>
+ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Déplacer en haut à droite"</string>
+ <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Déplacer en bas à gauche"</string>
+ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Déplacer en bas à droite"</string>
<string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"développer <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"réduire <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubbles_app_settings" msgid="3617224938701566416">"Paramètres <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Fermer"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Ouvrir le menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Agrandir l\'écran"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Aligner l\'écran"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml
index 357ff91df06d..4002e4d04d51 100644
--- a/libs/WindowManager/Shell/res/values-fr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr/strings.xml
@@ -52,7 +52,7 @@
<string name="accessibility_split_right" msgid="8441001008181296837">"Affichée à droite"</string>
<string name="accessibility_split_top" msgid="2789329702027147146">"Affichée en haut"</string>
<string name="accessibility_split_bottom" msgid="8694551025220868191">"Affichée en haut"</string>
- <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Utiliser le mode une main"</string>
+ <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Utilisation du mode une main"</string>
<string name="one_handed_tutorial_description" msgid="3486582858591353067">"Pour quitter, balayez l\'écran de bas en haut ou appuyez n\'importe où au-dessus de l\'application"</string>
<string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Démarrer le mode une main"</string>
<string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Quitter le mode une main"</string>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Fermer"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Fermer le menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Ouvrir le menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Mettre en plein écran"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fractionner l\'écran"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml
index a62190754129..c371f7f62feb 100644
--- a/libs/WindowManager/Shell/res/values-gl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-gl/strings.xml
@@ -61,10 +61,10 @@
<string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Engadir de novo á pilla"</string>
<string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
<string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g> e <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g> máis"</string>
- <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover á parte super. esquerda"</string>
- <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover á parte superior dereita"</string>
- <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover á parte infer. esquerda"</string>
- <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover á parte inferior dereita"</string>
+ <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover arriba á esquerda"</string>
+ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover arriba á dereita"</string>
+ <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover abaixo á esquerda"</string>
+ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover abaixo á dereita"</string>
<string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"despregar <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"contraer <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubbles_app_settings" msgid="3617224938701566416">"Configuración de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Pechar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Pechar o menú"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Abrir menú"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Encaixar pantalla"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml
index 43c178f50d15..7e3d7a373be4 100644
--- a/libs/WindowManager/Shell/res/values-gu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-gu/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"બંધ કરો"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"મેનૂ બંધ કરો"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"મેનૂ ખોલો"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"સ્ક્રીન કરો મોટી કરો"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"સ્ક્રીન સ્નૅપ કરો"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml
index 9f6a57fa0d73..cd0f4e3618f7 100644
--- a/libs/WindowManager/Shell/res/values-hi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hi/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"बंद करें"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"मेन्यू बंद करें"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"मेन्यू खोलें"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रीन को बड़ा करें"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्नैप स्क्रीन"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml
index 4378c5642f5c..27d4cfcf22d5 100644
--- a/libs/WindowManager/Shell/res/values-hr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hr/strings.xml
@@ -52,7 +52,7 @@
<string name="accessibility_split_right" msgid="8441001008181296837">"Podijeli desno"</string>
<string name="accessibility_split_top" msgid="2789329702027147146">"Podijeli gore"</string>
<string name="accessibility_split_bottom" msgid="8694551025220868191">"Podijeli dolje"</string>
- <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Korištenje načina rada jednom rukom"</string>
+ <string name="one_handed_tutorial_title" msgid="4583241688067426350">"Upotreba načina rada jednom rukom"</string>
<string name="one_handed_tutorial_description" msgid="3486582858591353067">"Za izlaz prijeđite prstom od dna zaslona prema gore ili dodirnite bio gdje iznad aplikacije"</string>
<string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Pokretanje načina rada jednom rukom"</string>
<string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Izlaz iz načina rada jednom rukom"</string>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Zatvorite"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zatvorite izbornik"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Otvaranje izbornika"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimalno povećaj zaslon"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Izradi snimku zaslona"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml
index e5f199fea647..a8cc5c120efc 100644
--- a/libs/WindowManager/Shell/res/values-hu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hu/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Bezárás"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Menü bezárása"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Menü megnyitása"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Képernyő méretének maximalizálása"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Igazodás a képernyő adott részéhez"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml
index e0a5afe13827..7f372774241a 100644
--- a/libs/WindowManager/Shell/res/values-hy/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hy/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Փակել"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Փակել ընտրացանկը"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Բացել ընտրացանկը"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ծավալել էկրանը"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ծալել էկրանը"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml
index 802583771852..3cf55fa0ede2 100644
--- a/libs/WindowManager/Shell/res/values-in/strings.xml
+++ b/libs/WindowManager/Shell/res/values-in/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Tutup"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Tutup Menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Buka Menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Perbesar Layar"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Gabungkan Layar"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml
index cece56ec960f..6aa56f9858ad 100644
--- a/libs/WindowManager/Shell/res/values-is/strings.xml
+++ b/libs/WindowManager/Shell/res/values-is/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Loka"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Loka valmynd"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Opna valmynd"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Stækka skjá"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Smelluskjár"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml
index 731db8c12825..3c1d5e4dac02 100644
--- a/libs/WindowManager/Shell/res/values-it/strings.xml
+++ b/libs/WindowManager/Shell/res/values-it/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Chiudi"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Chiudi il menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Apri menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Massimizza schermo"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Aggancia schermo"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml
index adf55f3696c3..a0c3b3a95ca8 100644
--- a/libs/WindowManager/Shell/res/values-iw/strings.xml
+++ b/libs/WindowManager/Shell/res/values-iw/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"סגירה"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"סגירת התפריט"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"פתיחת התפריט"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"הגדלת המסך"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"כיווץ המסך"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml
index 35432229dc7b..fb726c180997 100644
--- a/libs/WindowManager/Shell/res/values-ja/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ja/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"閉じる"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"メニューを閉じる"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"メニューを開く"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"画面の最大化"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"画面のスナップ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml
index 1e6e657b5cf8..e9f620a17203 100644
--- a/libs/WindowManager/Shell/res/values-ka/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ka/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"დახურვა"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"მენიუს დახურვა"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"მენიუს გახსნა"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"აპლიკაციის გაშლა სრულ ეკრანზე"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"აპლიკაციის დაპატარავება ეკრანზე"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml
index 6d9ff26132ea..34e41038f285 100644
--- a/libs/WindowManager/Shell/res/values-kk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-kk/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Жабу"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Мәзірді жабу"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Мәзірді ашу"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Экранды ұлғайту"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Экранды бөлу"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml
index 586ef7327a70..362bbad4ec12 100644
--- a/libs/WindowManager/Shell/res/values-km/strings.xml
+++ b/libs/WindowManager/Shell/res/values-km/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"បិទ"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"បិទ​ម៉ឺនុយ"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"បើកម៉ឺនុយ"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ពង្រីកអេក្រង់"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ថតអេក្រង់"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml
index 78ca0c7979a9..77cc4a44f81a 100644
--- a/libs/WindowManager/Shell/res/values-kn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-kn/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"ಮುಚ್ಚಿ"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"ಮೆನು ಮುಚ್ಚಿ"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"ಮೆನು ತೆರೆಯಿರಿ"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ಸ್ಕ್ರೀನ್ ಅನ್ನು ಮ್ಯಾಕ್ಸಿಮೈಸ್ ಮಾಡಿ"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ಸ್ನ್ಯಾಪ್ ಸ್ಕ್ರೀನ್"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml
index 70aa3767d7dd..caa114fd6f88 100644
--- a/libs/WindowManager/Shell/res/values-ko/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ko/strings.xml
@@ -95,7 +95,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>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"닫기"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"메뉴 닫기"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"메뉴 열기"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"화면 최대화"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"화면 분할"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml
index b2a0a49b401a..302c0071a73a 100644
--- a/libs/WindowManager/Shell/res/values-ky/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ky/strings.xml
@@ -71,7 +71,7 @@
<string name="bubble_dismiss_text" msgid="8816558050659478158">"Калкып чыкма билдирмени жабуу"</string>
<string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"Жазышууда калкып чыкма билдирмелер көрүнбөсүн"</string>
<string name="bubbles_user_education_title" msgid="2112319053732691899">"Калкып чыкма билдирмелер аркылуу маектешүү"</string>
- <string name="bubbles_user_education_description" msgid="4215862563054175407">"Жаңы жазышуулар калкыма сүрөтчөлөр же калкып чыкма билдирмелер түрүндө көрүнөт. Калкып чыкма билдирмелерди ачуу үчүн таптап коюңуз. Жылдыруу үчүн сүйрөңүз."</string>
+ <string name="bubbles_user_education_description" msgid="4215862563054175407">"Жаңы жазышуулар калкыма сүрөтчөлөр же калкып чыкма билдирмелер түрүндө көрүнөт. Калкып чыкма билдирмелерди ачуу үчүн тийип коюңуз. Жылдыруу үчүн сүйрөңүз."</string>
<string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"Калкып чыкма билдирмелерди каалаган убакта көзөмөлдөңүз"</string>
<string name="bubbles_user_education_manage" msgid="3460756219946517198">"Бул колдонмодогу калкып чыкма билдирмелерди өчүрүү үчүн \"Башкарууну\" басыңыз"</string>
<string name="bubbles_user_education_got_it" msgid="3382046149225428296">"Түшүндүм"</string>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Жабуу"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Менюну жабуу"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Менюну ачуу"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Экранды чоңойтуу"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Экранды сүрөткө тартып алуу"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml
index 1cbdbd412631..a3519636b71f 100644
--- a/libs/WindowManager/Shell/res/values-lo/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lo/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"ປິດ"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"ປິດເມນູ"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"ເປີດເມນູ"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ປັບຈໍໃຫຍ່ສຸດ"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ສະແນັບໜ້າຈໍ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml
index d154c57704a1..e4dd7398f679 100644
--- a/libs/WindowManager/Shell/res/values-lt/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lt/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Uždaryti"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Uždaryti meniu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Atidaryti meniu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Išskleisti ekraną"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Sutraukti ekraną"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml
index ce269503ceef..99aebf626322 100644
--- a/libs/WindowManager/Shell/res/values-lv/strings.xml
+++ b/libs/WindowManager/Shell/res/values-lv/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Aizvērt"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Aizvērt izvēlni"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Atvērt izvēlni"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimizēt ekrānu"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fiksēt ekrānu"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml
index 9d69c50626be..c152c60fa631 100644
--- a/libs/WindowManager/Shell/res/values-mk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mk/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Затворете"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Затворете го менито"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Отвори го менито"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Максимизирај го екранот"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Подели го екранот на половина"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml
index c0e83386d572..90275cdb517a 100644
--- a/libs/WindowManager/Shell/res/values-ml/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ml/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"അടയ്ക്കുക"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"മെനു അടയ്ക്കുക"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"മെനു തുറക്കുക"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"സ്‌ക്രീൻ വലുതാക്കുക"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"സ്‌ക്രീൻ സ്‌നാപ്പ് ചെയ്യുക"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml
index ba5d283fb8a7..5e43506ab621 100644
--- a/libs/WindowManager/Shell/res/values-mn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mn/strings.xml
@@ -53,7 +53,7 @@
<string name="accessibility_split_top" msgid="2789329702027147146">"Дээд талд хуваах"</string>
<string name="accessibility_split_bottom" msgid="8694551025220868191">"Доод талд хуваах"</string>
<string name="one_handed_tutorial_title" msgid="4583241688067426350">"Нэг гарын горимыг ашиглаж байна"</string>
- <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Гарахын тулд дэлгэцийн доод хэсгээс дээш шударч эсвэл апп дээр хүссэн газраа товшино уу"</string>
+ <string name="one_handed_tutorial_description" msgid="3486582858591353067">"Гарахын тулд дэлгэцийн доод хэсгээс дээш шударч эсвэл аппын дээр хүссэн газраа товшино уу"</string>
<string name="accessibility_action_start_one_handed" msgid="5070337354072861426">"Нэг гарын горимыг эхлүүлэх"</string>
<string name="accessibility_action_stop_one_handed" msgid="1369940261782179442">"Нэг гарын горимоос гарах"</string>
<string name="bubbles_settings_button_description" msgid="1301286017420516912">"<xliff:g id="APP_NAME">%1$s</xliff:g>-н бөмбөлгүүдийн тохиргоо"</string>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Хаах"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Цэсийг хаах"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Цэс нээх"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Дэлгэцийг томруулах"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Дэлгэцийг таллах"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml
index 17601c18366a..5874bffc9199 100644
--- a/libs/WindowManager/Shell/res/values-mr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mr/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"बंद करा"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"मेनू बंद करा"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"मेनू उघडा"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रीन मोठी करा"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्क्रीन स्नॅप करा"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml
index d5547fa8a056..4de8a7b03547 100644
--- a/libs/WindowManager/Shell/res/values-ms/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ms/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Tutup"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Tutup Menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Buka Menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimumkan Skrin"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Tangkap Skrin"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml
index 07bfc990d62d..5b9e9cb7353e 100644
--- a/libs/WindowManager/Shell/res/values-my/strings.xml
+++ b/libs/WindowManager/Shell/res/values-my/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"ပိတ်ရန်"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"မီနူး ပိတ်ရန်"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"မီနူး ဖွင့်ရန်"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"စခရင်ကို ချဲ့မည်"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"စခရင်ကို ချုံ့မည်"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml
index f609d019daae..6005be4fb5c9 100644
--- a/libs/WindowManager/Shell/res/values-nb/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nb/strings.xml
@@ -85,7 +85,7 @@
<string name="manage_bubbles_text" msgid="7730624269650594419">"Administrer"</string>
<string name="accessibility_bubble_dismissed" msgid="8367471990421247357">"Boblen er avvist."</string>
<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>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Lukk"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Lukk menyen"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Åpne menyen"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimer skjermen"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fest skjermen"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml
index 9a26b7e25187..a5bd2ab5c10b 100644
--- a/libs/WindowManager/Shell/res/values-ne/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ne/strings.xml
@@ -69,7 +69,7 @@
<string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"<xliff:g id="BUBBLE_TITLE">%1$s</xliff:g> कोल्याप्स गर्नुहोस्"</string>
<string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> का सेटिङहरू"</string>
<string name="bubble_dismiss_text" msgid="8816558050659478158">"बबल खारेज गर्नुहोस्"</string>
- <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"वार्तालाप बबलको रूपमा नदेखाइयोस्"</string>
+ <string name="bubbles_dont_bubble_conversation" msgid="310000317885712693">"वार्तालाप बबलको रूपमा नदेखाउनुहोस्"</string>
<string name="bubbles_user_education_title" msgid="2112319053732691899">"बबलहरू प्रयोग गरी कुराकानी गर्नुहोस्"</string>
<string name="bubbles_user_education_description" msgid="4215862563054175407">"नयाँ वार्तालापहरू तैरने आइकन वा बबलका रूपमा देखिन्छन्। बबल खोल्न ट्याप गर्नुहोस्। बबल सार्न सो बबललाई ड्र्याग गर्नुहोस्।"</string>
<string name="bubbles_user_education_manage_title" msgid="7042699946735628035">"जुनसुकै बेला बबलहरू नियन्त्रण गर्नुहोस्"</string>
@@ -99,7 +99,7 @@
<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>
- <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"फेरि नदेखाइयोस्"</string>
+ <string name="letterbox_restart_dialog_checkbox_title" msgid="5252918008140768386">"फेरि नदेखाउनुहोस्"</string>
<string name="letterbox_reachability_reposition_text" msgid="3522042240665748268">"यो एप सार्न डबल\nट्याप गर्नुहोस्"</string>
<string name="maximize_button_text" msgid="1650859196290301963">"ठुलो बनाउनुहोस्"</string>
<string name="minimize_button_text" msgid="271592547935841753">"मिनिमाइज गर्नुहोस्"</string>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"बन्द गर्नुहोस्"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"मेनु बन्द गर्नुहोस्"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"मेनु खोल्नुहोस्"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रिन ठुलो बनाउनुहोस्"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्क्रिन स्न्याप गर्नुहोस्"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml
index a38cb7547385..0cd27c5c1457 100644
--- a/libs/WindowManager/Shell/res/values-nl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nl/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Sluiten"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Menu sluiten"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Menu openen"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Scherm maximaliseren"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Scherm halveren"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml
index e3097beb6166..bf751852a255 100644
--- a/libs/WindowManager/Shell/res/values-or/strings.xml
+++ b/libs/WindowManager/Shell/res/values-or/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"ବନ୍ଦ କରନ୍ତୁ"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"ମେନୁ ବନ୍ଦ କରନ୍ତୁ"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"ମେନୁ ଖୋଲନ୍ତୁ"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ସ୍କ୍ରିନକୁ ବଡ଼ କରନ୍ତୁ"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ସ୍କ୍ରିନକୁ ସ୍ନାପ କରନ୍ତୁ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml
index 3aea6f69749f..325c1e80c433 100644
--- a/libs/WindowManager/Shell/res/values-pa/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pa/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"ਬੰਦ ਕਰੋ"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"ਮੀਨੂ ਬੰਦ ਕਰੋ"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"ਮੀਨੂ ਖੋਲ੍ਹੋ"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ਸਕ੍ਰੀਨ ਦਾ ਆਕਾਰ ਵਧਾਓ"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ਸਕ੍ਰੀਨ ਨੂੰ ਸਨੈਪ ਕਰੋ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml
index aec3722cefa5..a7648c8e323b 100644
--- a/libs/WindowManager/Shell/res/values-pl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pl/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Zamknij"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zamknij menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Otwórz menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksymalizuj ekran"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Przyciągnij ekran"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
index ba24d7b3eb07..e47d151337b2 100644
--- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Fechar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Abrir o menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar tela"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
index f636da7997eb..1210fe8fda05 100644
--- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
@@ -62,9 +62,9 @@
<string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> de <xliff:g id="APP_NAME">%2$s</xliff:g>"</string>
<string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> do <xliff:g id="APP_NAME">%2$s</xliff:g> e mais<xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>."</string>
<string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Mover p/ parte sup. esquerda"</string>
- <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover parte superior direita"</string>
+ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Mover p/ parte sup. direita"</string>
<string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Mover p/ parte infer. esquerda"</string>
- <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover parte inferior direita"</string>
+ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Mover p/ parte inf. direita"</string>
<string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"expandir <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"reduzir <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubbles_app_settings" msgid="3617224938701566416">"Definições de <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Fechar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Abrir menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar ecrã"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Encaixar ecrã"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml
index ba24d7b3eb07..e47d151337b2 100644
--- a/libs/WindowManager/Shell/res/values-pt/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Fechar"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Fechar menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Abrir o menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar tela"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml
index c20f350ae198..ae871f3dd42b 100644
--- a/libs/WindowManager/Shell/res/values-ro/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ro/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Închide"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Închide meniul"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Deschide meniul"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizează fereastra"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Micșorează fereastra și fixeaz-o"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml
index 49347d2d8086..971e146ba77e 100644
--- a/libs/WindowManager/Shell/res/values-ru/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ru/strings.xml
@@ -61,10 +61,10 @@
<string name="bubble_accessibility_action_add_back" msgid="1830101076853540953">"Добавить обратно в стек"</string>
<string name="bubble_content_description_single" msgid="8495748092720065813">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> из приложения \"<xliff:g id="APP_NAME">%2$s</xliff:g>\""</string>
<string name="bubble_content_description_stack" msgid="8071515017164630429">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g> от приложения \"<xliff:g id="APP_NAME">%2$s</xliff:g>\" и ещё <xliff:g id="BUBBLE_COUNT">%3$d</xliff:g>"</string>
- <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Перенести в левый верхний угол"</string>
- <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Перенести в правый верхний угол"</string>
- <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Перенести в левый нижний угол"</string>
- <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Перенести в правый нижний угол"</string>
+ <string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"Переместить в левый верхний угол"</string>
+ <string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"Переместить в правый верхний угол"</string>
+ <string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"Переместить в левый нижний угол"</string>
+ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"Переместить в правый нижний угол"</string>
<string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"Развернуть <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"Свернуть <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubbles_app_settings" msgid="3617224938701566416">"<xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>: настройки"</string>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Закрыть"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Закрыть меню"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Открыть меню"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Развернуть на весь экран"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Свернуть"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml
index e5a974683e49..ef1381cbe635 100644
--- a/libs/WindowManager/Shell/res/values-si/strings.xml
+++ b/libs/WindowManager/Shell/res/values-si/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"වසන්න"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"මෙනුව වසන්න"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"මෙනුව විවෘත කරන්න"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"තිරය උපරිම කරන්න"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ස්නැප් තිරය"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml
index c2d20ddb0d3b..55a03122483b 100644
--- a/libs/WindowManager/Shell/res/values-sk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sk/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Zavrieť"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zavrieť ponuku"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Otvoriť ponuku"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximalizovať obrazovku"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Zobraziť polovicu obrazovky"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml
index cfe4480c6e1a..bb123dcdbfb6 100644
--- a/libs/WindowManager/Shell/res/values-sl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sl/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Zapri"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Zapri meni"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Odpri meni"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimiraj zaslon"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Pripni zaslon"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml
index cba98c2fb61a..c74a8cd23338 100644
--- a/libs/WindowManager/Shell/res/values-sq/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sq/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Mbyll"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Mbyll menynë"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Hap menynë"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimizo ekranin"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Regjistro ekranin"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml
index 5031f5bf5d64..0694a973dc1e 100644
--- a/libs/WindowManager/Shell/res/values-sr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sr/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Затворите"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Затворите мени"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Отворите мени"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Повећај екран"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Уклопи екран"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml
index 742be37b67ef..8e0bcfe91679 100644
--- a/libs/WindowManager/Shell/res/values-sv/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sv/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Stäng"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Stäng menyn"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Öppna menyn"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximera skärmen"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fäst skärmen"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml
index 68a7262d0eaf..41180abcf712 100644
--- a/libs/WindowManager/Shell/res/values-sw/strings.xml
+++ b/libs/WindowManager/Shell/res/values-sw/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Funga"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Funga Menyu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Fungua Menyu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Panua Dirisha kwenye Skrini"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Panga Madirisha kwenye Skrini"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml
index fe8fa057dc95..01ac78d984f3 100644
--- a/libs/WindowManager/Shell/res/values-ta/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ta/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"மூடும்"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"மெனுவை மூடும்"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"மெனுவைத் திற"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"திரையைப் பெரிதாக்கு"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"திரையை ஸ்னாப் செய்"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml
index 9be3f3354635..6224e72c19fe 100644
--- a/libs/WindowManager/Shell/res/values-te/strings.xml
+++ b/libs/WindowManager/Shell/res/values-te/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"మూసివేయండి"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"మెనూను మూసివేయండి"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"మెనూను తెరవండి"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"స్క్రీన్ సైజ్‌ను పెంచండి"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"స్క్రీన్‌ను స్నాప్ చేయండి"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml
index cd3bf6a626c0..fe0b74c469f4 100644
--- a/libs/WindowManager/Shell/res/values-th/strings.xml
+++ b/libs/WindowManager/Shell/res/values-th/strings.xml
@@ -64,7 +64,7 @@
<string name="bubble_accessibility_action_move_top_left" msgid="2644118920500782758">"ย้ายไปด้านซ้ายบน"</string>
<string name="bubble_accessibility_action_move_top_right" msgid="5864594920870245525">"ย้ายไปด้านขวาบน"</string>
<string name="bubble_accessibility_action_move_bottom_left" msgid="850271002773745634">"ย้ายไปด้านซ้ายล่าง"</string>
- <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ย้ายไปด้านขาวล่าง"</string>
+ <string name="bubble_accessibility_action_move_bottom_right" msgid="2107626346109206352">"ย้ายไปด้านขวาล่าง"</string>
<string name="bubble_accessibility_announce_expand" msgid="5388792092888203776">"ขยาย <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubble_accessibility_announce_collapse" msgid="3178806224494537097">"ยุบ <xliff:g id="BUBBLE_TITLE">%1$s</xliff:g>"</string>
<string name="bubbles_app_settings" msgid="3617224938701566416">"การตั้งค่า <xliff:g id="NOTIFICATION_TITLE">%1$s</xliff:g>"</string>
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"ปิด"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"ปิดเมนู"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"เปิดเมนู"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ขยายหน้าจอให้ใหญ่สุด"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"สแนปหน้าจอ"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml
index bf05e149ea57..786e99cfe8c8 100644
--- a/libs/WindowManager/Shell/res/values-tl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-tl/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Isara"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Isara ang Menu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Buksan ang Menu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"I-maximize ang Screen"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"I-snap ang Screen"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml
index 2dfa38a76dfa..e953f5808aff 100644
--- a/libs/WindowManager/Shell/res/values-tr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-tr/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Kapat"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Menüyü kapat"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Menüyü Aç"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ekranı Büyüt"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ekranın Yarısına Tuttur"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml
index 57ca64f5b159..fbdf42e582d1 100644
--- a/libs/WindowManager/Shell/res/values-uk/strings.xml
+++ b/libs/WindowManager/Shell/res/values-uk/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Закрити"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Закрити меню"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Відкрити меню"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Розгорнути екран"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Зафіксувати екран"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml
index 077037373aa2..5562fa70bf09 100644
--- a/libs/WindowManager/Shell/res/values-ur/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ur/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"بند کریں"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"مینیو بند کریں"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"مینو کھولیں"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"اسکرین کو بڑا کریں"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"اسکرین کا اسناپ شاٹ لیں"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml
index e2d1f47c210b..50e42329a1a0 100644
--- a/libs/WindowManager/Shell/res/values-uz/strings.xml
+++ b/libs/WindowManager/Shell/res/values-uz/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Yopish"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Menyuni yopish"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Menyuni ochish"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ekranni yoyish"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ekranni biriktirish"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml
index 4608b2b10c3f..6da85881210d 100644
--- a/libs/WindowManager/Shell/res/values-vi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-vi/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Đóng"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Đóng trình đơn"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Mở Trình đơn"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Mở rộng màn hình"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Điều chỉnh kích thước màn hình"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
index cbb857c58611..4318caf26199 100644
--- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"关闭"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"关闭菜单"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"打开菜单"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"最大化屏幕"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"屏幕快照"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
index d89b2c29216e..72cd39d8e00a 100644
--- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"關閉"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"關閉選單"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"打開選單"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"畫面最大化"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"貼齊畫面"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
index 4ce50a44e12a..c06d7b105694 100644
--- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"關閉"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"關閉選單"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"開啟選單"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"畫面最大化"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"貼齊畫面"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml
index fa680f6eee89..755414e52762 100644
--- a/libs/WindowManager/Shell/res/values-zu/strings.xml
+++ b/libs/WindowManager/Shell/res/values-zu/strings.xml
@@ -117,4 +117,6 @@
<string name="close_text" msgid="4986518933445178928">"Vala"</string>
<string name="collapse_menu_text" msgid="7515008122450342029">"Vala Imenyu"</string>
<string name="expand_menu_text" msgid="3847736164494181168">"Vula Imenyu"</string>
+ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Khulisa Isikrini Sifike Ekugcineni"</string>
+ <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Thwebula Isikrini"</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/colors.xml b/libs/WindowManager/Shell/res/values/colors.xml
index 758dbfd5f3c5..cf18da6e7463 100644
--- a/libs/WindowManager/Shell/res/values/colors.xml
+++ b/libs/WindowManager/Shell/res/values/colors.xml
@@ -62,10 +62,6 @@
<color name="desktop_mode_caption_handle_bar_dark">#1C1C17</color>
<color name="desktop_mode_resize_veil_light">#EFF1F2</color>
<color name="desktop_mode_resize_veil_dark">#1C1C17</color>
- <color name="desktop_mode_maximize_menu_button">#DDDACD</color>
- <color name="desktop_mode_maximize_menu_button_outline">#797869</color>
- <color name="desktop_mode_maximize_menu_button_outline_on_hover">#606219</color>
- <color name="desktop_mode_maximize_menu_button_on_hover">#E7E790</color>
<color name="desktop_mode_maximize_menu_progress_light">#33000000</color>
<color name="desktop_mode_maximize_menu_progress_dark">#33FFFFFF</color>
<color name="desktop_mode_caption_button_on_hover_light">#11000000</color>
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 8baaf2f155af..c2ba064ac7b6 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -45,9 +45,6 @@
<!-- Allow PIP to resize to a slightly bigger state upon touch/showing the menu -->
<bool name="config_pipEnableResizeForMenu">true</bool>
- <!-- PiP minimum size, which is a % based off the shorter side of display width and height -->
- <fraction name="config_pipShortestEdgePercent">40%</fraction>
-
<!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself
if a custom action is present before closing it. -->
<integer name="config_pipForceCloseDelay">1000</integer>
@@ -91,11 +88,45 @@
16x16
</string>
+ <!-- Default percentages for the PIP size logic.
+ 1. Determine max widths
+ Subtract width of system UI and default padding from the shortest edge of the device.
+ This is the max width.
+ 2. Calculate Default and Mins
+ Default is config_pipSystemPreferredDefaultSizePercent of max-width/height.
+ Min is config_pipSystemPreferredMinimumSizePercent of it. -->
+ <item name="config_pipSystemPreferredDefaultSizePercent" format="float" type="dimen">0.6</item>
+ <item name="config_pipSystemPreferredMinimumSizePercent" format="float" type="dimen">0.5</item>
+ <!-- Default percentages for the PIP size logic when the Display is close to square.
+ This is used instead when the display is square-ish, like fold-ables when unfolded,
+ to make sure that default PiP does not cover the hinge (halfway of the display).
+ 0. Determine if the display is square-ish
+ If min(displayWidth, displayHeight) / max(displayWidth, displayHeight) is greater than
+ config_pipSquareDisplayThresholdForSystemPreferredSize, we use the percent for
+ square display listed below.
+ 1. Determine max widths
+ Subtract width of system UI and default padding from the shortest edge of the device.
+ This is the max width.
+ 2. Calculate Default and Mins
+ Default is config_pipSystemPreferredDefaultSizePercentForSquareDisplay of max-width/height.
+ Min is config_pipSystemPreferredMinimumSizePercentForSquareDisplay of it. -->
+ <item name="config_pipSquareDisplayThresholdForSystemPreferredSize"
+ format="float" type="dimen">0.95</item>
+ <item name="config_pipSystemPreferredDefaultSizePercentForSquareDisplay"
+ format="float" type="dimen">0.5</item>
+ <item name="config_pipSystemPreferredMinimumSizePercentForSquareDisplay"
+ format="float" type="dimen">0.4</item>
+
<!-- The percentage of the screen width to use for the default width or height of
picture-in-picture windows. Regardless of the percent set here, calculated size will never
- be smaller than @dimen/default_minimal_size_pip_resizable_task. -->
+ be smaller than @dimen/default_minimal_size_pip_resizable_task.
+ This is used in legacy spec, use config_pipSystemPreferredDefaultSizePercent instead. -->
<item name="config_pictureInPictureDefaultSizePercent" format="float" type="dimen">0.23</item>
+ <!-- PiP minimum size, which is a % based off the shorter side of display width and height.
+ This is used in legacy spec, use config_pipSystemPreferredMinimumSizePercent instead. -->
+ <fraction name="config_pipShortestEdgePercent">40%</fraction>
+
<!-- The default aspect ratio for picture-in-picture windows. -->
<item name="config_pictureInPictureDefaultAspectRatio" format="float" type="dimen">
1.777778
@@ -145,4 +176,7 @@
<!-- Whether CompatUIController is enabled -->
<bool name="config_enableCompatUIController">true</bool>
+
+ <!-- Whether pointer pilfer is required to start back animation. -->
+ <bool name="config_backAnimationRequiresPointerPilfer">true</bool>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 74967ef0d97c..595d34664cfa 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -213,7 +213,7 @@
<dimen name="bubble_swap_animation_offset">15dp</dimen>
<!-- How far offscreen the bubble stack rests. There's some padding around the bubble so
add 3dp to the desired overhang. -->
- <dimen name="bubble_stack_offscreen">3dp</dimen>
+ <dimen name="bubble_stack_offscreen">2.5dp</dimen>
<!-- How far down the screen the stack starts. -->
<dimen name="bubble_stack_starting_offset_y">120dp</dimen>
<!-- Space between the pointer triangle and the bubble expanded view -->
@@ -247,13 +247,13 @@
<!-- Padding for the bubble popup view contents. -->
<dimen name="bubble_popup_padding">24dp</dimen>
<!-- The size of the caption bar inset at the top of bubble bar expanded view. -->
- <dimen name="bubble_bar_expanded_view_caption_height">32dp</dimen>
+ <dimen name="bubble_bar_expanded_view_caption_height">36dp</dimen>
<!-- The width of the caption bar at the top of bubble bar expanded view. -->
- <dimen name="bubble_bar_expanded_view_caption_width">128dp</dimen>
- <!-- The height of the dots shown for the caption menu in the bubble bar expanded view.. -->
- <dimen name="bubble_bar_expanded_view_caption_dot_size">4dp</dimen>
- <!-- The spacing between the dots for the caption menu in the bubble bar expanded view.. -->
- <dimen name="bubble_bar_expanded_view_caption_dot_spacing">4dp</dimen>
+ <dimen name="bubble_bar_expanded_view_caption_width">80dp</dimen>
+ <!-- The height of the handle shown for the caption menu in the bubble bar expanded view. -->
+ <dimen name="bubble_bar_expanded_view_handle_height">4dp</dimen>
+ <!-- Width of the expanded bubble bar view shown when the bubble is expanded. -->
+ <dimen name="bubble_bar_expanded_view_width">412dp</dimen>
<!-- Minimum width of the bubble bar manage menu. -->
<dimen name="bubble_bar_manage_menu_min_width">200dp</dimen>
<!-- Size of the dismiss icon in the bubble bar manage menu. -->
@@ -272,6 +272,13 @@
<dimen name="bubble_bar_expanded_view_corner_radius">16dp</dimen>
<!-- Corner radius for expanded view while it is being dragged -->
<dimen name="bubble_bar_expanded_view_corner_radius_dragged">28dp</dimen>
+ <!-- Corner radius for expanded view drop target -->
+ <dimen name="bubble_bar_expanded_view_drop_target_corner">28dp</dimen>
+ <dimen name="bubble_bar_expanded_view_drop_target_padding">24dp</dimen>
+ <!-- Width of the box around bottom center of the screen where drag only leads to dismiss -->
+ <dimen name="bubble_bar_dismiss_zone_width">192dp</dimen>
+ <!-- Height of the box around bottom center of the screen where drag only leads to dismiss -->
+ <dimen name="bubble_bar_dismiss_zone_height">242dp</dimen>
<!-- Bottom and end margin for compat buttons. -->
<dimen name="compat_button_margin">24dp</dimen>
@@ -414,13 +421,14 @@
<dimen name="freeform_decor_caption_height">42dp</dimen>
<!-- Height of desktop mode caption for freeform tasks. -->
- <dimen name="desktop_mode_freeform_decor_caption_height">42dp</dimen>
+ <dimen name="desktop_mode_freeform_decor_caption_height">40dp</dimen>
<!-- Height of desktop mode caption for fullscreen tasks. -->
<dimen name="desktop_mode_fullscreen_decor_caption_height">36dp</dimen>
- <!-- Width of desktop mode caption for fullscreen tasks. -->
- <dimen name="desktop_mode_fullscreen_decor_caption_width">128dp</dimen>
+ <!-- Width of desktop mode caption for fullscreen tasks.
+ 80 dp for handle + 20 dp for room to grow on the sides when hovered. -->
+ <dimen name="desktop_mode_fullscreen_decor_caption_width">100dp</dimen>
<!-- Required empty space to be visible for partially offscreen tasks. -->
<dimen name="freeform_required_visible_empty_space_in_header">48dp</dimen>
@@ -451,17 +459,35 @@
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">287dp</dimen>
+ <dimen name="desktop_mode_maximize_menu_width">228dp</dimen>
<!-- The height of the maximize menu in desktop mode. -->
- <dimen name="desktop_mode_maximize_menu_height">112dp</dimen>
+ <dimen name="desktop_mode_maximize_menu_height">114dp</dimen>
+
+ <!-- The padding of the maximize menu in desktop mode. -->
+ <dimen name="desktop_mode_menu_padding">16dp</dimen>
- <!-- The larger of the two corner radii of the maximize menu buttons. -->
- <dimen name="desktop_mode_maximize_menu_buttons_large_corner_radius">4dp</dimen>
+ <!-- The height of the buttons in the maximize menu. -->
+ <dimen name="desktop_mode_maximize_menu_button_height">52dp</dimen>
- <!-- The smaller of the two corner radii of the maximize menu buttons. -->
- <dimen name="desktop_mode_maximize_menu_buttons_small_corner_radius">2dp</dimen>
+ <!-- The radius of the maximize menu buttons. -->
+ <dimen name="desktop_mode_maximize_menu_buttons_radius">4dp</dimen>
+
+ <!-- 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>
@@ -502,18 +528,29 @@
<!-- The radius of the caption menu shadow. -->
<dimen name="desktop_mode_handle_menu_shadow_radius">2dp</dimen>
+ <!-- The size of the icon shown in the resize veil. -->
+ <dimen name="desktop_mode_resize_veil_icon_size">96dp</dimen>
+
+ <!-- The with of the border around the app task for edge resizing, when
+ enable_windowing_edge_drag_resize is enabled. -->
+ <dimen name="desktop_mode_edge_handle">12dp</dimen>
+
+ <!-- The original width of the border around the app task for edge resizing, when
+ enable_windowing_edge_drag_resize is disabled. -->
<dimen name="freeform_resize_handle">15dp</dimen>
+ <!-- The size of the corner region for drag resizing with touch, when a larger touch region is
+ appropriate. Applied when enable_windowing_edge_drag_resize is enabled. -->
+ <dimen name="desktop_mode_corner_resize_large">48dp</dimen>
+
+ <!-- The original size of the corner region for darg resizing, when
+ enable_windowing_edge_drag_resize is disabled. -->
<dimen name="freeform_resize_corner">44dp</dimen>
<!-- The width of the area at the sides of the screen where a freeform task will transition to
split select if dragged until the touch input is within the range. -->
<dimen name="desktop_mode_transition_area_width">32dp</dimen>
- <!-- The height of the area at the top of the screen where a freeform task will transition to
- fullscreen if dragged until the top bound of the task is within the area. -->
- <dimen name="desktop_mode_transition_area_height">16dp</dimen>
-
<!-- The width of the area where a desktop task will transition to fullscreen. -->
<dimen name="desktop_mode_fullscreen_from_desktop_width">80dp</dimen>
@@ -525,6 +562,28 @@
for corner drags without transition. -->
<dimen name="desktop_mode_split_from_desktop_height">100dp</dimen>
+ <!-- The corner radius of a task that was dragged from fullscreen. -->
+ <dimen name="desktop_mode_dragged_task_radius">28dp</dimen>
+
+ <!-- The corner radius of the app chip, maximize and close button's ripple drawable -->
+ <dimen name="desktop_mode_header_buttons_ripple_radius">16dp</dimen>
+ <!-- The vertical inset to apply to the app chip's ripple drawable -->
+ <dimen name="desktop_mode_header_app_chip_ripple_inset_vertical">4dp</dimen>
+
+ <!-- The corner radius of the maximize button's ripple drawable -->
+ <dimen name="desktop_mode_header_maximize_ripple_radius">18dp</dimen>
+ <!-- The vertical inset to apply to the maximize button's ripple drawable -->
+ <dimen name="desktop_mode_header_maximize_ripple_inset_vertical">4dp</dimen>
+ <!-- The horizontal inset to apply to the maximize button's ripple drawable -->
+ <dimen name="desktop_mode_header_maximize_ripple_inset_horizontal">6dp</dimen>
+
+ <!-- The corner radius of the close button's ripple drawable -->
+ <dimen name="desktop_mode_header_close_ripple_radius">18dp</dimen>
+ <!-- The vertical inset to apply to the close button's ripple drawable -->
+ <dimen name="desktop_mode_header_close_ripple_inset_vertical">4dp</dimen>
+ <!-- The horizontal inset to apply to the close button's ripple drawable -->
+ <dimen name="desktop_mode_header_close_ripple_inset_horizontal">6dp</dimen>
+
<!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (72 x 72) / (108 x 108) -->
<item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item>
<!-- Scaling factor applied to splash icons without provided background i.e. (192 / 160) -->
@@ -535,5 +594,7 @@
<!-- The vertical margin that needs to be preserved between the scaled window bounds and the
original window bounds (once the surface is scaled enough to do so) -->
<dimen name="cross_task_back_vertical_margin">8dp</dimen>
+ <!-- The offset from the left edge of the entering page for the cross-activity animation -->
+ <dimen name="cross_activity_back_entering_start_offset">96dp</dimen>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 812a81ba33d1..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>
@@ -280,4 +286,8 @@
<string name="collapse_menu_text">Close Menu</string>
<!-- Accessibility text for the handle menu open menu button [CHAR LIMIT=NONE] -->
<string name="expand_menu_text">Open Menu</string>
+ <!-- Maximize menu maximize button string. -->
+ <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string>
+ <!-- Maximize menu snap buttons string. -->
+ <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml
index 08c2a02acf55..13c0e6646002 100644
--- a/libs/WindowManager/Shell/res/values/styles.xml
+++ b/libs/WindowManager/Shell/res/values/styles.xml
@@ -23,6 +23,14 @@
<item name="android:windowAnimationStyle">@style/Animation.ForcedResizable</item>
</style>
+ <!-- Theme used for the activity that shows below the desktop mode windows to show wallpaper -->
+ <style name="DesktopWallpaperTheme" parent="@android:style/Theme.Wallpaper.NoTitleBar">
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ </style>
+
<style name="Animation.ForcedResizable" parent="@android:style/Animation">
<item name="android:activityOpenEnterAnimation">@anim/forced_resizable_enter</item>
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
new file mode 100644
index 000000000000..4876f327a650
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.shared;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.SystemProperties;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
+
+/**
+ * Constants for desktop mode feature
+ */
+public class DesktopModeStatus {
+
+ /**
+ * Flag to indicate whether task resizing is veiled.
+ */
+ private static final boolean IS_VEILED_RESIZE_ENABLED = SystemProperties.getBoolean(
+ "persist.wm.debug.desktop_veiled_resizing", true);
+
+ /**
+ * Flag to indicate is moving task to another display is enabled.
+ */
+ public static final boolean IS_DISPLAY_CHANGE_ENABLED = SystemProperties.getBoolean(
+ "persist.wm.debug.desktop_change_display", false);
+
+ /**
+ * Flag to indicate whether to apply shadows to windows in desktop mode.
+ */
+ private static final boolean USE_WINDOW_SHADOWS = SystemProperties.getBoolean(
+ "persist.wm.debug.desktop_use_window_shadows", true);
+
+ /**
+ * Flag to indicate whether to apply shadows to the focused window in desktop mode.
+ *
+ * Note: this flag is only relevant if USE_WINDOW_SHADOWS is false.
+ */
+ private static final boolean USE_WINDOW_SHADOWS_FOCUSED_WINDOW = SystemProperties.getBoolean(
+ "persist.wm.debug.desktop_use_window_shadows_focused_window", false);
+
+ /**
+ * Flag to indicate whether to use rounded corners for windows in desktop mode.
+ */
+ private static final boolean USE_ROUNDED_CORNERS = SystemProperties.getBoolean(
+ "persist.wm.debug.desktop_use_rounded_corners", true);
+
+ /**
+ * Flag to indicate whether to restrict desktop mode to supported devices.
+ */
+ 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);
+
+ /** The minimum override density allowed for tasks inside the desktop. */
+ private static final int DESKTOP_DENSITY_MIN = 100;
+
+ /** The maximum override density allowed for tasks inside the desktop. */
+ private static final int DESKTOP_DENSITY_MAX = 1000;
+
+ /**
+ * Default value for {@code MAX_TASK_LIMIT}.
+ */
+ @VisibleForTesting
+ public static final int DEFAULT_MAX_TASK_LIMIT = 4;
+
+ // TODO(b/335131008): add a config-overlay field for the max number of tasks in Desktop Mode
+ /**
+ * Flag declaring the maximum number of Tasks to show in Desktop Mode at any one time.
+ *
+ * <p> The limit does NOT affect Picture-in-Picture, Bubbles, or System Modals (like a screen
+ * recording window, or Bluetooth pairing window).
+ */
+ private static final int MAX_TASK_LIMIT = SystemProperties.getInt(
+ "persist.wm.debug.desktop_max_task_limit", DEFAULT_MAX_TASK_LIMIT);
+
+ /**
+ * Return {@code true} if desktop windowing is enabled. Only to be used for testing. Callers
+ * should use {@link #canEnterDesktopMode(Context)} to query the state of desktop windowing.
+ */
+ @VisibleForTesting
+ public static boolean isEnabled() {
+ return Flags.enableDesktopWindowingMode();
+ }
+
+ /**
+ * Return {@code true} if veiled resizing is active. If false, fluid resizing is used.
+ */
+ public static boolean isVeiledResizeEnabled() {
+ return IS_VEILED_RESIZE_ENABLED;
+ }
+
+ /**
+ * Return whether to use window shadows.
+ *
+ * @param isFocusedWindow whether the window to apply shadows to is focused
+ */
+ public static boolean useWindowShadow(boolean isFocusedWindow) {
+ return USE_WINDOW_SHADOWS
+ || (USE_WINDOW_SHADOWS_FOCUSED_WINDOW && isFocusedWindow);
+ }
+
+ /**
+ * Return whether to use rounded corners for windows.
+ */
+ public static boolean useRoundedCorners() {
+ return USE_ROUNDED_CORNERS;
+ }
+
+ /**
+ * Return {@code true} if desktop mode should be restricted to supported devices.
+ */
+ @VisibleForTesting
+ public static boolean enforceDeviceRestrictions() {
+ return ENFORCE_DEVICE_RESTRICTIONS;
+ }
+
+ /**
+ * Return the maximum limit on the number of Tasks to show in Desktop Mode at any one time.
+ */
+ public static int getMaxTaskLimit() {
+ return MAX_TASK_LIMIT;
+ }
+
+ /**
+ * Return {@code true} if the current device supports desktop mode.
+ */
+ @VisibleForTesting
+ public static boolean isDesktopModeSupported(@NonNull Context context) {
+ return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported);
+ }
+
+ /**
+ * Return {@code true} if desktop mode is enabled and can be entered on the current device.
+ */
+ public static boolean canEnterDesktopMode(@NonNull Context context) {
+ return (!enforceDeviceRestrictions() || isDesktopModeSupported(context)) && isEnabled();
+ }
+
+ /**
+ * 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.
+ */
+ 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/transition/IHomeTransitionListener.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IHomeTransitionListener.aidl
index 72fba3bb7de4..8481c446c6aa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IHomeTransitionListener.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.transition;
+package com.android.wm.shell.shared;
import android.window.RemoteTransition;
import android.window.TransitionFilter;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
index 7f4a8f1d476a..3256abf09116 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-package com.android.wm.shell.transition;
+package com.android.wm.shell.shared;
import android.view.SurfaceControl;
import android.window.RemoteTransition;
import android.window.TransitionFilter;
-import com.android.wm.shell.transition.IHomeTransitionListener;
+import com.android.wm.shell.shared.IHomeTransitionListener;
/**
* Interface that is exposed to remote callers to manipulate the transitions feature.
@@ -28,13 +28,14 @@ import com.android.wm.shell.transition.IHomeTransitionListener;
interface IShellTransitions {
/**
- * Registers a remote transition handler.
+ * Registers a remote transition handler for all operations excluding takeovers (see
+ * registerRemoteForTakeover()).
*/
oneway void registerRemote(in TransitionFilter filter,
in RemoteTransition remoteTransition) = 1;
/**
- * Unregisters a remote transition handler.
+ * Unregisters a remote transition handler for all operations.
*/
oneway void unregisterRemote(in RemoteTransition remoteTransition) = 2;
@@ -52,4 +53,10 @@ interface IShellTransitions {
* Returns a container surface for the home root task.
*/
SurfaceControl getHomeTaskOverlayContainer() = 5;
+
+ /**
+ * Registers a remote transition for takeover operations only.
+ */
+ oneway void registerRemoteForTakeover(in TransitionFilter filter,
+ in RemoteTransition remoteTransition) = 6;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
index da39017a0313..6d4ab4c1bd09 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-package com.android.wm.shell.transition;
+package com.android.wm.shell.shared;
import android.annotation.NonNull;
import android.window.RemoteTransition;
import android.window.TransitionFilter;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
/**
* Interface to manage remote transitions.
@@ -28,13 +28,20 @@ import com.android.wm.shell.common.annotations.ExternalThread;
@ExternalThread
public interface ShellTransitions {
/**
- * Registers a remote transition.
+ * Registers a remote transition for all operations excluding takeovers (see
+ * {@link ShellTransitions#registerRemoteForTakeover(TransitionFilter, RemoteTransition)}).
*/
default void registerRemote(@NonNull TransitionFilter filter,
@NonNull RemoteTransition remoteTransition) {}
/**
- * Unregisters a remote transition.
+ * Registers a remote transition for takeover operations only.
+ */
+ default void registerRemoteForTakeover(@NonNull TransitionFilter filter,
+ @NonNull RemoteTransition remoteTransition) {}
+
+ /**
+ * Unregisters a remote transition for all operations.
*/
default void unregisterRemote(@NonNull RemoteTransition remoteTransition) {}
}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
index dcd4062cb819..dc022b4afd3b 100644
--- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java
@@ -69,8 +69,17 @@ public class TransitionUtil {
/** Returns {@code true} if the transition is opening or closing mode. */
public static boolean isOpenOrCloseMode(@TransitionInfo.TransitionMode int mode) {
- return mode == TRANSIT_OPEN || mode == TRANSIT_CLOSE
- || mode == TRANSIT_TO_FRONT || mode == TRANSIT_TO_BACK;
+ return isOpeningMode(mode) || isClosingMode(mode);
+ }
+
+ /** Returns {@code true} if the transition is opening mode. */
+ public static boolean isOpeningMode(@TransitionInfo.TransitionMode int mode) {
+ return mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT;
+ }
+
+ /** Returns {@code true} if the transition is closing mode. */
+ public static boolean isClosingMode(@TransitionInfo.TransitionMode int mode) {
+ return mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK;
}
/** Returns {@code true} if the transition has a display change. */
@@ -318,7 +327,7 @@ public class TransitionUtil {
null,
new Rect(change.getStartAbsBounds()),
taskInfo,
- change.getAllowEnterPip(),
+ change.isAllowEnterPip(),
INVALID_WINDOW_TYPE
);
target.setWillShowImeOnTarget(
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimator.kt
index ee8c41417458..9d3b56d22a2f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimator.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.animation
+package com.android.wm.shell.shared.animation
import android.util.ArrayMap
import android.util.Log
@@ -25,7 +25,7 @@ import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringAnimation
import androidx.dynamicanimation.animation.SpringForce
-import com.android.wm.shell.animation.PhysicsAnimator.Companion.getInstance
+import com.android.wm.shell.shared.animation.PhysicsAnimator.Companion.getInstance
import java.lang.ref.WeakReference
import java.util.WeakHashMap
import kotlin.math.abs
@@ -505,7 +505,6 @@ class PhysicsAnimator<T> private constructor (target: T) {
// Check for a spring configuration. If one is present, we're either springing, or
// flinging-then-springing.
if (springConfig != null) {
-
// If there is no corresponding fling config, we're only springing.
if (flingConfig == null) {
// Apply the configuration and start the animation.
@@ -679,7 +678,6 @@ class PhysicsAnimator<T> private constructor (target: T) {
value: Float,
velocity: Float
) {
-
// If this property animation isn't relevant to this listener, ignore it.
if (!properties.contains(property)) {
return
@@ -702,7 +700,6 @@ class PhysicsAnimator<T> private constructor (target: T) {
finalVelocity: Float,
isFling: Boolean
): Boolean {
-
// If this property animation isn't relevant to this listener, ignore it.
if (!properties.contains(property)) {
return false
@@ -877,7 +874,7 @@ class PhysicsAnimator<T> private constructor (target: T) {
*
* @param <T> The type of the object being animated.
</T> */
- interface UpdateListener<T> {
+ fun interface UpdateListener<T> {
/**
* Called on each animation frame with the target object, and a map of FloatPropertyCompat
@@ -907,7 +904,7 @@ class PhysicsAnimator<T> private constructor (target: T) {
*
* @param <T> The type of the object being animated.
</T> */
- interface EndListener<T> {
+ fun interface EndListener<T> {
/**
* Called with the final animation values as each property animation ends. This can be used
@@ -971,17 +968,18 @@ class PhysicsAnimator<T> private constructor (target: T) {
companion object {
/**
- * Constructor to use to for new physics animator instances in [getInstance]. This is
- * typically the default constructor, but [PhysicsAnimatorTestUtils] can change it so that
- * all code using the physics animator is given testable instances instead.
+ * Callback to notify that a new animator was created. Used in [PhysicsAnimatorTestUtils]
+ * to be able to keep track of animators and wait for them to finish.
*/
- internal var instanceConstructor: (Any) -> PhysicsAnimator<*> = ::PhysicsAnimator
+ internal var onAnimatorCreated: (PhysicsAnimator<*>, Any) -> Unit = { _, _ -> }
@JvmStatic
@Suppress("UNCHECKED_CAST")
fun <T : Any> getInstance(target: T): PhysicsAnimator<T> {
if (!animators.containsKey(target)) {
- animators[target] = instanceConstructor(target)
+ val animator = PhysicsAnimator(target)
+ onAnimatorCreated(animator, target)
+ animators[target] = animator
}
return animators[target] as PhysicsAnimator<T>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt
index 86eb8da952f1..235b9bf7b9fd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt
@@ -13,13 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.wm.shell.animation
+package com.android.wm.shell.shared.animation
import android.os.Handler
import android.os.Looper
import android.util.ArrayMap
import androidx.dynamicanimation.animation.FloatPropertyCompat
-import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.prepareForTest
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.prepareForTest
import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@@ -62,12 +62,9 @@ object PhysicsAnimatorTestUtils {
*/
@JvmStatic
fun prepareForTest() {
- val defaultConstructor = PhysicsAnimator.instanceConstructor
- PhysicsAnimator.instanceConstructor = fun(target: Any): PhysicsAnimator<*> {
- val animator = defaultConstructor(target)
+ PhysicsAnimator.onAnimatorCreated = { animator, target ->
allAnimatedObjects.add(target)
animatorTestHelpers[animator] = AnimatorTestHelper(animator)
- return animator
}
timeoutMs = 2000
@@ -158,12 +155,12 @@ object PhysicsAnimatorTestUtils {
@Throws(InterruptedException::class)
@Suppress("UNCHECKED_CAST")
fun <T : Any> blockUntilAnimationsEnd(
- properties: FloatPropertyCompat<in T>
+ vararg properties: FloatPropertyCompat<in T>
) {
for (target in allAnimatedObjects) {
try {
blockUntilAnimationsEnd(
- PhysicsAnimator.getInstance(target) as PhysicsAnimator<T>, properties)
+ PhysicsAnimator.getInstance(target) as PhysicsAnimator<T>, *properties)
} catch (e: ClassCastException) {
// Keep checking the other objects for ones whose types match the provided
// properties.
@@ -267,10 +264,8 @@ object PhysicsAnimatorTestUtils {
// Loop through the updates from the testable animator.
for (update in framesForProperty) {
-
// Check whether this frame satisfies the current matcher.
if (curMatcher(update)) {
-
// If that was the last unsatisfied matcher, we're good here. 'Verify' all remaining
// frames and return without failing.
if (matchers.size == 0) {
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ChoreographerSfVsync.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ChoreographerSfVsync.java
new file mode 100644
index 000000000000..a1496ac1d33b
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ChoreographerSfVsync.java
@@ -0,0 +1,34 @@
+/*
+ * 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.shared.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/**
+ * Annotates a method that or qualifies a provider runs aligned to the Choreographer SF vsync
+ * instead of the app vsync.
+ */
+@Documented
+@Inherited
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ChoreographerSfVsync {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalMainThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalMainThread.java
index 9ac7a12bc509..52a717b3a60c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalMainThread.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalMainThread.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.common.annotations;
+package com.android.wm.shell.shared.annotations;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalThread.java
new file mode 100644
index 000000000000..ae5188cf8093
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalThread.java
@@ -0,0 +1,31 @@
+/*
+ * 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.shared.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/** Annotates a method or class that is called from an external thread to the Shell threads. */
+@Documented
+@Inherited
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ExternalThread {}
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellAnimationThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellAnimationThread.java
new file mode 100644
index 000000000000..bd2887e39ef1
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellAnimationThread.java
@@ -0,0 +1,31 @@
+/*
+ * 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.shared.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/** Annotates a method or qualifies a provider that runs on the Shell animation-thread */
+@Documented
+@Inherited
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ShellAnimationThread {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellBackgroundThread.java
index 4cd3c903f2f8..586ac8297e26 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellBackgroundThread.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.common.annotations;
+package com.android.wm.shell.shared.annotations;
import java.lang.annotation.Documented;
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellMainThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellMainThread.java
new file mode 100644
index 000000000000..6c879a491fe0
--- /dev/null
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellMainThread.java
@@ -0,0 +1,31 @@
+/*
+ * 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.shared.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/** Annotates a method or qualifies a provider that runs on the Shell main-thread */
+@Documented
+@Inherited
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ShellMainThread {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellSplashscreenThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellSplashscreenThread.java
index c2fd54fd96d7..4887dbe81b25 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellSplashscreenThread.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellSplashscreenThread.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.common.annotations;
+package com.android.wm.shell.shared.annotations;
import java.lang.annotation.Documented;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index d8d0d876b4f2..3ded7d246499 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -16,6 +16,7 @@
package com.android.wm.shell;
+
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
@@ -30,7 +31,7 @@ import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager.RunningTaskInfo;
-import android.app.AppCompatTaskInfo;
+import android.app.CameraCompatTaskInfo.CameraCompatControlState;
import android.app.TaskInfo;
import android.app.WindowConfiguration;
import android.content.LocusId;
@@ -174,6 +175,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
.setName("home_task_overlay_container")
.setContainerLayer()
.setHidden(false)
+ .setCallsite("ShellTaskOrganizer.mHomeTaskOverlayContainer")
.build();
/**
@@ -551,10 +553,12 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
// Notify the compat UI if the listener or task info changed.
notifyCompatUI(taskInfo, newListener);
}
- if (data.getTaskInfo().getWindowingMode() != taskInfo.getWindowingMode()) {
- // Notify the recent tasks when a task changes windowing modes
+ final boolean windowModeChanged =
+ data.getTaskInfo().getWindowingMode() != taskInfo.getWindowingMode();
+ final boolean visibilityChanged = data.getTaskInfo().isVisible != taskInfo.isVisible;
+ if (windowModeChanged || visibilityChanged) {
mRecentTasks.ifPresent(recentTasks ->
- recentTasks.onTaskWindowingModeChanged(taskInfo));
+ recentTasks.onTaskRunningInfoChanged(taskInfo));
}
// TODO (b/207687679): Remove check for HOME once bug is fixed
final boolean isFocusedOrHome = taskInfo.isFocused
@@ -718,8 +722,7 @@ public class ShellTaskOrganizer extends TaskOrganizer implements
}
@Override
- public void onCameraControlStateUpdated(
- int taskId, @AppCompatTaskInfo.CameraCompatControlState int state) {
+ public void onCameraControlStateUpdated(int taskId, @CameraCompatControlState int state) {
final TaskAppearedInfo info;
synchronized (mLock) {
info = mTasks.get(taskId);
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 539832e3cf3c..a426b206b0cd 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
@@ -26,6 +26,7 @@ import static com.android.wm.shell.activityembedding.ActivityEmbeddingAnimationS
import static com.android.wm.shell.transition.TransitionAnimationHelper.addBackgroundToTransition;
import static com.android.wm.shell.transition.TransitionAnimationHelper.edgeExtendWindow;
import static com.android.wm.shell.transition.TransitionAnimationHelper.getTransitionBackgroundColorIfSet;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE;
import android.animation.Animator;
import android.animation.ValueAnimator;
@@ -190,6 +191,10 @@ class ActivityEmbeddingAnimationRunner {
@NonNull
private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters(
@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
+ if (info.getType() == TRANSIT_TASK_FRAGMENT_DRAG_RESIZE) {
+ // Jump cut for AE drag resizing because the content is veiled.
+ return new ArrayList<>();
+ }
boolean isChangeTransition = false;
for (TransitionInfo.Change change : info.getChanges()) {
if (change.hasFlags(FLAG_IS_BEHIND_STARTING_WINDOW)) {
@@ -523,8 +528,8 @@ class ActivityEmbeddingAnimationRunner {
/**
* Whether we should use jump cut for the change transition.
* This normally happens when opening a new secondary with the existing primary using a
- * different split layout. This can be complicated, like from horizontal to vertical split with
- * new split pairs.
+ * different split layout (ratio or direction). This can be complicated, like from horizontal to
+ * vertical split with new split pairs.
* Uses a jump cut animation to simplify.
*/
private boolean shouldUseJumpCutForChangeTransition(@NonNull TransitionInfo info) {
@@ -553,8 +558,8 @@ class ActivityEmbeddingAnimationRunner {
}
// Check if the transition contains both opening and closing windows.
- boolean hasOpeningWindow = false;
- boolean hasClosingWindow = false;
+ final List<TransitionInfo.Change> openChanges = new ArrayList<>();
+ final List<TransitionInfo.Change> closeChanges = new ArrayList<>();
for (TransitionInfo.Change change : info.getChanges()) {
if (changingChanges.contains(change)) {
continue;
@@ -564,10 +569,30 @@ class ActivityEmbeddingAnimationRunner {
// No-op if it will be covered by the changing parent window.
continue;
}
- hasOpeningWindow |= TransitionUtil.isOpeningType(change.getMode());
- hasClosingWindow |= TransitionUtil.isClosingType(change.getMode());
+ if (TransitionUtil.isOpeningType(change.getMode())) {
+ openChanges.add(change);
+ } else if (TransitionUtil.isClosingType(change.getMode())) {
+ closeChanges.add(change);
+ }
+ }
+ if (openChanges.isEmpty() || closeChanges.isEmpty()) {
+ // Only skip if the transition contains both open and close.
+ return false;
+ }
+ if (changingChanges.size() != 1 || openChanges.size() != 1 || closeChanges.size() != 1) {
+ // Skip when there are too many windows involved.
+ return true;
+ }
+ final TransitionInfo.Change changingChange = changingChanges.get(0);
+ final TransitionInfo.Change openChange = openChanges.get(0);
+ final TransitionInfo.Change closeChange = closeChanges.get(0);
+ if (changingChange.getStartAbsBounds().equals(openChange.getEndAbsBounds())
+ && changingChange.getEndAbsBounds().equals(closeChange.getStartAbsBounds())) {
+ // Don't skip if the transition is a simple shifting without split direction or ratio
+ // change. For example, A|B -> B|C.
+ return false;
}
- return hasOpeningWindow && hasClosingWindow;
+ return true;
}
/** Updates the changes to end states in {@code startTransaction} for jump cut animation. */
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..b9868629e64b 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
@@ -38,6 +38,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 +203,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, isEnter);
final Animation animation;
if (customAnimation != null) {
animation = customAnimation;
@@ -229,7 +230,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, isEnter);
final Animation animation;
if (customAnimation != null) {
animation = customAnimation;
@@ -261,8 +262,14 @@ 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, boolean isEnter) {
+ final TransitionInfo.AnimationOptions options;
+ if (Flags.moveAnimationOptionsToChange()) {
+ options = change.getAnimationOptions();
+ } else {
+ options = info.getAnimationOptions();
+ }
if (options == null || options.getType() != ANIM_CUSTOM) {
return null;
}
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 1f9358e2aa91..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
@@ -22,6 +22,7 @@ import static android.window.TransitionInfo.FLAG_FILLS_TASK;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
import static com.android.wm.shell.transition.DefaultTransitionHandler.isSupportedOverrideAnimation;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE;
import static java.util.Objects.requireNonNull;
@@ -31,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;
@@ -38,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;
@@ -90,6 +93,12 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle
/** Whether ActivityEmbeddingController should animate this transition. */
public boolean shouldAnimate(@NonNull TransitionInfo info) {
+ if (info.getType() == TRANSIT_TASK_FRAGMENT_DRAG_RESIZE) {
+ // The TRANSIT_TASK_FRAGMENT_DRAG_RESIZE type happens when the user drags the
+ // interactive divider to resize the split containers. The content is veiled, so we will
+ // handle the transition with a jump cut.
+ return true;
+ }
boolean containsEmbeddingChange = false;
for (TransitionInfo.Change change : info.getChanges()) {
if (!change.hasFlags(FLAG_FILLS_TASK) && change.hasFlags(
@@ -110,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/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
index 19963675ff86..ce0bf8b29374 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java
@@ -17,6 +17,7 @@
package com.android.wm.shell.animation;
import android.graphics.Path;
+import android.view.animation.BackGestureInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.PathInterpolator;
@@ -95,6 +96,15 @@ public class Interpolators {
public static final PathInterpolator DIM_INTERPOLATOR =
new PathInterpolator(.23f, .87f, .52f, -0.11f);
+ /**
+ * Use this interpolator for animating progress values coming from the back callback to get
+ * the predictive-back-typical decelerate motion.
+ *
+ * This interpolator is similar to {@link Interpolators#STANDARD_DECELERATE} but has a slight
+ * acceleration phase at the start.
+ */
+ public static final Interpolator BACK_GESTURE = new BackGestureInterpolator();
+
// Create the default emphasized interpolator
private static PathInterpolator createEmphasizedInterpolator() {
Path path = new Path();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
index 8d8dc10951a6..196f89d5794e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
@@ -20,7 +20,7 @@ import android.view.KeyEvent;
import android.view.MotionEvent;
import android.window.BackEvent;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
/**
* Interface for external process to get access to the Back animation related methods.
@@ -49,9 +49,9 @@ public interface BackAnimation {
@BackEvent.SwipeEdge int swipeEdge);
/**
- * Called when the input pointers are pilfered.
+ * Called when the back swipe threshold is crossed.
*/
- void onPilferPointers();
+ void onThresholdCrossed();
/**
* Sets whether the back gesture is past the trigger threshold or not.
@@ -101,4 +101,10 @@ public interface BackAnimation {
* @param customizer the controller to control system bar color.
*/
void setStatusBarCustomizer(StatusBarCustomizer customizer);
+
+ /**
+ * Set a callback to pilfer pointers.
+ * @param pilferCallback the callback to pilfer pointers.
+ */
+ void setPilferPointerCallback(Runnable pilferCallback);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java
index 7749394b21d3..d754d04e6b33 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationBackground.java
@@ -19,8 +19,6 @@ package com.android.wm.shell.back;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
-import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD;
-
import android.annotation.NonNull;
import android.graphics.Color;
import android.graphics.Rect;
@@ -45,6 +43,7 @@ public class BackAnimationBackground {
private boolean mIsRequestingStatusBarAppearance;
private boolean mBackgroundIsDark;
private Rect mStartBounds;
+ private int mStatusbarHeight;
public BackAnimationBackground(RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
@@ -56,9 +55,10 @@ public class BackAnimationBackground {
* @param startRect The start bounds of the closing target.
* @param color The background color.
* @param transaction The animation transaction.
+ * @param statusbarHeight The height of the statusbar (in px).
*/
- public void ensureBackground(
- Rect startRect, int color, @NonNull SurfaceControl.Transaction transaction) {
+ public void ensureBackground(Rect startRect, int color,
+ @NonNull SurfaceControl.Transaction transaction, int statusbarHeight) {
if (mBackgroundSurface != null) {
return;
}
@@ -80,6 +80,7 @@ public class BackAnimationBackground {
.show(mBackgroundSurface);
mStartBounds = startRect;
mIsRequestingStatusBarAppearance = false;
+ mStatusbarHeight = statusbarHeight;
}
/**
@@ -111,14 +112,14 @@ public class BackAnimationBackground {
/**
* Update back animation background with for the progress.
*
- * @param progress Progress value from {@link android.window.BackProgressAnimator}
+ * @param top The top coordinate of the closing target
*/
- public void onBackProgressed(float progress) {
+ public void customizeStatusBarAppearance(int top) {
if (mCustomizer == null || mStartBounds.isEmpty()) {
return;
}
- final boolean shouldCustomizeSystemBar = progress > UPDATE_SYSUI_FLAGS_THRESHOLD;
+ final boolean shouldCustomizeSystemBar = top > mStatusbarHeight / 2;
if (shouldCustomizeSystemBar == mIsRequestingStatusBarAppearance) {
return;
}
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 2606fb661e80..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,13 +18,9 @@ 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;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
@@ -32,7 +28,9 @@ import android.app.ActivityTaskManager;
import android.app.IActivityTaskManager;
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;
@@ -43,19 +41,19 @@ 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.util.MathUtils;
import android.view.IRemoteAnimationRunner;
import android.view.InputDevice;
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;
import android.window.BackNavigationInfo;
+import android.window.BackTouchTracker;
import android.window.IBackAnimationFinishedCallback;
import android.window.IBackAnimationRunner;
import android.window.IOnBackInvokedCallback;
@@ -64,12 +62,13 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.LatencyTracker;
import com.android.internal.view.AppearanceRegion;
-import com.android.wm.shell.animation.FlingAnimationUtils;
+import com.android.wm.shell.R;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ShellBackgroundThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -80,22 +79,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
/**
* Controls the window animation run when a user initiates a back gesture.
*/
-public class BackAnimationController implements RemoteCallable<BackAnimationController> {
+public class BackAnimationController implements RemoteCallable<BackAnimationController>,
+ ConfigurationChangeListener {
private static final String TAG = "ShellBackPreview";
private static final int SETTING_VALUE_OFF = 0;
private static final int SETTING_VALUE_ON = 1;
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);
@@ -114,9 +105,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
/** Tracks if we should start the back gesture on the next motion move event */
private boolean mShouldStartOnNextMoveEvent = false;
private boolean mOnBackStartDispatched = false;
- private boolean mPointerPilfered = false;
-
- private final FlingAnimationUtils mFlingAnimationUtils;
+ private boolean mThresholdCrossed = false;
+ private boolean mPointersPilfered = false;
+ private final boolean mRequirePointerPilfer;
/** Registry for the back animations */
private final ShellBackAnimationRegistry mShellBackAnimationRegistry;
@@ -130,22 +121,25 @@ 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.
*/
- private TouchTracker mCurrentTracker = new TouchTracker();
+ private BackTouchTracker mCurrentTracker = new BackTouchTracker();
/**
* Tracks the next back gesture in case a new user gesture has started while the back animation
* (and navigation) associated with {@link #mCurrentTracker} have not yet finished.
*/
- private TouchTracker mQueuedTracker = new TouchTracker();
+ private BackTouchTracker mQueuedTracker = new BackTouchTracker();
private final Runnable mAnimationTimeoutRunnable = () -> {
ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...",
MAX_ANIMATION_DURATION);
- onBackAnimationFinished();
+ finishBackAnimation();
};
private IBackAnimationFinishedCallback mBackAnimationFinishedCallback;
@@ -154,6 +148,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
@Nullable
private IOnBackInvokedCallback mActiveCallback;
+ @Nullable
+ private RemoteAnimationTarget[] mApps;
@VisibleForTesting
final RemoteCallback mNavigationObserver = new RemoteCallback(
@@ -169,6 +165,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Navigation window gone.");
setTriggerBack(false);
resetTouchTracker();
+ // Don't wait for animation start
+ mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
});
}
});
@@ -180,6 +178,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
// Keep previous navigation type before remove mBackNavigationInfo.
@BackNavigationInfo.BackTargetType
private int mPreviousNavigationType;
+ private Runnable mPilferPointerCallback;
public BackAnimationController(
@NonNull ShellInit shellInit,
@@ -220,17 +219,16 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mActivityTaskManager = activityTaskManager;
mContext = context;
mContentResolver = contentResolver;
+ mRequirePointerPilfer =
+ context.getResources().getBoolean(R.bool.config_backAnimationRequiresPointerPilfer);
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() {
@@ -240,6 +238,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION,
this::createExternalInterface, this);
mShellCommandHandler.addDumpCallback(this::dump, this);
+ mShellController.addConfigurationChangeListener(this);
}
private void setupAnimationDeveloperSettingsObserver(
@@ -289,6 +288,16 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
private final BackAnimationImpl mBackAnimation = new BackAnimationImpl();
@Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ mShellBackAnimationRegistry.onConfigurationChanged(newConfig);
+ updateTouchableArea();
+ }
+
+ private void updateTouchableArea() {
+ mTouchableArea.set(mWindowManager.getCurrentWindowMetrics().getBounds());
+ }
+
+ @Override
public Context getContext() {
return mContext;
}
@@ -318,8 +327,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
@Override
- public void onPilferPointers() {
- BackAnimationController.this.onPilferPointers();
+ public void onThresholdCrossed() {
+ BackAnimationController.this.onThresholdCrossed();
}
@Override
@@ -341,6 +350,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mCustomizer = customizer;
mAnimationBackground.setStatusBarCustomizer(customizer);
}
+
+ @Override
+ public void setPilferPointerCallback(Runnable callback) {
+ mShellExecutor.execute(() -> {
+ mPilferPointerCallback = callback;
+ });
+ }
}
private static class IBackAnimationImpl extends IBackAnimation.Stub
@@ -397,23 +413,34 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mShellBackAnimationRegistry.unregisterAnimation(type);
}
- private TouchTracker getActiveTracker() {
+ private BackTouchTracker getActiveTracker() {
if (mCurrentTracker.isActive()) return mCurrentTracker;
if (mQueuedTracker.isActive()) return mQueuedTracker;
return null;
}
@VisibleForTesting
- void onPilferPointers() {
- mPointerPilfered = true;
+ public void onThresholdCrossed() {
+ mThresholdCrossed = true;
// Dispatch onBackStarted, only to app callbacks.
// System callbacks will receive onBackStarted when the remote animation starts.
- if (!shouldDispatchToAnimator() && mActiveCallback != null) {
+ final boolean shouldDispatchToAnimator = shouldDispatchToAnimator();
+ 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}
@@ -426,7 +453,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
int keyAction,
@BackEvent.SwipeEdge int swipeEdge) {
- TouchTracker activeTouchTracker = getActiveTracker();
+ BackTouchTracker activeTouchTracker = getActiveTracker();
if (activeTouchTracker != null) {
activeTouchTracker.update(touchX, touchY, velocityX, velocityY);
}
@@ -462,7 +489,15 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
- TouchTracker touchTracker;
+ boolean interruptCancelPostCommitAnimation = mPostCommitAnimationInProgress
+ && mCurrentTracker.isFinished() && !mCurrentTracker.getTriggerBack()
+ && mQueuedTracker.isInInitialState();
+ if (interruptCancelPostCommitAnimation) {
+ // If a system animation is currently in the post-commit phase animating an
+ // onBackCancelled event, let's interrupt it and start animating a new back gesture
+ resetTouchTracker();
+ }
+ BackTouchTracker touchTracker;
if (mCurrentTracker.isInInitialState()) {
touchTracker = mCurrentTracker;
} else if (mQueuedTracker.isInInitialState()) {
@@ -473,17 +508,23 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
return;
}
touchTracker.setGestureStartLocation(touchX, touchY, swipeEdge);
- touchTracker.setState(TouchTracker.TouchTrackerState.ACTIVE);
+ touchTracker.setState(BackTouchTracker.TouchTrackerState.ACTIVE);
mBackGestureStarted = true;
- if (touchTracker == mCurrentTracker) {
+ if (interruptCancelPostCommitAnimation) {
+ // post-commit cancel is currently running. let's interrupt it and dispatch a new
+ // onBackStarted event.
+ mPostCommitAnimationInProgress = false;
+ mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
+ startSystemAnimation();
+ } else if (touchTracker == mCurrentTracker) {
// Only start the back navigation if no other gesture is being processed. Otherwise,
- // the back navigation will be started once the current gesture has finished.
+ // the back navigation will fall back to legacy back event injection.
startBackNavigation(mCurrentTracker);
}
}
- private void startBackNavigation(@NonNull TouchTracker touchTracker) {
+ private void startBackNavigation(@NonNull BackTouchTracker touchTracker) {
try {
startLatencyTracking();
mBackNavigationInfo = mActivityTaskManager.startBackNavigation(
@@ -496,7 +537,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
private void onBackNavigationInfoReceived(@Nullable BackNavigationInfo backNavigationInfo,
- @NonNull TouchTracker touchTracker) {
+ @NonNull BackTouchTracker touchTracker) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo);
if (backNavigationInfo == null) {
ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Received BackNavigationInfo is null.");
@@ -509,11 +550,15 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
if (!mShellBackAnimationRegistry.startGesture(backType)) {
mActiveCallback = null;
}
+ tryPilferPointers();
} else {
mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
// App is handling back animation. Cancel system animation latency tracking.
cancelLatencyTracking();
tryDispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null));
+ if (!isAppProgressGenerationAllowed()) {
+ tryPilferPointers();
+ }
}
}
@@ -557,10 +602,22 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
&& mBackNavigationInfo.isPrepareRemoteAnimation();
}
+ private void tryPilferPointers() {
+ if (mPointersPilfered || !mThresholdCrossed) {
+ return;
+ }
+ if (mPilferPointerCallback != null) {
+ mPilferPointerCallback.run();
+ }
+ mPointersPilfered = true;
+ }
+
private void tryDispatchOnBackStarted(
IOnBackInvokedCallback callback,
BackMotionEvent backEvent) {
- if (mOnBackStartDispatched || callback == null || !mPointerPilfered) {
+ if (mOnBackStartDispatched
+ || callback == null
+ || (!mThresholdCrossed && mRequirePointerPilfer)) {
return;
}
dispatchOnBackStarted(callback, backEvent);
@@ -580,79 +637,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
}
-
- /**
- * Allows us to manage the fling gesture, it smoothly animates the current progress value to
- * the final position, calculated based on the current velocity.
- *
- * @param callback the callback to be invoked when the animation ends.
- */
- private void dispatchOrAnimateOnBackInvoked(IOnBackInvokedCallback callback,
- @NonNull TouchTracker touchTracker) {
- if (callback == null) {
- return;
- }
-
- boolean animationStarted = false;
-
- if (mBackNavigationInfo != null && mBackNavigationInfo.isAnimationCallback()) {
-
- final BackMotionEvent backMotionEvent = touchTracker.createProgressEvent();
- if (backMotionEvent != null) {
- // Constraints - absolute values
- float minVelocity = mFlingAnimationUtils.getMinVelocityPxPerSecond();
- float maxVelocity = mFlingAnimationUtils.getHighVelocityPxPerSecond();
- float maxX = touchTracker.getMaxDistance(); // px
- float maxFlingDistance = maxX * MAX_FLING_PROGRESS; // px
-
- // Current state
- float currentX = backMotionEvent.getTouchX();
- float velocity = MathUtils.constrain(backMotionEvent.getVelocityX(),
- -maxVelocity, maxVelocity);
-
- // Target state
- float animationFaction = velocity / maxVelocity; // value between -1 and 1
- float flingDistance = animationFaction * maxFlingDistance; // px
- float endX = MathUtils.constrain(currentX + flingDistance, 0f, maxX);
-
- if (!Float.isNaN(endX)
- && currentX != endX
- && Math.abs(velocity) >= minVelocity) {
- ValueAnimator animator = ValueAnimator.ofFloat(currentX, endX);
-
- mFlingAnimationUtils.apply(
- /* animator = */ animator,
- /* currValue = */ currentX,
- /* endValue = */ endX,
- /* velocity = */ velocity,
- /* maxDistance = */ maxFlingDistance
- );
-
- animator.addUpdateListener(animation -> {
- Float animatedValue = (Float) animation.getAnimatedValue();
- float progress = touchTracker.getProgress(animatedValue);
- final BackMotionEvent backEvent = touchTracker.createProgressEvent(
- progress);
- dispatchOnBackProgressed(mActiveCallback, backEvent);
- });
-
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- dispatchOnBackInvoked(callback);
- }
- });
- animator.start();
- animationStarted = true;
- }
- }
- }
-
- if (!animationStarted) {
- dispatchOnBackInvoked(callback);
- }
- }
-
private void dispatchOnBackInvoked(IOnBackInvokedCallback callback) {
if (callback == null) {
return;
@@ -664,7 +648,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
}
- private void dispatchOnBackCancelled(IOnBackInvokedCallback callback) {
+ private void tryDispatchOnBackCancelled(IOnBackInvokedCallback callback) {
+ if (!mOnBackStartDispatched) {
+ Log.d(TAG, "Skipping dispatching onBackCancelled. Start was never dispatched.");
+ return;
+ }
if (callback == null) {
return;
}
@@ -677,7 +665,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
private void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
BackMotionEvent backEvent) {
- if (callback == null) {
+ if (callback == null || (!shouldDispatchToAnimator() && mBackNavigationInfo != null
+ && isAppProgressGenerationAllowed())) {
return;
}
try {
@@ -691,7 +680,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
* Sets to true when the back gesture has passed the triggering threshold, false otherwise.
*/
public void setTriggerBack(boolean triggerBack) {
- TouchTracker activeBackGestureInfo = getActiveTracker();
+ if (mActiveCallback != null) {
+ try {
+ mActiveCallback.setTriggerBack(triggerBack);
+ } catch (RemoteException e) {
+ Log.e(TAG, "remote setTriggerBack error: ", e);
+ }
+ }
+ BackTouchTracker activeBackGestureInfo = getActiveTracker();
if (activeBackGestureInfo != null) {
activeBackGestureInfo.setTriggerBack(triggerBack);
}
@@ -705,7 +701,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mQueuedTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor);
}
- private void invokeOrCancelBack(@NonNull TouchTracker touchTracker) {
+ private void invokeOrCancelBack(@NonNull BackTouchTracker touchTracker) {
// Make a synchronized call to core before dispatch back event to client side.
// If the close transition happens before the core receives onAnimationFinished, there will
// play a second close animation for that transition.
@@ -721,9 +717,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
if (mBackNavigationInfo != null) {
final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
if (touchTracker.getTriggerBack()) {
- dispatchOrAnimateOnBackInvoked(callback, touchTracker);
+ dispatchOnBackInvoked(callback);
} else {
- dispatchOnBackCancelled(callback);
+ tryDispatchOnBackCancelled(callback);
}
}
finishBackNavigation(touchTracker.getTriggerBack());
@@ -733,7 +729,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
* Called when the gesture is released, then it could start the post commit animation.
*/
private void onGestureFinished() {
- TouchTracker activeTouchTracker = getActiveTracker();
+ BackTouchTracker activeTouchTracker = getActiveTracker();
if (!mBackGestureStarted || activeTouchTracker == null) {
// This can happen when an unfinished gesture has been reset in resetTouchTracker
ProtoLog.d(WM_SHELL_BACK_PREVIEW,
@@ -743,8 +739,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
boolean triggerBack = activeTouchTracker.getTriggerBack();
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", triggerBack);
+ // Reset gesture states.
+ mThresholdCrossed = false;
+ mPointersPilfered = false;
mBackGestureStarted = false;
- activeTouchTracker.setState(TouchTracker.TouchTrackerState.FINISHED);
+ activeTouchTracker.setState(BackTouchTracker.TouchTrackerState.FINISHED);
if (mPostCommitAnimationInProgress) {
ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation is still running");
@@ -800,9 +799,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
// The next callback should be {@link #onBackAnimationFinished}.
if (mCurrentTracker.getTriggerBack()) {
- dispatchOrAnimateOnBackInvoked(mActiveCallback, mCurrentTracker);
+ // notify gesture finished
+ mBackNavigationInfo.onBackGestureFinished(true);
+ dispatchOnBackInvoked(mActiveCallback);
} else {
- dispatchOnBackCancelled(mActiveCallback);
+ tryDispatchOnBackCancelled(mActiveCallback);
}
}
@@ -812,6 +813,20 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
*/
@VisibleForTesting
void onBackAnimationFinished() {
+ if (!mPostCommitAnimationInProgress) {
+ // This can happen when a post-commit cancel animation was interrupted by a new back
+ // gesture but the timing of interruption was bad such that the back-callback
+ // implementation finished in between the time of the new gesture having started and
+ // the time of the back-callback receiving the new onBackStarted event. Due to the
+ // asynchronous APIs this isn't an unlikely case. To handle this, let's return early.
+ // The back-callback implementation will call onBackAnimationFinished again when it is
+ // done with animating the second gesture.
+ return;
+ }
+ finishBackAnimation();
+ }
+
+ private void finishBackAnimation() {
// Stop timeout runner.
mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
mPostCommitAnimationInProgress = false;
@@ -829,10 +844,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
/**
- * Resets the TouchTracker and potentially starts a new back navigation in case one is queued
+ * Resets the BackTouchTracker and potentially starts a new back navigation in case one
+ * is queued.
*/
private void resetTouchTracker() {
- TouchTracker temp = mCurrentTracker;
+ BackTouchTracker temp = mCurrentTracker;
mCurrentTracker = mQueuedTracker;
temp.reset();
mQueuedTracker = temp;
@@ -840,7 +856,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
if (mCurrentTracker.isInInitialState()) {
if (mBackGestureStarted) {
mBackGestureStarted = false;
- dispatchOnBackCancelled(mActiveCallback);
+ tryDispatchOnBackCancelled(mActiveCallback);
finishBackNavigation(false);
ProtoLog.d(WM_SHELL_BACK_PREVIEW,
"resetTouchTracker -> reset an unfinished gesture");
@@ -872,9 +888,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
void finishBackNavigation(boolean triggerBack) {
ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()");
mActiveCallback = null;
+ mApps = null;
mShouldStartOnNextMoveEvent = false;
mOnBackStartDispatched = false;
- mPointerPilfered = false;
+ mThresholdCrossed = false;
+ mPointersPilfered = false;
mShellBackAnimationRegistry.resetDefaultCrossActivity();
cancelLatencyTracking();
if (mBackNavigationInfo != null) {
@@ -908,6 +926,57 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mTrackingLatency = false;
}
+ private void startSystemAnimation() {
+ if (mBackNavigationInfo == null) {
+ ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Lack of navigation info to start animation.");
+ return;
+ }
+ if (!validateAnimationTargets(mApps)) {
+ ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Not starting animation due to mApps being null.");
+ return;
+ }
+
+ final BackAnimationRunner runner =
+ mShellBackAnimationRegistry.getAnimationRunnerAndInit(mBackNavigationInfo);
+ if (runner == null) {
+ if (mBackAnimationFinishedCallback != null) {
+ try {
+ mBackAnimationFinishedCallback.onAnimationFinished(false);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call IBackNaviAnimationController", e);
+ }
+ }
+ return;
+ }
+ mActiveCallback = runner.getCallback();
+
+ ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()");
+
+ runner.startAnimation(mApps, /*wallpapers*/ null, /*nonApps*/ null,
+ () -> mShellExecutor.execute(this::onBackAnimationFinished));
+
+ if (mApps.length >= 1) {
+ mCurrentTracker.updateStartLocation();
+ BackMotionEvent startEvent = mCurrentTracker.createStartEvent(mApps[0]);
+ dispatchOnBackStarted(mActiveCallback, startEvent);
+ }
+ }
+
+ /**
+ * Validate animation targets.
+ */
+ static boolean validateAnimationTargets(RemoteAnimationTarget[] apps) {
+ if (apps == null || apps.length == 0) {
+ return false;
+ }
+ for (int i = apps.length - 1; i >= 0; --i) {
+ if (!apps[i].leash.isValid()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private void createAdapter() {
IBackAnimationRunner runner =
new IBackAnimationRunner.Stub() {
@@ -920,48 +989,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
mShellExecutor.execute(
() -> {
endLatencyTracking();
- if (mBackNavigationInfo == null) {
- ProtoLog.e(WM_SHELL_BACK_PREVIEW,
- "Lack of navigation info to start animation.");
- return;
- }
- final BackAnimationRunner runner =
- mShellBackAnimationRegistry.getAnimationRunnerAndInit(
- mBackNavigationInfo);
- if (runner == null) {
- if (finishedCallback != null) {
- try {
- finishedCallback.onAnimationFinished(false);
- } catch (RemoteException e) {
- Log.w(
- TAG,
- "Failed call IBackNaviAnimationController",
- e);
- }
- }
+ if (!validateAnimationTargets(apps)) {
+ Log.e(TAG, "Invalid animation targets!");
return;
}
- mActiveCallback = runner.getCallback();
mBackAnimationFinishedCallback = finishedCallback;
-
- ProtoLog.d(
- WM_SHELL_BACK_PREVIEW,
- "BackAnimationController: startAnimation()");
- runner.startAnimation(
- apps,
- wallpapers,
- nonApps,
- () ->
- mShellExecutor.execute(
- BackAnimationController.this
- ::onBackAnimationFinished));
-
- if (apps.length >= 1) {
- mCurrentTracker.updateStartLocation();
- BackMotionEvent startEvent =
- mCurrentTracker.createStartEvent(apps[0]);
- dispatchOnBackStarted(mActiveCallback, startEvent);
- }
+ mApps = apps;
+ startSystemAnimation();
// Dispatch the first progress after animation start for
// smoothing the initial animation, instead of waiting for next
@@ -1006,6 +1040,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
pw.println(prefix + " mBackGestureStarted=" + mBackGestureStarted);
pw.println(prefix + " mPostCommitAnimationInProgress=" + mPostCommitAnimationInProgress);
pw.println(prefix + " mShouldStartOnNextMoveEvent=" + mShouldStartOnNextMoveEvent);
+ pw.println(prefix + " mPointerPilfered=" + mThresholdCrossed);
+ pw.println(prefix + " mRequirePointerPilfer=" + mRequirePointerPilfer);
pw.println(prefix + " mCurrentTracker state:");
mCurrentTracker.dump(pw, prefix + " ");
pw.println(prefix + " mQueuedTracker state:");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
index a32b435ff99e..4988a9481d21 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationRunner.java
@@ -28,6 +28,7 @@ import android.view.RemoteAnimationTarget;
import android.window.IBackAnimationRunner;
import android.window.IOnBackInvokedCallback;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.Cuj.CujType;
import com.android.wm.shell.common.InteractionJankMonitorUtils;
@@ -108,7 +109,8 @@ public class BackAnimationRunner {
}
}
- private boolean shouldMonitorCUJ(RemoteAnimationTarget[] apps) {
+ @VisibleForTesting
+ boolean shouldMonitorCUJ(RemoteAnimationTarget[] apps) {
return apps.length > 0 && mCujType != NO_CUJ;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
deleted file mode 100644
index d6f7c367f772..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
+++ /dev/null
@@ -1,455 +0,0 @@
-/*
- * Copyright (C) 2022 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.back;
-
-import static android.view.RemoteAnimationTarget.MODE_CLOSING;
-import static android.view.RemoteAnimationTarget.MODE_OPENING;
-import static android.window.BackEvent.EDGE_RIGHT;
-
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
-import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD;
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.annotation.NonNull;
-import android.content.Context;
-import android.graphics.Matrix;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.RemoteException;
-import android.util.FloatProperty;
-import android.util.TypedValue;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.view.animation.Interpolator;
-import android.window.BackEvent;
-import android.window.BackMotionEvent;
-import android.window.BackProgressAnimator;
-import android.window.IOnBackInvokedCallback;
-
-import com.android.internal.dynamicanimation.animation.SpringAnimation;
-import com.android.internal.dynamicanimation.animation.SpringForce;
-import com.android.internal.policy.ScreenDecorationsUtils;
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-
-import javax.inject.Inject;
-
-/** Class that defines cross-activity animation. */
-@ShellMainThread
-public class CrossActivityBackAnimation extends ShellBackAnimation {
- /**
- * Minimum scale of the entering/closing window.
- */
- private static final float MIN_WINDOW_SCALE = 0.9f;
-
- /** Duration of post animation after gesture committed. */
- private static final int POST_ANIMATION_DURATION = 350;
- private static final Interpolator INTERPOLATOR = Interpolators.STANDARD_DECELERATE;
- private static final FloatProperty<CrossActivityBackAnimation> ENTER_PROGRESS_PROP =
- new FloatProperty<>("enter-alpha") {
- @Override
- public void setValue(CrossActivityBackAnimation anim, float value) {
- anim.setEnteringProgress(value);
- }
-
- @Override
- public Float get(CrossActivityBackAnimation object) {
- return object.getEnteringProgress();
- }
- };
- private static final FloatProperty<CrossActivityBackAnimation> LEAVE_PROGRESS_PROP =
- new FloatProperty<>("leave-alpha") {
- @Override
- public void setValue(CrossActivityBackAnimation anim, float value) {
- anim.setLeavingProgress(value);
- }
-
- @Override
- public Float get(CrossActivityBackAnimation object) {
- return object.getLeavingProgress();
- }
- };
- private static final float MIN_WINDOW_ALPHA = 0.01f;
- private static final float WINDOW_X_SHIFT_DP = 48;
- private static final int SCALE_FACTOR = 100;
- // TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists.
- private static final float TARGET_COMMIT_PROGRESS = 0.5f;
- private static final float ENTER_ALPHA_THRESHOLD = 0.22f;
-
- private final Rect mStartTaskRect = new Rect();
- private final float mCornerRadius;
-
- // The closing window properties.
- private final RectF mClosingRect = new RectF();
-
- // The entering window properties.
- private final Rect mEnteringStartRect = new Rect();
- private final RectF mEnteringRect = new RectF();
- private final SpringAnimation mEnteringProgressSpring;
- private final SpringAnimation mLeavingProgressSpring;
- // Max window x-shift in pixels.
- private final float mWindowXShift;
- private final BackAnimationRunner mBackAnimationRunner;
-
- private float mEnteringProgress = 0f;
- private float mLeavingProgress = 0f;
-
- private final PointF mInitialTouchPos = new PointF();
-
- private final Matrix mTransformMatrix = new Matrix();
-
- private final float[] mTmpFloat9 = new float[9];
-
- private RemoteAnimationTarget mEnteringTarget;
- private RemoteAnimationTarget mClosingTarget;
- private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
-
- private boolean mBackInProgress = false;
- private boolean mIsRightEdge;
- private boolean mTriggerBack = false;
-
- private PointF mTouchPos = new PointF();
- private IRemoteAnimationFinishedCallback mFinishCallback;
-
- private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
-
- private final BackAnimationBackground mBackground;
-
- @Inject
- public CrossActivityBackAnimation(Context context, BackAnimationBackground background) {
- mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
- mBackAnimationRunner = new BackAnimationRunner(
- new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY);
- mBackground = background;
- mEnteringProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP);
- mEnteringProgressSpring.setSpring(new SpringForce()
- .setStiffness(SpringForce.STIFFNESS_MEDIUM)
- .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
- mLeavingProgressSpring = new SpringAnimation(this, LEAVE_PROGRESS_PROP);
- mLeavingProgressSpring.setSpring(new SpringForce()
- .setStiffness(SpringForce.STIFFNESS_MEDIUM)
- .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
- mWindowXShift = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, WINDOW_X_SHIFT_DP,
- context.getResources().getDisplayMetrics());
- }
-
- /**
- * Returns 1 if x >= edge1, 0 if x <= edge0, and a smoothed value between the two.
- * From https://en.wikipedia.org/wiki/Smoothstep
- */
- private static float smoothstep(float edge0, float edge1, float x) {
- if (x < edge0) return 0;
- if (x >= edge1) return 1;
-
- x = (x - edge0) / (edge1 - edge0);
- return x * x * (3 - 2 * x);
- }
-
- /**
- * Linearly map x from range (a1, a2) to range (b1, b2).
- */
- private static float mapLinear(float x, float a1, float a2, float b1, float b2) {
- return b1 + (x - a1) * (b2 - b1) / (a2 - a1);
- }
-
- /**
- * Linearly map a normalized value from (0, 1) to (min, max).
- */
- private static float mapRange(float value, float min, float max) {
- return min + (value * (max - min));
- }
-
- private void startBackAnimation() {
- if (mEnteringTarget == null || mClosingTarget == null) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
- return;
- }
- mTransaction.setAnimationTransaction();
-
- // Offset start rectangle to align task bounds.
- mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds());
- mStartTaskRect.offsetTo(0, 0);
-
- // Draw background with task background color.
- mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
- mEnteringTarget.taskInfo.taskDescription.getBackgroundColor(), mTransaction);
- setEnteringProgress(0);
- setLeavingProgress(0);
- }
-
- private void applyTransform(SurfaceControl leash, RectF targetRect, float targetAlpha) {
- if (leash == null || !leash.isValid()) {
- return;
- }
-
- final float scale = targetRect.width() / mStartTaskRect.width();
- mTransformMatrix.reset();
- mTransformMatrix.setScale(scale, scale);
- mTransformMatrix.postTranslate(targetRect.left, targetRect.top);
- mTransaction.setAlpha(leash, targetAlpha)
- .setMatrix(leash, mTransformMatrix, mTmpFloat9)
- .setWindowCrop(leash, mStartTaskRect)
- .setCornerRadius(leash, mCornerRadius);
- }
-
- private void finishAnimation() {
- if (mEnteringTarget != null) {
- if (mEnteringTarget.leash != null && mEnteringTarget.leash.isValid()) {
- mTransaction.setCornerRadius(mEnteringTarget.leash, 0);
- mEnteringTarget.leash.release();
- }
- mEnteringTarget = null;
- }
- if (mClosingTarget != null) {
- if (mClosingTarget.leash != null) {
- mClosingTarget.leash.release();
- }
- mClosingTarget = null;
- }
- if (mBackground != null) {
- mBackground.removeBackground(mTransaction);
- }
-
- mTransaction.apply();
- mBackInProgress = false;
- mTransformMatrix.reset();
- mInitialTouchPos.set(0, 0);
-
- if (mFinishCallback != null) {
- try {
- mFinishCallback.onAnimationFinished();
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- mFinishCallback = null;
- }
- mEnteringProgressSpring.animateToFinalPosition(0);
- mEnteringProgressSpring.skipToEnd();
- mLeavingProgressSpring.animateToFinalPosition(0);
- mLeavingProgressSpring.skipToEnd();
- }
-
- private void onGestureProgress(@NonNull BackEvent backEvent) {
- if (!mBackInProgress) {
- mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT;
- mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
- mBackInProgress = true;
- }
- mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
-
- float progress = backEvent.getProgress();
- float springProgress = (mTriggerBack
- ? mapLinear(progress, 0f, 1, TARGET_COMMIT_PROGRESS, 1)
- : mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR;
- mLeavingProgressSpring.animateToFinalPosition(springProgress);
- mEnteringProgressSpring.animateToFinalPosition(springProgress);
- mBackground.onBackProgressed(progress);
- }
-
- private void onGestureCommitted() {
- if (mEnteringTarget == null || mClosingTarget == null || mClosingTarget.leash == null
- || mEnteringTarget.leash == null || !mEnteringTarget.leash.isValid()
- || !mClosingTarget.leash.isValid()) {
- finishAnimation();
- return;
- }
- // End the fade animations
- mLeavingProgressSpring.cancel();
- mEnteringProgressSpring.cancel();
-
- // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
- // coordinate of the gesture driven phase.
- mEnteringRect.round(mEnteringStartRect);
- mTransaction.hide(mClosingTarget.leash);
-
- ValueAnimator valueAnimator =
- ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION);
- valueAnimator.setInterpolator(INTERPOLATOR);
- valueAnimator.addUpdateListener(animation -> {
- float progress = animation.getAnimatedFraction();
- updatePostCommitEnteringAnimation(progress);
- if (progress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD) {
- mBackground.resetStatusBarCustomization();
- }
- mTransaction.apply();
- });
-
- valueAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mBackground.resetStatusBarCustomization();
- finishAnimation();
- }
- });
- valueAnimator.start();
- }
-
- private void updatePostCommitEnteringAnimation(float progress) {
- float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left);
- float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top);
- float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width());
- float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height());
- float alpha = mapRange(progress, getPreCommitEnteringAlpha(), 1.0f);
- mEnteringRect.set(left, top, left + width, top + height);
- applyTransform(mEnteringTarget.leash, mEnteringRect, alpha);
- }
-
- private float getPreCommitEnteringAlpha() {
- return Math.max(smoothstep(ENTER_ALPHA_THRESHOLD, 0.7f, mEnteringProgress),
- MIN_WINDOW_ALPHA);
- }
-
- private float getEnteringProgress() {
- return mEnteringProgress * SCALE_FACTOR;
- }
-
- private void setEnteringProgress(float value) {
- mEnteringProgress = value / SCALE_FACTOR;
- if (mEnteringTarget != null && mEnteringTarget.leash != null) {
- transformWithProgress(
- mEnteringProgress,
- getPreCommitEnteringAlpha(),
- mEnteringTarget.leash,
- mEnteringRect,
- -mWindowXShift,
- 0
- );
- }
- }
-
- private float getPreCommitLeavingAlpha() {
- return Math.max(1 - smoothstep(0, ENTER_ALPHA_THRESHOLD, mLeavingProgress),
- MIN_WINDOW_ALPHA);
- }
-
- private float getLeavingProgress() {
- return mLeavingProgress * SCALE_FACTOR;
- }
-
- private void setLeavingProgress(float value) {
- mLeavingProgress = value / SCALE_FACTOR;
- if (mClosingTarget != null && mClosingTarget.leash != null) {
- transformWithProgress(
- mLeavingProgress,
- getPreCommitLeavingAlpha(),
- mClosingTarget.leash,
- mClosingRect,
- 0,
- mIsRightEdge ? 0 : mWindowXShift
- );
- }
- }
-
- private void transformWithProgress(float progress, float alpha, SurfaceControl surface,
- RectF targetRect, float deltaXMin, float deltaXMax) {
-
- final int width = mStartTaskRect.width();
- final int height = mStartTaskRect.height();
-
- final float interpolatedProgress = INTERPOLATOR.getInterpolation(progress);
- final float closingScale = MIN_WINDOW_SCALE
- + (1 - interpolatedProgress) * (1 - MIN_WINDOW_SCALE);
- final float closingWidth = closingScale * width;
- final float closingHeight = (float) height / width * closingWidth;
-
- // Move the window along the X axis.
- float closingLeft = mStartTaskRect.left + (width - closingWidth) / 2;
- closingLeft += mapRange(interpolatedProgress, deltaXMin, deltaXMax);
-
- // Move the window along the Y axis.
- final float closingTop = (height - closingHeight) * 0.5f;
- targetRect.set(
- closingLeft, closingTop, closingLeft + closingWidth, closingTop + closingHeight);
-
- applyTransform(surface, targetRect, Math.max(alpha, MIN_WINDOW_ALPHA));
- mTransaction.apply();
- }
-
- @Override
- public BackAnimationRunner getRunner() {
- return mBackAnimationRunner;
- }
-
- private final class Callback extends IOnBackInvokedCallback.Default {
- @Override
- public void onBackStarted(BackMotionEvent backEvent) {
- mTriggerBack = backEvent.getTriggerBack();
- mProgressAnimator.onBackStarted(backEvent,
- CrossActivityBackAnimation.this::onGestureProgress);
- }
-
- @Override
- public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
- mTriggerBack = backEvent.getTriggerBack();
- mProgressAnimator.onBackProgressed(backEvent);
- }
-
- @Override
- public void onBackCancelled() {
- mProgressAnimator.onBackCancelled(() -> {
- // mProgressAnimator can reach finish stage earlier than mLeavingProgressSpring,
- // and if we release all animation leash first, the leavingProgressSpring won't
- // able to update the animation anymore, which cause flicker.
- // Here should force update the closing animation target to the final stage before
- // release it.
- setLeavingProgress(0);
- finishAnimation();
- });
- }
-
- @Override
- public void onBackInvoked() {
- mProgressAnimator.reset();
- onGestureCommitted();
- }
- }
-
- private final class Runner extends IRemoteAnimationRunner.Default {
- @Override
- public void onAnimationStart(
- int transit,
- RemoteAnimationTarget[] apps,
- RemoteAnimationTarget[] wallpapers,
- RemoteAnimationTarget[] nonApps,
- IRemoteAnimationFinishedCallback finishedCallback) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to activity animation.");
- for (RemoteAnimationTarget a : apps) {
- if (a.mode == MODE_CLOSING) {
- mClosingTarget = a;
- }
- if (a.mode == MODE_OPENING) {
- mEnteringTarget = a;
- }
- }
-
- startBackAnimation();
- mFinishCallback = finishedCallback;
- }
-
- @Override
- public void onAnimationCancelled() {
- finishAnimation();
- }
- }
-}
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
new file mode 100644
index 000000000000..a3111b31a2f9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
@@ -0,0 +1,586 @@
+/*
+ * 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.back
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.RectF
+import android.os.RemoteException
+import android.util.TimeUtils
+import android.view.Choreographer
+import android.view.Display
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.IRemoteAnimationRunner
+import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Interpolator
+import android.view.animation.Transformation
+import android.window.BackEvent
+import android.window.BackMotionEvent
+import android.window.BackNavigationInfo
+import android.window.BackProgressAnimator
+import android.window.IOnBackInvokedCallback
+import com.android.internal.dynamicanimation.animation.FloatValueHolder
+import com.android.internal.dynamicanimation.animation.SpringAnimation
+import com.android.internal.dynamicanimation.animation.SpringForce
+import com.android.internal.jank.Cuj
+import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.internal.policy.SystemBarUtils
+import com.android.internal.protolog.common.ProtoLog
+import com.android.wm.shell.R
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.animation.Interpolators
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+
+abstract class CrossActivityBackAnimation(
+ private val context: Context,
+ private val background: BackAnimationBackground,
+ private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+ protected val transaction: SurfaceControl.Transaction,
+ private val choreographer: Choreographer
+) : ShellBackAnimation() {
+
+ protected val startClosingRect = RectF()
+ protected val targetClosingRect = RectF()
+ protected val currentClosingRect = RectF()
+
+ protected val startEnteringRect = RectF()
+ protected val targetEnteringRect = RectF()
+ protected val currentEnteringRect = RectF()
+
+ protected val backAnimRect = Rect()
+ private val cropRect = Rect()
+ private val tempRectF = RectF()
+
+ private var cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+ private var statusbarHeight = SystemBarUtils.getStatusBarHeight(context)
+
+ private val backAnimationRunner =
+ BackAnimationRunner(Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY)
+ private val initialTouchPos = PointF()
+ private val transformMatrix = Matrix()
+ private val tmpFloat9 = FloatArray(9)
+ protected var enteringTarget: RemoteAnimationTarget? = null
+ protected var closingTarget: RemoteAnimationTarget? = null
+ private var triggerBack = false
+ private var finishCallback: IRemoteAnimationFinishedCallback? = null
+ private val progressAnimator = BackProgressAnimator()
+ protected val displayBoundsMargin =
+ context.resources.getDimension(R.dimen.cross_task_back_vertical_margin)
+
+ private val gestureInterpolator = Interpolators.BACK_GESTURE
+ private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator()
+
+ private var scrimLayer: SurfaceControl? = null
+ private var maxScrimAlpha: Float = 0f
+
+ private var isLetterboxed = false
+ private var enteringHasSameLetterbox = false
+ private var leftLetterboxLayer: SurfaceControl? = null
+ private var rightLetterboxLayer: SurfaceControl? = null
+ private var letterboxColor: Int = 0
+
+ private val postCommitFlingScale = FloatValueHolder(SPRING_SCALE)
+ private var lastPostCommitFlingScale = SPRING_SCALE
+ private val postCommitFlingSpring = SpringForce(SPRING_SCALE)
+ .setStiffness(SpringForce.STIFFNESS_LOW)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
+ protected var gestureProgress = 0f
+
+ /** Background color to be used during the animation, also see [getBackgroundColor] */
+ protected var customizedBackgroundColor = 0
+
+ /**
+ * Whether the entering target should be shifted vertically with the user gesture in pre-commit
+ */
+ abstract val allowEnteringYShift: Boolean
+
+ /**
+ * Subclasses must set the [startClosingRect] and [targetClosingRect] to define the movement
+ * of the closingTarget during pre-commit phase.
+ */
+ abstract fun preparePreCommitClosingRectMovement(@BackEvent.SwipeEdge swipeEdge: Int)
+
+ /**
+ * Subclasses must set the [startEnteringRect] and [targetEnteringRect] to define the movement
+ * of the enteringTarget during pre-commit phase.
+ */
+ abstract fun preparePreCommitEnteringRectMovement()
+
+ /**
+ * Subclasses must provide a duration (in ms) for the post-commit part of the animation
+ */
+ abstract fun getPostCommitAnimationDuration(): Long
+
+ /**
+ * Returns a base transformation to apply to the entering target during pre-commit. The system
+ * will apply the default animation on top of it.
+ */
+ protected open fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation? =
+ null
+
+ override fun onConfigurationChanged(newConfiguration: Configuration) {
+ cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+ statusbarHeight = SystemBarUtils.getStatusBarHeight(context)
+ }
+
+ override fun getRunner() = backAnimationRunner
+
+ private fun getBackgroundColor(): Int =
+ when {
+ customizedBackgroundColor != 0 -> customizedBackgroundColor
+ isLetterboxed -> letterboxColor
+ enteringTarget != null -> enteringTarget!!.taskInfo.taskDescription!!.backgroundColor
+ else -> 0
+ }
+
+ protected open fun startBackAnimation(backMotionEvent: BackMotionEvent) {
+ if (enteringTarget == null || closingTarget == null) {
+ ProtoLog.d(
+ ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
+ "Entering target or closing target is null."
+ )
+ return
+ }
+ triggerBack = backMotionEvent.triggerBack
+ initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY)
+
+ transaction.setAnimationTransaction()
+ isLetterboxed = closingTarget!!.taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed
+ enteringHasSameLetterbox =
+ isLetterboxed && closingTarget!!.localBounds.equals(enteringTarget!!.localBounds)
+
+ if (isLetterboxed && !enteringHasSameLetterbox) {
+ // Play animation with letterboxes, if closing and entering target have mismatching
+ // letterboxes
+ backAnimRect.set(closingTarget!!.windowConfiguration.bounds)
+ } else {
+ // otherwise play animation on localBounds only
+ backAnimRect.set(closingTarget!!.localBounds)
+ }
+ // Offset start rectangle to align task bounds.
+ backAnimRect.offsetTo(0, 0)
+
+ preparePreCommitClosingRectMovement(backMotionEvent.swipeEdge)
+ preparePreCommitEnteringRectMovement()
+
+ background.ensureBackground(
+ closingTarget!!.windowConfiguration.bounds,
+ getBackgroundColor(),
+ transaction,
+ statusbarHeight
+ )
+ ensureScrimLayer()
+ if (isLetterboxed && enteringHasSameLetterbox) {
+ // crop left and right letterboxes
+ cropRect.set(
+ closingTarget!!.localBounds.left,
+ 0,
+ closingTarget!!.localBounds.right,
+ closingTarget!!.windowConfiguration.bounds.height()
+ )
+ // and add fake letterbox square surfaces instead
+ ensureLetterboxes()
+ } else {
+ cropRect.set(backAnimRect)
+ }
+ applyTransaction()
+ }
+
+ private fun onGestureProgress(backEvent: BackEvent) {
+ val progress = gestureInterpolator.getInterpolation(backEvent.progress)
+ gestureProgress = progress
+ currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
+ val yOffset = getYOffset(currentClosingRect, backEvent.touchY)
+ currentClosingRect.offset(0f, yOffset)
+ applyTransform(closingTarget?.leash, currentClosingRect, 1f)
+ currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
+ if (allowEnteringYShift) currentEnteringRect.offset(0f, yOffset)
+ val enteringTransformation = getPreCommitEnteringBaseTransformation(progress)
+ applyTransform(
+ enteringTarget?.leash,
+ currentEnteringRect,
+ enteringTransformation?.alpha ?: 1f,
+ enteringTransformation
+ )
+ applyTransaction()
+ background.customizeStatusBarAppearance(currentClosingRect.top.toInt())
+ }
+
+ private fun getYOffset(centeredRect: RectF, touchY: Float): Float {
+ val screenHeight = backAnimRect.height()
+ // Base the window movement in the Y axis on the touch movement in the Y axis.
+ val rawYDelta = touchY - initialTouchPos.y
+ val yDirection = (if (rawYDelta < 0) -1 else 1)
+ // limit yDelta interpretation to 1/2 of screen height in either direction
+ val deltaYRatio = min(screenHeight / 2f, abs(rawYDelta)) / (screenHeight / 2f)
+ val interpolatedYRatio: Float = verticalMoveInterpolator.getInterpolation(deltaYRatio)
+ // limit y-shift so surface never passes 8dp screen margin
+ val deltaY =
+ max(0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin) *
+ interpolatedYRatio *
+ yDirection
+ return deltaY
+ }
+
+ protected open fun onGestureCommitted(velocity: Float) {
+ if (
+ closingTarget?.leash == null ||
+ enteringTarget?.leash == null ||
+ !enteringTarget!!.leash.isValid ||
+ !closingTarget!!.leash.isValid
+ ) {
+ finishAnimation()
+ return
+ }
+
+ // kick off spring animation with the current velocity from the pre-commit phase, this
+ // affects the scaling of the closing and/or opening activity during post-commit
+ val startVelocity =
+ if (gestureProgress < 0.1f) -DEFAULT_FLING_VELOCITY else -velocity * SPRING_SCALE
+ val flingAnimation = SpringAnimation(postCommitFlingScale, SPRING_SCALE)
+ .setStartVelocity(startVelocity.coerceIn(-MAX_FLING_VELOCITY, 0f))
+ .setStartValue(SPRING_SCALE)
+ .setSpring(postCommitFlingSpring)
+ flingAnimation.start()
+ // do an animation-frame immediately to prevent idle frame
+ flingAnimation.doAnimationFrame(choreographer.lastFrameTimeNanos / TimeUtils.NANOS_PER_MS)
+
+ val valueAnimator =
+ ValueAnimator.ofFloat(1f, 0f).setDuration(getPostCommitAnimationDuration())
+ valueAnimator.addUpdateListener { animation: ValueAnimator ->
+ val progress = animation.animatedFraction
+ onPostCommitProgress(progress)
+ if (progress > 1 - BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD) {
+ background.resetStatusBarCustomization()
+ }
+ }
+ valueAnimator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ background.resetStatusBarCustomization()
+ finishAnimation()
+ }
+ }
+ )
+ valueAnimator.start()
+ }
+
+ protected open fun onPostCommitProgress(linearProgress: Float) {
+ scrimLayer?.let { transaction.setAlpha(it, maxScrimAlpha * (1f - linearProgress)) }
+ }
+
+ protected open fun finishAnimation() {
+ enteringTarget?.let {
+ if (it.leash != null && it.leash.isValid) {
+ transaction.setCornerRadius(it.leash, 0f)
+ if (!triggerBack) transaction.setAlpha(it.leash, 0f)
+ it.leash.release()
+ }
+ enteringTarget = null
+ }
+
+ closingTarget?.leash?.release()
+ closingTarget = null
+
+ background.removeBackground(transaction)
+ applyTransaction()
+ transformMatrix.reset()
+ initialTouchPos.set(0f, 0f)
+ try {
+ finishCallback?.onAnimationFinished()
+ } catch (e: RemoteException) {
+ e.printStackTrace()
+ }
+ finishCallback = null
+ removeScrimLayer()
+ removeLetterbox()
+ isLetterboxed = false
+ enteringHasSameLetterbox = false
+ lastPostCommitFlingScale = SPRING_SCALE
+ gestureProgress = 0f
+ }
+
+ protected fun applyTransform(
+ leash: SurfaceControl?,
+ rect: RectF,
+ alpha: Float,
+ baseTransformation: Transformation? = null,
+ flingMode: FlingMode = FlingMode.NO_FLING
+ ) {
+ if (leash == null || !leash.isValid) return
+ tempRectF.set(rect)
+ if (flingMode != FlingMode.NO_FLING) {
+ lastPostCommitFlingScale = min(
+ postCommitFlingScale.value / SPRING_SCALE,
+ if (flingMode == FlingMode.FLING_BOUNCE) 1f else lastPostCommitFlingScale
+ )
+ // apply an additional scale to the closing target to account for fling velocity
+ tempRectF.scaleCentered(lastPostCommitFlingScale)
+ }
+ val scale = tempRectF.width() / backAnimRect.width()
+ val matrix = baseTransformation?.matrix ?: transformMatrix.apply { reset() }
+ val scalePivotX =
+ if (isLetterboxed && enteringHasSameLetterbox) {
+ closingTarget!!.localBounds.left.toFloat()
+ } else {
+ 0f
+ }
+ matrix.postScale(scale, scale, scalePivotX, 0f)
+ matrix.postTranslate(tempRectF.left, tempRectF.top)
+ transaction
+ .setAlpha(leash, alpha)
+ .setMatrix(leash, matrix, tmpFloat9)
+ .setCrop(leash, cropRect)
+ .setCornerRadius(leash, cornerRadius)
+ }
+
+ protected fun applyTransaction() {
+ transaction.setFrameTimelineVsync(choreographer.vsyncId)
+ transaction.apply()
+ }
+
+ private fun ensureScrimLayer() {
+ if (scrimLayer != null) return
+ val isDarkTheme: Boolean = isDarkMode(context)
+ val scrimBuilder =
+ SurfaceControl.Builder()
+ .setName("Cross-Activity back animation scrim")
+ .setCallsite("CrossActivityBackAnimation")
+ .setColorLayer()
+ .setOpaque(false)
+ .setHidden(false)
+
+ rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder)
+ scrimLayer = scrimBuilder.build()
+ val colorComponents = floatArrayOf(0f, 0f, 0f)
+ maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT
+ val scrimCrop =
+ if (isLetterboxed) {
+ closingTarget!!.windowConfiguration.bounds
+ } else {
+ closingTarget!!.localBounds
+ }
+ transaction
+ .setColor(scrimLayer, colorComponents)
+ .setAlpha(scrimLayer!!, maxScrimAlpha)
+ .setCrop(scrimLayer!!, scrimCrop)
+ .setRelativeLayer(scrimLayer!!, closingTarget!!.leash, -1)
+ .show(scrimLayer)
+ }
+
+ private fun removeScrimLayer() {
+ if (removeLayer(scrimLayer)) applyTransaction()
+ scrimLayer = null
+ }
+
+ /**
+ * Adds two "fake" letterbox square surfaces to the left and right of the localBounds of the
+ * closing target
+ */
+ private fun ensureLetterboxes() {
+ closingTarget?.let { t ->
+ if (t.localBounds.left != 0 && leftLetterboxLayer == null) {
+ val bounds =
+ Rect(
+ 0,
+ t.windowConfiguration.bounds.top,
+ t.localBounds.left,
+ t.windowConfiguration.bounds.bottom
+ )
+ leftLetterboxLayer = ensureLetterbox(bounds)
+ }
+ if (
+ t.localBounds.right != t.windowConfiguration.bounds.right &&
+ rightLetterboxLayer == null
+ ) {
+ val bounds =
+ Rect(
+ t.localBounds.right,
+ t.windowConfiguration.bounds.top,
+ t.windowConfiguration.bounds.right,
+ t.windowConfiguration.bounds.bottom
+ )
+ rightLetterboxLayer = ensureLetterbox(bounds)
+ }
+ }
+ }
+
+ private fun ensureLetterbox(bounds: Rect): SurfaceControl {
+ val letterboxBuilder =
+ SurfaceControl.Builder()
+ .setName("Cross-Activity back animation letterbox")
+ .setCallsite("CrossActivityBackAnimation")
+ .setColorLayer()
+ .setOpaque(true)
+ .setHidden(false)
+
+ rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, letterboxBuilder)
+ val layer = letterboxBuilder.build()
+ val colorComponents =
+ floatArrayOf(
+ Color.red(letterboxColor) / 255f,
+ Color.green(letterboxColor) / 255f,
+ Color.blue(letterboxColor) / 255f
+ )
+ transaction
+ .setColor(layer, colorComponents)
+ .setCrop(layer, bounds)
+ .setRelativeLayer(layer, closingTarget!!.leash, 1)
+ .show(layer)
+ return layer
+ }
+
+ private fun removeLetterbox() {
+ if (removeLayer(leftLetterboxLayer) || removeLayer(rightLetterboxLayer)) applyTransaction()
+ leftLetterboxLayer = null
+ rightLetterboxLayer = null
+ }
+
+ private fun removeLayer(layer: SurfaceControl?): Boolean {
+ layer?.let {
+ if (it.isValid) {
+ transaction.remove(it)
+ return true
+ }
+ }
+ return false
+ }
+
+ override fun prepareNextAnimation(
+ animationInfo: BackNavigationInfo.CustomAnimationInfo?,
+ letterboxColor: Int
+ ): Boolean {
+ this.letterboxColor = letterboxColor
+ return false
+ }
+
+ private inner class Callback : IOnBackInvokedCallback.Default() {
+ override fun onBackStarted(backMotionEvent: BackMotionEvent) {
+ // in case we're still animating an onBackCancelled event, let's remove the finish-
+ // callback from the progress animator to prevent calling finishAnimation() before
+ // restarting a new animation
+ progressAnimator.removeOnBackCancelledFinishCallback()
+
+ startBackAnimation(backMotionEvent)
+ progressAnimator.onBackStarted(backMotionEvent) { backEvent: BackEvent ->
+ onGestureProgress(backEvent)
+ }
+ }
+
+ override fun onBackProgressed(backEvent: BackMotionEvent) {
+ triggerBack = backEvent.triggerBack
+ progressAnimator.onBackProgressed(backEvent)
+ }
+
+ override fun onBackCancelled() {
+ progressAnimator.onBackCancelled { finishAnimation() }
+ }
+
+ override fun onBackInvoked() {
+ progressAnimator.reset()
+ onGestureCommitted(progressAnimator.velocity)
+ }
+ }
+
+ private inner class Runner : IRemoteAnimationRunner.Default() {
+ override fun onAnimationStart(
+ transit: Int,
+ apps: Array<RemoteAnimationTarget>,
+ wallpapers: Array<RemoteAnimationTarget>?,
+ nonApps: Array<RemoteAnimationTarget>?,
+ finishedCallback: IRemoteAnimationFinishedCallback
+ ) {
+ ProtoLog.d(
+ ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
+ "Start back to activity animation."
+ )
+ for (a in apps) {
+ when (a.mode) {
+ RemoteAnimationTarget.MODE_CLOSING -> closingTarget = a
+ RemoteAnimationTarget.MODE_OPENING -> enteringTarget = a
+ }
+ }
+ finishCallback = finishedCallback
+ }
+
+ override fun onAnimationCancelled() {
+ finishAnimation()
+ }
+ }
+
+ companion object {
+ /** Max scale of the closing window. */
+ internal const val MAX_SCALE = 0.9f
+ private const val MAX_SCRIM_ALPHA_DARK = 0.8f
+ private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f
+ private const val SPRING_SCALE = 100f
+ private const val MAX_FLING_VELOCITY = 1000f
+ private const val DEFAULT_FLING_VELOCITY = 120f
+ }
+
+ enum class FlingMode {
+ NO_FLING,
+
+ /**
+ * This is used for the closing target in custom cross-activity back animations. When the
+ * back gesture is flung, the closing target shrinks a bit further with a spring motion.
+ */
+ FLING_SHRINK,
+
+ /**
+ * This is used for the closing and opening target in the default cross-activity back
+ * animation. When the back gesture is flung, the closing and opening targets shrink a
+ * bit further and then bounce back with a spring motion.
+ */
+ FLING_BOUNCE
+ }
+}
+
+private fun isDarkMode(context: Context): Boolean {
+ return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
+ Configuration.UI_MODE_NIGHT_YES
+}
+
+internal fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Float) {
+ require(!(progress < 0 || progress > 1)) { "Progress value must be between 0 and 1" }
+ left = start.left + (target.left - start.left) * progress
+ top = start.top + (target.top - start.top) * progress
+ right = start.right + (target.right - start.right) * progress
+ bottom = start.bottom + (target.bottom - start.bottom) * progress
+}
+
+internal fun RectF.scaleCentered(
+ scale: Float,
+ pivotX: Float = left + width() / 2,
+ pivotY: Float = top + height() / 2
+) {
+ offset(-pivotX, -pivotY) // move pivot to origin
+ scale(scale)
+ offset(pivotX, pivotY) // Move back to the original position
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
index 4b3154190910..381914a58cf2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -29,11 +29,13 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.content.Context;
+import android.content.res.Configuration;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.RemoteException;
+import android.view.Choreographer;
import android.view.IRemoteAnimationFinishedCallback;
import android.view.IRemoteAnimationRunner;
import android.view.RemoteAnimationTarget;
@@ -46,10 +48,11 @@ import android.window.BackProgressAnimator;
import android.window.IOnBackInvokedCallback;
import com.android.internal.policy.ScreenDecorationsUtils;
+import com.android.internal.policy.SystemBarUtils;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
import javax.inject.Inject;
@@ -79,7 +82,8 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
private static final int POST_ANIMATION_DURATION_MS = 500;
private final Rect mStartTaskRect = new Rect();
- private final float mCornerRadius;
+ private float mCornerRadius;
+ private int mStatusbarHeight;
// The closing window properties.
private final Rect mClosingStartRect = new Rect();
@@ -91,7 +95,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
private final PointF mInitialTouchPos = new PointF();
private final Interpolator mPostAnimationInterpolator = Interpolators.EMPHASIZED;
- private final Interpolator mProgressInterpolator = Interpolators.STANDARD_DECELERATE;
+ private final Interpolator mProgressInterpolator = Interpolators.BACK_GESTURE;
private final Interpolator mVerticalMoveInterpolator = new DecelerateInterpolator();
private final Matrix mTransformMatrix = new Matrix();
@@ -112,11 +116,21 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
@Inject
public CrossTaskBackAnimation(Context context, BackAnimationBackground background) {
- mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
mBackAnimationRunner = new BackAnimationRunner(
new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_TASK);
mBackground = background;
mContext = context;
+ loadResources();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ loadResources();
+ }
+
+ private void loadResources() {
+ mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
+ mStatusbarHeight = SystemBarUtils.getStatusBarHeight(mContext);
}
private static float mapRange(float value, float min, float max) {
@@ -142,7 +156,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
// Draw background.
mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
- BACKGROUNDCOLOR, mTransaction);
+ BACKGROUNDCOLOR, mTransaction, mStatusbarHeight);
mInterWindowMargin = mContext.getResources()
.getDimension(R.dimen.cross_task_back_inter_window_margin);
mVerticalMargin = mContext.getResources()
@@ -192,9 +206,9 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
applyTransform(mClosingTarget.leash, mClosingCurrentRect, mCornerRadius);
applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius);
- mTransaction.apply();
+ applyTransaction();
- mBackground.onBackProgressed(progress);
+ mBackground.customizeStatusBarAppearance((int) scaledTop);
}
private void updatePostCommitClosingAnimation(float progress) {
@@ -242,6 +256,11 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
.setCornerRadius(leash, cornerRadius);
}
+ private void applyTransaction() {
+ mTransaction.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+ mTransaction.apply();
+ }
+
private void finishAnimation() {
if (mEnteringTarget != null) {
mEnteringTarget.leash.release();
@@ -255,8 +274,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
if (mBackground != null) {
mBackground.removeBackground(mTransaction);
}
-
- mTransaction.apply();
+ applyTransaction();
mBackInProgress = false;
mTransformMatrix.reset();
mClosingCurrentRect.setEmpty();
@@ -275,8 +293,6 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
private void onGestureProgress(@NonNull BackEvent backEvent) {
if (!mBackInProgress) {
- mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT;
- mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
mBackInProgress = true;
}
float progress = backEvent.getProgress();
@@ -305,7 +321,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
if (progress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD) {
mBackground.resetStatusBarCustomization();
}
- mTransaction.apply();
+ applyTransaction();
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@@ -326,6 +342,13 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
private final class Callback extends IOnBackInvokedCallback.Default {
@Override
public void onBackStarted(BackMotionEvent backEvent) {
+ // in case we're still animating an onBackCancelled event, let's remove the finish-
+ // callback from the progress animator to prevent calling finishAnimation() before
+ // restarting a new animation
+ mProgressAnimator.removeOnBackCancelledFinishCallback();
+
+ mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT;
+ mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
mProgressAnimator.onBackStarted(backEvent,
CrossTaskBackAnimation.this::onGestureProgress);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt
new file mode 100644
index 000000000000..c738ce542f8a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt
@@ -0,0 +1,302 @@
+/*
+ * 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.back
+
+import android.content.Context
+import android.graphics.Rect
+import android.graphics.RectF
+import android.util.MathUtils
+import android.view.Choreographer
+import android.view.SurfaceControl
+import android.view.animation.Animation
+import android.view.animation.Transformation
+import android.window.BackEvent
+import android.window.BackMotionEvent
+import android.window.BackNavigationInfo
+import com.android.internal.R
+import com.android.internal.policy.TransitionAnimation
+import com.android.internal.protolog.common.ProtoLog
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import javax.inject.Inject
+import kotlin.math.max
+import kotlin.math.min
+
+/** Class that handles customized predictive cross activity back animations. */
+@ShellMainThread
+class CustomCrossActivityBackAnimation(
+ context: Context,
+ background: BackAnimationBackground,
+ rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+ transaction: SurfaceControl.Transaction,
+ choreographer: Choreographer,
+ private val customAnimationLoader: CustomAnimationLoader
+) :
+ CrossActivityBackAnimation(
+ context,
+ background,
+ rootTaskDisplayAreaOrganizer,
+ transaction,
+ choreographer
+ ) {
+
+ private var enterAnimation: Animation? = null
+ private var closeAnimation: Animation? = null
+ private val transformation = Transformation()
+
+ override val allowEnteringYShift = false
+
+ @Inject
+ constructor(
+ context: Context,
+ background: BackAnimationBackground,
+ rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+ ) : this(
+ context,
+ background,
+ rootTaskDisplayAreaOrganizer,
+ SurfaceControl.Transaction(),
+ Choreographer.getInstance(),
+ CustomAnimationLoader(
+ TransitionAnimation(context, false /* debug */, "CustomCrossActivityBackAnimation")
+ )
+ )
+
+ override fun preparePreCommitClosingRectMovement(swipeEdge: Int) {
+ startClosingRect.set(backAnimRect)
+
+ // scale closing target to the left for right-hand-swipe and to the right for
+ // left-hand-swipe
+ targetClosingRect.set(startClosingRect)
+ targetClosingRect.scaleCentered(MAX_SCALE)
+ val offset = if (swipeEdge != BackEvent.EDGE_RIGHT) {
+ startClosingRect.right - targetClosingRect.right - displayBoundsMargin
+ } else {
+ -targetClosingRect.left + displayBoundsMargin
+ }
+ targetClosingRect.offset(offset, 0f)
+ }
+
+ override fun preparePreCommitEnteringRectMovement() {
+ // No movement for the entering rect
+ startEnteringRect.set(startClosingRect)
+ targetEnteringRect.set(startClosingRect)
+ }
+
+ override fun getPostCommitAnimationDuration(): Long {
+ return min(
+ MAX_POST_COMMIT_ANIM_DURATION, max(closeAnimation!!.duration, enterAnimation!!.duration)
+ )
+ }
+
+ override fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation {
+ transformation.clear()
+ enterAnimation!!.getTransformationAt(progress * PRE_COMMIT_MAX_PROGRESS, transformation)
+ return transformation
+ }
+
+ override fun startBackAnimation(backMotionEvent: BackMotionEvent) {
+ super.startBackAnimation(backMotionEvent)
+ if (
+ closeAnimation == null ||
+ enterAnimation == null ||
+ closingTarget == null ||
+ enteringTarget == null
+ ) {
+ ProtoLog.d(
+ ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
+ "Enter animation or close animation is null."
+ )
+ return
+ }
+ initializeAnimation(closeAnimation!!, closingTarget!!.localBounds)
+ initializeAnimation(enterAnimation!!, enteringTarget!!.localBounds)
+ }
+
+ override fun onPostCommitProgress(linearProgress: Float) {
+ super.onPostCommitProgress(linearProgress)
+ if (closingTarget == null || enteringTarget == null) return
+
+ val closingProgress = closeAnimation!!.getPostCommitProgress(linearProgress)
+ applyTransform(
+ closingTarget!!.leash,
+ currentClosingRect,
+ closingProgress,
+ closeAnimation!!,
+ FlingMode.FLING_SHRINK
+ )
+ val enteringProgress = MathUtils.lerp(
+ gestureProgress * PRE_COMMIT_MAX_PROGRESS,
+ 1f,
+ enterAnimation!!.getPostCommitProgress(linearProgress)
+ )
+ applyTransform(
+ enteringTarget!!.leash,
+ currentEnteringRect,
+ enteringProgress,
+ enterAnimation!!,
+ FlingMode.NO_FLING
+ )
+ applyTransaction()
+ }
+
+ private fun applyTransform(
+ leash: SurfaceControl,
+ rect: RectF,
+ progress: Float,
+ animation: Animation,
+ flingMode: FlingMode
+ ) {
+ transformation.clear()
+ animation.getTransformationAt(progress, transformation)
+ applyTransform(leash, rect, transformation.alpha, transformation, flingMode)
+ }
+
+ override fun finishAnimation() {
+ closeAnimation?.reset()
+ closeAnimation = null
+ enterAnimation?.reset()
+ enterAnimation = null
+ transformation.clear()
+ super.finishAnimation()
+ }
+
+ /** Load customize animation before animation start. */
+ override fun prepareNextAnimation(
+ animationInfo: BackNavigationInfo.CustomAnimationInfo?,
+ letterboxColor: Int
+ ): Boolean {
+ super.prepareNextAnimation(animationInfo, letterboxColor)
+ if (animationInfo == null) return false
+ customAnimationLoader.loadAll(animationInfo)?.let { result ->
+ closeAnimation = result.closeAnimation
+ enterAnimation = result.enterAnimation
+ customizedBackgroundColor = result.backgroundColor
+ return true
+ }
+ return false
+ }
+
+ private fun Animation.getPostCommitProgress(linearProgress: Float): Float {
+ return when (duration) {
+ 0L -> 1f
+ else -> min(
+ 1f,
+ getPostCommitAnimationDuration() / min(
+ MAX_POST_COMMIT_ANIM_DURATION,
+ duration
+ ).toFloat() * linearProgress
+ )
+ }
+ }
+
+ class AnimationLoadResult {
+ var closeAnimation: Animation? = null
+ var enterAnimation: Animation? = null
+ var backgroundColor = 0
+ }
+
+ companion object {
+ private const val PRE_COMMIT_MAX_PROGRESS = 0.2f
+ private const val MAX_POST_COMMIT_ANIM_DURATION = 2000L
+ }
+}
+
+/** Helper class to load custom animation. */
+class CustomAnimationLoader(private val transitionAnimation: TransitionAnimation) {
+
+ /**
+ * Load both enter and exit animation for the close activity transition. Note that the result is
+ * only valid if the exit animation has set and loaded success. If the entering animation has
+ * not set(i.e. 0), here will load the default entering animation for it.
+ *
+ * @param animationInfo The information of customize animation, which can be set from
+ * [Activity.overrideActivityTransition] and/or [LayoutParams.windowAnimations]
+ */
+ fun loadAll(
+ animationInfo: BackNavigationInfo.CustomAnimationInfo
+ ): CustomCrossActivityBackAnimation.AnimationLoadResult? {
+ if (animationInfo.packageName.isEmpty()) return null
+ val close = loadAnimation(animationInfo, false) ?: return null
+ val open = loadAnimation(animationInfo, true)
+ val result = CustomCrossActivityBackAnimation.AnimationLoadResult()
+ result.closeAnimation = close
+ result.enterAnimation = open
+ result.backgroundColor = animationInfo.customBackground
+ return result
+ }
+
+ /**
+ * Load enter or exit animation from CustomAnimationInfo
+ *
+ * @param animationInfo The information for customize animation.
+ * @param enterAnimation true when load for enter animation, false for exit animation.
+ * @return Loaded animation.
+ */
+ fun loadAnimation(
+ animationInfo: BackNavigationInfo.CustomAnimationInfo,
+ enterAnimation: Boolean
+ ): Animation? {
+ var a: Animation? = null
+ // Activity#overrideActivityTransition has higher priority than windowAnimations
+ // Try to get animation from Activity#overrideActivityTransition
+ if (
+ enterAnimation && animationInfo.customEnterAnim != 0 ||
+ !enterAnimation && animationInfo.customExitAnim != 0
+ ) {
+ a =
+ transitionAnimation.loadAppTransitionAnimation(
+ animationInfo.packageName,
+ if (enterAnimation) animationInfo.customEnterAnim
+ else animationInfo.customExitAnim
+ )
+ } else if (animationInfo.windowAnimations != 0) {
+ // try to get animation from LayoutParams#windowAnimations
+ a =
+ transitionAnimation.loadAnimationAttr(
+ animationInfo.packageName,
+ animationInfo.windowAnimations,
+ if (enterAnimation) R.styleable.WindowAnimation_activityCloseEnterAnimation
+ else R.styleable.WindowAnimation_activityCloseExitAnimation,
+ false /* translucent */
+ )
+ }
+ // Only allow to load default animation for opening target.
+ if (a == null && enterAnimation) {
+ a = loadDefaultOpenAnimation()
+ }
+ if (a != null) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "custom animation loaded %s", a)
+ } else {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "No custom animation loaded")
+ }
+ return a
+ }
+
+ private fun loadDefaultOpenAnimation(): Animation? {
+ return transitionAnimation.loadDefaultAnimationAttr(
+ R.styleable.WindowAnimation_activityCloseEnterAnimation,
+ false /* translucent */
+ )
+ }
+}
+
+private fun initializeAnimation(animation: Animation, bounds: Rect) {
+ val width = bounds.width()
+ val height = bounds.height()
+ animation.initialize(width, height, width, height)
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
deleted file mode 100644
index 5254ff466123..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
+++ /dev/null
@@ -1,429 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.back;
-
-import static android.view.RemoteAnimationTarget.MODE_CLOSING;
-import static android.view.RemoteAnimationTarget.MODE_OPENING;
-
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.Activity;
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.Rect;
-import android.os.RemoteException;
-import android.util.FloatProperty;
-import android.view.Choreographer;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.view.WindowManager.LayoutParams;
-import android.view.animation.Animation;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Transformation;
-import android.window.BackEvent;
-import android.window.BackMotionEvent;
-import android.window.BackNavigationInfo;
-import android.window.BackProgressAnimator;
-import android.window.IOnBackInvokedCallback;
-
-import com.android.internal.R;
-import com.android.internal.dynamicanimation.animation.SpringAnimation;
-import com.android.internal.dynamicanimation.animation.SpringForce;
-import com.android.internal.policy.ScreenDecorationsUtils;
-import com.android.internal.policy.TransitionAnimation;
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-
-import javax.inject.Inject;
-
-/** Class that handle customized close activity transition animation. */
-@ShellMainThread
-public class CustomizeActivityAnimation extends ShellBackAnimation {
- private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
- private final BackAnimationRunner mBackAnimationRunner;
- private final float mCornerRadius;
- private final SurfaceControl.Transaction mTransaction;
- private final BackAnimationBackground mBackground;
- private RemoteAnimationTarget mEnteringTarget;
- private RemoteAnimationTarget mClosingTarget;
- private IRemoteAnimationFinishedCallback mFinishCallback;
- /** Duration of post animation after gesture committed. */
- private static final int POST_ANIMATION_DURATION = 250;
-
- private static final int SCALE_FACTOR = 1000;
- private final SpringAnimation mProgressSpring;
- private float mLatestProgress = 0.0f;
-
- private static final float TARGET_COMMIT_PROGRESS = 0.5f;
-
- private final float[] mTmpFloat9 = new float[9];
- private final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
-
- final CustomAnimationLoader mCustomAnimationLoader;
- private Animation mEnterAnimation;
- private Animation mCloseAnimation;
- private int mNextBackgroundColor;
- final Transformation mTransformation = new Transformation();
-
- private final Choreographer mChoreographer;
-
- @Inject
- public CustomizeActivityAnimation(Context context, BackAnimationBackground background) {
- this(context, background, new SurfaceControl.Transaction(), null);
- }
-
- CustomizeActivityAnimation(Context context, BackAnimationBackground background,
- SurfaceControl.Transaction transaction, Choreographer choreographer) {
- mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
- mBackground = background;
- mBackAnimationRunner = new BackAnimationRunner(
- new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY);
- mCustomAnimationLoader = new CustomAnimationLoader(context);
-
- mProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP);
- mProgressSpring.setSpring(new SpringForce()
- .setStiffness(SpringForce.STIFFNESS_MEDIUM)
- .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
- mTransaction = transaction == null ? new SurfaceControl.Transaction() : transaction;
- mChoreographer = choreographer != null ? choreographer : Choreographer.getInstance();
- }
-
- private float getLatestProgress() {
- return mLatestProgress * SCALE_FACTOR;
- }
- private void setLatestProgress(float value) {
- mLatestProgress = value / SCALE_FACTOR;
- applyTransformTransaction(mLatestProgress);
- }
-
- private static final FloatProperty<CustomizeActivityAnimation> ENTER_PROGRESS_PROP =
- new FloatProperty<>("enter") {
- @Override
- public void setValue(CustomizeActivityAnimation anim, float value) {
- anim.setLatestProgress(value);
- }
-
- @Override
- public Float get(CustomizeActivityAnimation object) {
- return object.getLatestProgress();
- }
- };
-
- // The target will lose focus when alpha == 0, so keep a minimum value for it.
- private static float keepMinimumAlpha(float transAlpha) {
- return Math.max(transAlpha, 0.005f);
- }
-
- private static void initializeAnimation(Animation animation, Rect bounds) {
- final int width = bounds.width();
- final int height = bounds.height();
- animation.initialize(width, height, width, height);
- }
-
- private void startBackAnimation() {
- if (mEnteringTarget == null || mClosingTarget == null
- || mCloseAnimation == null || mEnterAnimation == null) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
- return;
- }
- initializeAnimation(mCloseAnimation, mClosingTarget.localBounds);
- initializeAnimation(mEnterAnimation, mEnteringTarget.localBounds);
-
- // Draw background with task background color.
- if (mEnteringTarget.taskInfo != null && mEnteringTarget.taskInfo.taskDescription != null) {
- mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
- mNextBackgroundColor == Color.TRANSPARENT
- ? mEnteringTarget.taskInfo.taskDescription.getBackgroundColor()
- : mNextBackgroundColor,
- mTransaction);
- }
- }
-
- private void applyTransformTransaction(float progress) {
- if (mClosingTarget == null || mEnteringTarget == null) {
- return;
- }
- applyTransform(mClosingTarget.leash, progress, mCloseAnimation);
- applyTransform(mEnteringTarget.leash, progress, mEnterAnimation);
- mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId());
- mTransaction.apply();
- }
-
- private void applyTransform(SurfaceControl leash, float progress, Animation animation) {
- mTransformation.clear();
- animation.getTransformationAt(progress, mTransformation);
- mTransaction.setMatrix(leash, mTransformation.getMatrix(), mTmpFloat9);
- mTransaction.setAlpha(leash, keepMinimumAlpha(mTransformation.getAlpha()));
- mTransaction.setCornerRadius(leash, mCornerRadius);
- }
-
- void finishAnimation() {
- if (mCloseAnimation != null) {
- mCloseAnimation.reset();
- mCloseAnimation = null;
- }
- if (mEnterAnimation != null) {
- mEnterAnimation.reset();
- mEnterAnimation = null;
- }
- if (mEnteringTarget != null) {
- mEnteringTarget.leash.release();
- mEnteringTarget = null;
- }
- if (mClosingTarget != null) {
- mClosingTarget.leash.release();
- mClosingTarget = null;
- }
- if (mBackground != null) {
- mBackground.removeBackground(mTransaction);
- }
- mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId());
- mTransaction.apply();
- mTransformation.clear();
- mLatestProgress = 0;
- mNextBackgroundColor = Color.TRANSPARENT;
- if (mFinishCallback != null) {
- try {
- mFinishCallback.onAnimationFinished();
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- mFinishCallback = null;
- }
- mProgressSpring.animateToFinalPosition(0);
- mProgressSpring.skipToEnd();
- }
-
- void onGestureProgress(@NonNull BackEvent backEvent) {
- if (mEnteringTarget == null || mClosingTarget == null
- || mCloseAnimation == null || mEnterAnimation == null) {
- return;
- }
-
- final float progress = backEvent.getProgress();
-
- float springProgress = (progress > 0.1f
- ? mapLinear(progress, 0.1f, 1f, TARGET_COMMIT_PROGRESS, 1f)
- : mapLinear(progress, 0, 1f, 0f, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR;
-
- mProgressSpring.animateToFinalPosition(springProgress);
- }
-
- static float mapLinear(float x, float a1, float a2, float b1, float b2) {
- return b1 + (x - a1) * (b2 - b1) / (a2 - a1);
- }
-
- void onGestureCommitted() {
- if (mEnteringTarget == null || mClosingTarget == null
- || mCloseAnimation == null || mEnterAnimation == null) {
- finishAnimation();
- return;
- }
- mProgressSpring.cancel();
-
- // Enter phase 2 of the animation
- final ValueAnimator valueAnimator = ValueAnimator.ofFloat(mLatestProgress, 1f)
- .setDuration(POST_ANIMATION_DURATION);
- valueAnimator.setInterpolator(mDecelerateInterpolator);
- valueAnimator.addUpdateListener(animation -> {
- float progress = (float) animation.getAnimatedValue();
- applyTransformTransaction(progress);
- });
-
- valueAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- finishAnimation();
- }
- });
- valueAnimator.start();
- }
-
- /** Load customize animation before animation start. */
- @Override
- public boolean prepareNextAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo) {
- if (animationInfo == null) {
- return false;
- }
- final AnimationLoadResult result = mCustomAnimationLoader.loadAll(animationInfo);
- if (result != null) {
- mCloseAnimation = result.mCloseAnimation;
- mEnterAnimation = result.mEnterAnimation;
- mNextBackgroundColor = result.mBackgroundColor;
- return true;
- }
- return false;
- }
-
- @Override
- public BackAnimationRunner getRunner() {
- return mBackAnimationRunner;
- }
-
- private final class Callback extends IOnBackInvokedCallback.Default {
- @Override
- public void onBackStarted(BackMotionEvent backEvent) {
- mProgressAnimator.onBackStarted(backEvent,
- CustomizeActivityAnimation.this::onGestureProgress);
- }
-
- @Override
- public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
- mProgressAnimator.onBackProgressed(backEvent);
- }
-
- @Override
- public void onBackCancelled() {
- mProgressAnimator.onBackCancelled(CustomizeActivityAnimation.this::finishAnimation);
- }
-
- @Override
- public void onBackInvoked() {
- mProgressAnimator.reset();
- onGestureCommitted();
- }
- }
-
- private final class Runner extends IRemoteAnimationRunner.Default {
- @Override
- public void onAnimationStart(
- int transit,
- RemoteAnimationTarget[] apps,
- RemoteAnimationTarget[] wallpapers,
- RemoteAnimationTarget[] nonApps,
- IRemoteAnimationFinishedCallback finishedCallback) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to customize animation.");
- for (RemoteAnimationTarget a : apps) {
- if (a.mode == MODE_CLOSING) {
- mClosingTarget = a;
- }
- if (a.mode == MODE_OPENING) {
- mEnteringTarget = a;
- }
- }
- if (mCloseAnimation == null || mEnterAnimation == null) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW,
- "No animation loaded, should choose cross-activity animation?");
- }
-
- startBackAnimation();
- mFinishCallback = finishedCallback;
- }
-
- @Override
- public void onAnimationCancelled() {
- finishAnimation();
- }
- }
-
-
- static final class AnimationLoadResult {
- Animation mCloseAnimation;
- Animation mEnterAnimation;
- int mBackgroundColor;
- }
-
- /**
- * Helper class to load custom animation.
- */
- static class CustomAnimationLoader {
- final TransitionAnimation mTransitionAnimation;
-
- CustomAnimationLoader(Context context) {
- mTransitionAnimation = new TransitionAnimation(
- context, false /* debug */, "CustomizeBackAnimation");
- }
-
- /**
- * Load both enter and exit animation for the close activity transition.
- * Note that the result is only valid if the exit animation has set and loaded success.
- * If the entering animation has not set(i.e. 0), here will load the default entering
- * animation for it.
- *
- * @param animationInfo The information of customize animation, which can be set from
- * {@link Activity#overrideActivityTransition} and/or
- * {@link LayoutParams#windowAnimations}
- */
- AnimationLoadResult loadAll(BackNavigationInfo.CustomAnimationInfo animationInfo) {
- if (animationInfo.getPackageName().isEmpty()) {
- return null;
- }
- final Animation close = loadAnimation(animationInfo, false);
- if (close == null) {
- return null;
- }
- final Animation open = loadAnimation(animationInfo, true);
- AnimationLoadResult result = new AnimationLoadResult();
- result.mCloseAnimation = close;
- result.mEnterAnimation = open;
- result.mBackgroundColor = animationInfo.getCustomBackground();
- return result;
- }
-
- /**
- * Load enter or exit animation from CustomAnimationInfo
- * @param animationInfo The information for customize animation.
- * @param enterAnimation true when load for enter animation, false for exit animation.
- * @return Loaded animation.
- */
- @Nullable
- Animation loadAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo,
- boolean enterAnimation) {
- Animation a = null;
- // Activity#overrideActivityTransition has higher priority than windowAnimations
- // Try to get animation from Activity#overrideActivityTransition
- if ((enterAnimation && animationInfo.getCustomEnterAnim() != 0)
- || (!enterAnimation && animationInfo.getCustomExitAnim() != 0)) {
- a = mTransitionAnimation.loadAppTransitionAnimation(
- animationInfo.getPackageName(),
- enterAnimation ? animationInfo.getCustomEnterAnim()
- : animationInfo.getCustomExitAnim());
- } else if (animationInfo.getWindowAnimations() != 0) {
- // try to get animation from LayoutParams#windowAnimations
- a = mTransitionAnimation.loadAnimationAttr(animationInfo.getPackageName(),
- animationInfo.getWindowAnimations(), enterAnimation
- ? R.styleable.WindowAnimation_activityCloseEnterAnimation
- : R.styleable.WindowAnimation_activityCloseExitAnimation,
- false /* translucent */);
- }
- // Only allow to load default animation for opening target.
- if (a == null && enterAnimation) {
- a = loadDefaultOpenAnimation();
- }
- if (a != null) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "custom animation loaded %s", a);
- } else {
- ProtoLog.e(WM_SHELL_BACK_PREVIEW, "No custom animation loaded");
- }
- return a;
- }
-
- private Animation loadDefaultOpenAnimation() {
- return mTransitionAnimation.loadDefaultAnimationAttr(
- R.styleable.WindowAnimation_activityCloseEnterAnimation,
- false /* translucent */);
- }
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt
new file mode 100644
index 000000000000..3b5eb3613d2a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.back
+
+import android.content.Context
+import android.view.Choreographer
+import android.view.SurfaceControl
+import android.window.BackEvent
+import com.android.wm.shell.R
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.animation.Interpolators
+import com.android.wm.shell.shared.annotations.ShellMainThread
+import javax.inject.Inject
+import kotlin.math.max
+
+/** Class that defines cross-activity animation. */
+@ShellMainThread
+class DefaultCrossActivityBackAnimation
+@Inject
+constructor(
+ context: Context,
+ background: BackAnimationBackground,
+ rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+) :
+ CrossActivityBackAnimation(
+ context,
+ background,
+ rootTaskDisplayAreaOrganizer,
+ SurfaceControl.Transaction(),
+ Choreographer.getInstance()
+ ) {
+
+ private val postCommitInterpolator = Interpolators.EMPHASIZED
+ private val enteringStartOffset =
+ context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset)
+ override val allowEnteringYShift = true
+
+ override fun preparePreCommitClosingRectMovement(swipeEdge: Int) {
+ startClosingRect.set(backAnimRect)
+
+ // scale closing target into the middle for rhs and to the right for lhs
+ targetClosingRect.set(startClosingRect)
+ targetClosingRect.scaleCentered(MAX_SCALE)
+ if (swipeEdge != BackEvent.EDGE_RIGHT) {
+ targetClosingRect.offset(
+ startClosingRect.right - targetClosingRect.right - displayBoundsMargin,
+ 0f
+ )
+ }
+ }
+
+ override fun preparePreCommitEnteringRectMovement() {
+ // the entering target starts 96dp to the left of the screen edge...
+ startEnteringRect.set(startClosingRect)
+ startEnteringRect.offset(-enteringStartOffset, 0f)
+ // ...and gets scaled in sync with the closing target
+ targetEnteringRect.set(startEnteringRect)
+ targetEnteringRect.scaleCentered(MAX_SCALE)
+ }
+
+ override fun getPostCommitAnimationDuration() = POST_COMMIT_DURATION
+
+ override fun onGestureCommitted(velocity: Float) {
+ // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
+ // coordinate of the gesture driven phase. Let's update the start and target rects and kick
+ // off the animator in the superclass
+ startClosingRect.set(currentClosingRect)
+ startEnteringRect.set(currentEnteringRect)
+ targetEnteringRect.set(backAnimRect)
+ targetClosingRect.set(backAnimRect)
+ targetClosingRect.offset(currentClosingRect.left + enteringStartOffset, 0f)
+ super.onGestureCommitted(velocity)
+ }
+
+ override fun onPostCommitProgress(linearProgress: Float) {
+ super.onPostCommitProgress(linearProgress)
+ val closingAlpha = max(1f - linearProgress * 5, 0f)
+ val progress = postCommitInterpolator.getInterpolation(linearProgress)
+ currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
+ applyTransform(
+ closingTarget?.leash,
+ currentClosingRect,
+ closingAlpha,
+ flingMode = FlingMode.FLING_BOUNCE
+ )
+ currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
+ applyTransform(
+ enteringTarget?.leash,
+ currentEnteringRect,
+ 1f,
+ flingMode = FlingMode.FLING_BOUNCE
+ )
+ applyTransaction()
+ }
+
+
+ companion object {
+ private const val POST_COMMIT_DURATION = 450L
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimation.java
index dc659197848e..9cd193b0f74c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimation.java
@@ -16,6 +16,7 @@
package com.android.wm.shell.back;
+import android.content.res.Configuration;
import android.window.BackNavigationInfo;
import javax.inject.Qualifier;
@@ -41,11 +42,16 @@ public abstract class ShellBackAnimation {
public abstract BackAnimationRunner getRunner();
/**
- * Prepare the next animation with customized animation.
+ * Prepare the next animation.
*
* @return true if this type of back animation should override the default.
*/
- public boolean prepareNextAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo) {
+ public boolean prepareNextAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo,
+ int letterboxColor) {
return false;
}
+
+ void onConfigurationChanged(Configuration newConfig) {
+
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java
index 26d20972c751..6fafa75e2f70 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java
@@ -18,6 +18,7 @@ package com.android.wm.shell.back;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.content.res.Configuration;
import android.util.Log;
import android.util.SparseArray;
import android.window.BackNavigationInfo;
@@ -27,8 +28,9 @@ public class ShellBackAnimationRegistry {
private static final String TAG = "ShellBackPreview";
private final SparseArray<BackAnimationRunner> mAnimationDefinition = new SparseArray<>();
- private final ShellBackAnimation mDefaultCrossActivityAnimation;
+ private ShellBackAnimation mDefaultCrossActivityAnimation;
private final ShellBackAnimation mCustomizeActivityAnimation;
+ private final ShellBackAnimation mCrossTaskAnimation;
public ShellBackAnimationRegistry(
@ShellBackAnimation.CrossActivity @Nullable ShellBackAnimation crossActivityAnimation,
@@ -57,6 +59,7 @@ public class ShellBackAnimationRegistry {
mDefaultCrossActivityAnimation = crossActivityAnimation;
mCustomizeActivityAnimation = customizeActivityAnimation;
+ mCrossTaskAnimation = crossTaskAnimation;
// TODO(b/236760237): register dialog close animation when it's completed.
}
@@ -64,10 +67,18 @@ public class ShellBackAnimationRegistry {
void registerAnimation(
@BackNavigationInfo.BackTargetType int type, @NonNull BackAnimationRunner runner) {
mAnimationDefinition.set(type, runner);
+ // Only happen in test
+ if (BackNavigationInfo.TYPE_CROSS_ACTIVITY == type) {
+ mDefaultCrossActivityAnimation = null;
+ }
}
void unregisterAnimation(@BackNavigationInfo.BackTargetType int type) {
mAnimationDefinition.remove(type);
+ // Only happen in test
+ if (BackNavigationInfo.TYPE_CROSS_ACTIVITY == type) {
+ mDefaultCrossActivityAnimation = null;
+ }
}
/**
@@ -125,17 +136,32 @@ public class ShellBackAnimationRegistry {
BackNavigationInfo.TYPE_CROSS_ACTIVITY, mDefaultCrossActivityAnimation.getRunner());
}
+ void onConfigurationChanged(Configuration newConfig) {
+ if (mCustomizeActivityAnimation != null) {
+ mCustomizeActivityAnimation.onConfigurationChanged(newConfig);
+ }
+ if (mDefaultCrossActivityAnimation != null) {
+ mDefaultCrossActivityAnimation.onConfigurationChanged(newConfig);
+ }
+ if (mCrossTaskAnimation != null) {
+ mCrossTaskAnimation.onConfigurationChanged(newConfig);
+ }
+ }
+
BackAnimationRunner getAnimationRunnerAndInit(BackNavigationInfo backNavigationInfo) {
int type = backNavigationInfo.getType();
// Initiate customized cross-activity animation, or fall back to cross activity animation
if (type == BackNavigationInfo.TYPE_CROSS_ACTIVITY && mAnimationDefinition.contains(type)) {
if (mCustomizeActivityAnimation != null
&& mCustomizeActivityAnimation.prepareNextAnimation(
- backNavigationInfo.getCustomAnimationInfo())) {
+ backNavigationInfo.getCustomAnimationInfo(), 0)) {
mAnimationDefinition.get(type).resetWaitingAnimation();
mAnimationDefinition.set(
BackNavigationInfo.TYPE_CROSS_ACTIVITY,
mCustomizeActivityAnimation.getRunner());
+ } else if (mDefaultCrossActivityAnimation != null) {
+ mDefaultCrossActivityAnimation.prepareNextAnimation(null,
+ backNavigationInfo.getLetterboxColor());
}
}
BackAnimationRunner runner = mAnimationDefinition.get(type);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
deleted file mode 100644
index 8f04f126960c..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java
+++ /dev/null
@@ -1,240 +0,0 @@
-/*
- * Copyright (C) 2022 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.back;
-
-import android.annotation.FloatRange;
-import android.os.SystemProperties;
-import android.util.MathUtils;
-import android.view.MotionEvent;
-import android.view.RemoteAnimationTarget;
-import android.window.BackEvent;
-import android.window.BackMotionEvent;
-
-import java.io.PrintWriter;
-
-/**
- * Helper class to record the touch location for gesture and generate back events.
- */
-class TouchTracker {
- private static final String PREDICTIVE_BACK_LINEAR_DISTANCE_PROP =
- "persist.wm.debug.predictive_back_linear_distance";
- private static final int LINEAR_DISTANCE = SystemProperties
- .getInt(PREDICTIVE_BACK_LINEAR_DISTANCE_PROP, -1);
- private float mLinearDistance = LINEAR_DISTANCE;
- private float mMaxDistance;
- private float mNonLinearFactor;
- /**
- * Location of the latest touch event
- */
- private float mLatestTouchX;
- private float mLatestTouchY;
- private boolean mTriggerBack;
-
- /**
- * Location of the initial touch event of the back gesture.
- */
- private float mInitTouchX;
- private float mInitTouchY;
- private float mLatestVelocityX;
- private float mLatestVelocityY;
- private float mStartThresholdX;
- private int mSwipeEdge;
- private TouchTrackerState mState = TouchTrackerState.INITIAL;
-
- void update(float touchX, float touchY, float velocityX, float velocityY) {
- /**
- * If back was previously cancelled but the user has started swiping in the forward
- * direction again, restart back.
- */
- if ((touchX < mStartThresholdX && mSwipeEdge == BackEvent.EDGE_LEFT)
- || (touchX > mStartThresholdX && mSwipeEdge == BackEvent.EDGE_RIGHT)) {
- mStartThresholdX = touchX;
- if ((mSwipeEdge == BackEvent.EDGE_LEFT && mStartThresholdX < mInitTouchX)
- || (mSwipeEdge == BackEvent.EDGE_RIGHT && mStartThresholdX > mInitTouchX)) {
- mInitTouchX = mStartThresholdX;
- }
- }
- mLatestTouchX = touchX;
- mLatestTouchY = touchY;
- mLatestVelocityX = velocityX;
- mLatestVelocityY = velocityY;
- }
-
- void setTriggerBack(boolean triggerBack) {
- if (mTriggerBack != triggerBack && !triggerBack) {
- mStartThresholdX = mLatestTouchX;
- }
- mTriggerBack = triggerBack;
- }
-
- boolean getTriggerBack() {
- return mTriggerBack;
- }
-
- void setState(TouchTrackerState state) {
- mState = state;
- }
-
- boolean isInInitialState() {
- return mState == TouchTrackerState.INITIAL;
- }
-
- boolean isActive() {
- return mState == TouchTrackerState.ACTIVE;
- }
-
- boolean isFinished() {
- return mState == TouchTrackerState.FINISHED;
- }
-
- void setGestureStartLocation(float touchX, float touchY, int swipeEdge) {
- mInitTouchX = touchX;
- mInitTouchY = touchY;
- mLatestTouchX = touchX;
- mLatestTouchY = touchY;
- mSwipeEdge = swipeEdge;
- mStartThresholdX = mInitTouchX;
- }
-
- /** Update the start location used to compute the progress
- * to the latest touch location.
- */
- void updateStartLocation() {
- mInitTouchX = mLatestTouchX;
- mInitTouchY = mLatestTouchY;
- mStartThresholdX = mInitTouchX;
- }
-
- void reset() {
- mInitTouchX = 0;
- mInitTouchY = 0;
- mStartThresholdX = 0;
- mTriggerBack = false;
- mState = TouchTrackerState.INITIAL;
- mSwipeEdge = BackEvent.EDGE_LEFT;
- }
-
- BackMotionEvent createStartEvent(RemoteAnimationTarget target) {
- return new BackMotionEvent(
- /* touchX = */ mInitTouchX,
- /* touchY = */ mInitTouchY,
- /* progress = */ 0,
- /* velocityX = */ 0,
- /* velocityY = */ 0,
- /* triggerBack = */ mTriggerBack,
- /* swipeEdge = */ mSwipeEdge,
- /* departingAnimationTarget = */ target);
- }
-
- BackMotionEvent createProgressEvent() {
- float progress = getProgress(mLatestTouchX);
- return createProgressEvent(progress);
- }
-
- /**
- * Progress value computed from the touch position.
- *
- * @param touchX the X touch position of the {@link MotionEvent}.
- * @return progress value
- */
- @FloatRange(from = 0.0, to = 1.0)
- float getProgress(float touchX) {
- // If back is committed, progress is the distance between the last and first touch
- // point, divided by the max drag distance. Otherwise, it's the distance between
- // the last touch point and the starting threshold, divided by max drag distance.
- // The starting threshold is initially the first touch location, and updated to
- // the location everytime back is restarted after being cancelled.
- float startX = mTriggerBack ? mInitTouchX : mStartThresholdX;
- float distance;
- if (mSwipeEdge == BackEvent.EDGE_LEFT) {
- distance = touchX - startX;
- } else {
- distance = startX - touchX;
- }
- float deltaX = Math.max(0f, distance);
- float linearDistance = mLinearDistance;
- float maxDistance = getMaxDistance();
- maxDistance = maxDistance == 0 ? 1 : maxDistance;
- float progress;
- if (linearDistance < maxDistance) {
- // Up to linearDistance it behaves linearly, then slowly reaches 1f.
-
- // maxDistance is composed of linearDistance + nonLinearDistance
- float nonLinearDistance = maxDistance - linearDistance;
- float initialTarget = linearDistance + nonLinearDistance * mNonLinearFactor;
-
- boolean isLinear = deltaX <= linearDistance;
- if (isLinear) {
- progress = deltaX / initialTarget;
- } else {
- float nonLinearDeltaX = deltaX - linearDistance;
- float nonLinearProgress = nonLinearDeltaX / nonLinearDistance;
- float currentTarget = MathUtils.lerp(
- /* start = */ initialTarget,
- /* stop = */ maxDistance,
- /* amount = */ nonLinearProgress);
- progress = deltaX / currentTarget;
- }
- } else {
- // Always linear behavior.
- progress = deltaX / maxDistance;
- }
- return MathUtils.constrain(progress, 0, 1);
- }
-
- /**
- * Maximum distance in pixels.
- * Progress is considered to be completed (1f) when this limit is exceeded.
- */
- float getMaxDistance() {
- return mMaxDistance;
- }
-
- BackMotionEvent createProgressEvent(float progress) {
- return new BackMotionEvent(
- /* touchX = */ mLatestTouchX,
- /* touchY = */ mLatestTouchY,
- /* progress = */ progress,
- /* velocityX = */ mLatestVelocityX,
- /* velocityY = */ mLatestVelocityY,
- /* triggerBack = */ mTriggerBack,
- /* swipeEdge = */ mSwipeEdge,
- /* departingAnimationTarget = */ null);
- }
-
- public void setProgressThresholds(float linearDistance, float maxDistance,
- float nonLinearFactor) {
- if (LINEAR_DISTANCE >= 0) {
- mLinearDistance = LINEAR_DISTANCE;
- } else {
- mLinearDistance = linearDistance;
- }
- mMaxDistance = maxDistance;
- mNonLinearFactor = nonLinearFactor;
- }
-
- void dump(PrintWriter pw, String prefix) {
- pw.println(prefix + "TouchTracker state:");
- pw.println(prefix + " mState=" + mState);
- pw.println(prefix + " mTriggerBack=" + mTriggerBack);
- }
-
- enum TouchTrackerState {
- INITIAL, ACTIVE, FINISHED
- }
-
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
index a67821b7e819..f9a1d940c734 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
@@ -82,8 +82,8 @@ public class BadgedImageView extends ConstraintLayout {
private BubbleViewProvider mBubble;
private BubblePositioner mPositioner;
- private boolean mOnLeft;
-
+ private boolean mBadgeOnLeft;
+ private boolean mDotOnLeft;
private DotRenderer mDotRenderer;
private DotRenderer.DrawParams mDrawParams;
private int mDotColor;
@@ -153,7 +153,8 @@ public class BadgedImageView extends ConstraintLayout {
public void hideDotAndBadge(boolean onLeft) {
addDotSuppressionFlag(BadgedImageView.SuppressionFlag.BEHIND_STACK);
- mOnLeft = onLeft;
+ mBadgeOnLeft = onLeft;
+ mDotOnLeft = onLeft;
hideBadge();
}
@@ -185,7 +186,7 @@ public class BadgedImageView extends ConstraintLayout {
mDrawParams.dotColor = mDotColor;
mDrawParams.iconBounds = mTempBounds;
- mDrawParams.leftAlign = mOnLeft;
+ mDrawParams.leftAlign = mDotOnLeft;
mDrawParams.scale = mDotScale;
mDotRenderer.draw(canvas, mDrawParams);
@@ -255,7 +256,7 @@ public class BadgedImageView extends ConstraintLayout {
* Whether decorations (badges or dots) are on the left.
*/
boolean getDotOnLeft() {
- return mOnLeft;
+ return mDotOnLeft;
}
/**
@@ -263,7 +264,7 @@ public class BadgedImageView extends ConstraintLayout {
*/
float[] getDotCenter() {
float[] dotPosition;
- if (mOnLeft) {
+ if (mDotOnLeft) {
dotPosition = mDotRenderer.getLeftDotPosition();
} else {
dotPosition = mDotRenderer.getRightDotPosition();
@@ -288,22 +289,26 @@ public class BadgedImageView extends ConstraintLayout {
/** Sets the position of the dot and badge, animating them out and back in if requested. */
void animateDotBadgePositions(boolean onLeft) {
- mOnLeft = onLeft;
-
- if (onLeft != getDotOnLeft() && shouldDrawDot()) {
- animateDotScale(0f /* showDot */, () -> {
- invalidate();
- animateDotScale(1.0f, null /* after */);
- });
+ if (onLeft != getDotOnLeft()) {
+ if (shouldDrawDot()) {
+ animateDotScale(0f /* showDot */, () -> {
+ mDotOnLeft = onLeft;
+ invalidate();
+ animateDotScale(1.0f, null /* after */);
+ });
+ } else {
+ mDotOnLeft = onLeft;
+ }
}
+ mBadgeOnLeft = onLeft;
// TODO animate badge
showBadge();
-
}
/** Sets the position of the dot and badge. */
void setDotBadgeOnLeft(boolean onLeft) {
- mOnLeft = onLeft;
+ mBadgeOnLeft = onLeft;
+ mDotOnLeft = onLeft;
invalidate();
showBadge();
}
@@ -358,7 +363,7 @@ public class BadgedImageView extends ConstraintLayout {
}
int translationX;
- if (mOnLeft) {
+ if (mBadgeOnLeft) {
translationX = -(mBubble.getBubbleIcon().getWidth() - appBadgeBitmap.getWidth());
} else {
translationX = 0;
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..1279fc42c066 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());
}
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 96aaf02cb5e3..d2c36e6b637c 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,12 +86,15 @@ 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;
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;
@@ -101,13 +104,14 @@ import com.android.wm.shell.common.SingleInstanceRemoteListener;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TaskStackListenerCallback;
import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.common.annotations.ShellBackgroundThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
import com.android.wm.shell.pip.PinnedStackListenerForwarder;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
@@ -169,6 +173,8 @@ public class BubbleController implements ConfigurationChangeListener,
* the pointer might need to be updated.
*/
void bubbleOrderChanged(List<Bubble> bubbleOrder, boolean updatePointer);
+ /** Called when the bubble overflow empty state changes, used to show/hide the overflow. */
+ void bubbleOverflowChanged(boolean hasBubbles);
}
private final Context mContext;
@@ -454,8 +460,7 @@ public class BubbleController implements ConfigurationChangeListener,
ProtoLog.d(WM_SHELL_BUBBLES,
"onActivityRestartAttempt - taskId=%d selecting matching bubble=%s",
task.taskId, b.getKey());
- mBubbleData.setSelectedBubble(b);
- mBubbleData.setExpanded(true);
+ mBubbleData.setSelectedBubbleAndExpandStack(b);
return;
}
}
@@ -474,11 +479,16 @@ public class BubbleController implements ConfigurationChangeListener,
mDisplayController.addDisplayChangingController(
(displayId, fromRotation, toRotation, newDisplayAreaInfo, t) -> {
- // This is triggered right before the rotation is applied
- if (fromRotation != toRotation) {
+ Rect newScreenBounds = new Rect();
+ if (newDisplayAreaInfo != null) {
+ newScreenBounds =
+ newDisplayAreaInfo.configuration.windowConfiguration.getBounds();
+ }
+ // This is triggered right before the rotation or new screen size is applied
+ if (fromRotation != toRotation || !newScreenBounds.equals(mScreenBounds)) {
if (mStackView != null) {
// Layout listener set on stackView will update the positioner
- // once the rotation is applied
+ // once the rotation or screen change is applied
mStackView.onOrientationChanged();
}
}
@@ -503,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);
@@ -510,7 +524,7 @@ public class BubbleController implements ConfigurationChangeListener,
}
private ExternalInterfaceBinder createExternalInterface() {
- return new BubbleController.IBubblesImpl(this);
+ return new IBubblesImpl(this);
}
@VisibleForTesting
@@ -584,21 +598,15 @@ 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);
}
}
- private void openBubbleOverflow() {
- ensureBubbleViewsAndWindowCreated();
- mBubbleData.setShowingOverflow(true);
- mBubbleData.setSelectedBubble(mBubbleData.getOverflow());
- mBubbleData.setExpanded(true);
- }
-
/**
* Called when the status bar has become visible or invisible (either permanently or
* temporarily).
@@ -708,6 +716,41 @@ public class BubbleController implements ConfigurationChangeListener,
return mBubbleProperties.isBubbleBarEnabled() && mBubblePositioner.isLargeScreen();
}
+ /**
+ * Returns current {@link BubbleBarLocation} if bubble bar is being used.
+ * Otherwise returns <code>null</code>
+ */
+ @Nullable
+ public BubbleBarLocation getBubbleBarLocation() {
+ if (canShowAsBubbleBar()) {
+ return mBubblePositioner.getBubbleBarLocation();
+ }
+ return null;
+ }
+
+ /**
+ * Update bubble bar location and trigger and update to listeners
+ */
+ public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
+ if (canShowAsBubbleBar()) {
+ mBubblePositioner.setBubbleBarLocation(bubbleBarLocation);
+ BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate();
+ bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation;
+ mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate);
+ }
+ }
+
+ /**
+ * Animate bubble bar to the given location. The location change is transient. It does not
+ * update the state of the bubble bar.
+ * To update bubble bar pinned location, use {@link #setBubbleBarLocation(BubbleBarLocation)}.
+ */
+ public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
+ if (canShowAsBubbleBar()) {
+ mBubbleStateListener.animateBubbleBarLocation(bubbleBarLocation);
+ }
+ }
+
/** Whether this userId belongs to the current user. */
private boolean isCurrentProfile(int userId) {
return userId == UserHandle.USER_ALL
@@ -950,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.
@@ -1129,16 +1191,52 @@ public class BubbleController implements ConfigurationChangeListener,
}
/**
- * Update expanded state when a single bubble is dragged in Launcher.
+ * A bubble is being dragged in Launcher.
* Will be called only when bubble bar is expanded.
- * @param bubbleKey key of the bubble to collapse/expand
- * @param isBeingDragged whether the bubble is being dragged
+ *
+ * @param bubbleKey key of the bubble being dragged
*/
- public void onBubbleDrag(String bubbleKey, boolean isBeingDragged) {
- if (mBubbleData.getSelectedBubble() != null
- && mBubbleData.getSelectedBubble().getKey().equals(bubbleKey)) {
- // Should collapse/expand only if equals to selected bubble.
- mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ !isBeingDragged);
+ public void startBubbleDrag(String bubbleKey) {
+ if (mBubbleData.getSelectedBubble() != null) {
+ mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ false);
+ }
+ if (mBubbleStateListener != null) {
+ boolean overflow = BubbleOverflow.KEY.equals(bubbleKey);
+ Rect rect = new Rect();
+ mBubblePositioner.getBubbleBarExpandedViewBounds(mBubblePositioner.isBubbleBarOnLeft(),
+ overflow, rect);
+ BubbleBarUpdate update = new BubbleBarUpdate();
+ update.expandedViewDropTargetSize = new Point(rect.width(), rect.height());
+ mBubbleStateListener.onBubbleStateChange(update);
+ }
+ }
+
+ /**
+ * A bubble is no longer being dragged in Launcher. And was released in given location.
+ * Will be called only when bubble bar is expanded.
+ *
+ * @param location location where bubble was released
+ * @param topOnScreen top coordinate of the bubble bar on the screen after release
+ */
+ public void stopBubbleDrag(BubbleBarLocation location, int topOnScreen) {
+ mBubblePositioner.setBubbleBarLocation(location);
+ mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen);
+ if (mBubbleData.getSelectedBubble() != null) {
+ mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ true);
+ }
+ }
+
+ /**
+ * A bubble was dragged and is released in dismiss target in Launcher.
+ *
+ * @param bubbleKey key of the bubble being dragged to dismiss target
+ */
+ public void dragBubbleToDismiss(String bubbleKey) {
+ String selectedBubbleKey = mBubbleData.getSelectedBubbleKey();
+ removeBubble(bubbleKey, Bubbles.DISMISS_USER_GESTURE);
+ if (selectedBubbleKey != null && !selectedBubbleKey.equals(bubbleKey)) {
+ // We did not remove the selected bubble. Expand it again
+ mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ true);
}
}
@@ -1178,8 +1276,8 @@ public class BubbleController implements ConfigurationChangeListener,
* <p>This is used by external callers (launcher).
*/
@VisibleForTesting
- public void expandStackAndSelectBubbleFromLauncher(String key, Rect bubbleBarBounds) {
- mBubblePositioner.setBubbleBarPosition(bubbleBarBounds);
+ public void expandStackAndSelectBubbleFromLauncher(String key, int topOnScreen) {
+ mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen);
if (BubbleOverflow.KEY.equals(key)) {
mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow());
@@ -1222,8 +1320,7 @@ public class BubbleController implements ConfigurationChangeListener,
}
if (mBubbleData.hasBubbleInStackWithKey(b.getKey())) {
// already in the stack
- mBubbleData.setSelectedBubble(b);
- mBubbleData.setExpanded(true);
+ mBubbleData.setSelectedBubbleAndExpandStack(b);
} else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) {
// promote it out of the overflow
promoteBubbleFromOverflow(b);
@@ -1234,20 +1331,21 @@ public class BubbleController implements ConfigurationChangeListener,
* Expands and selects a bubble based on the provided {@link BubbleEntry}. If no bubble
* exists for this entry, and it is able to bubble, a new bubble will be created.
*
- * This is the method to use when opening a bubble via a notification or in a state where
+ * <p>This is the method to use when opening a bubble via a notification or in a state where
* the device might not be unlocked.
*
* @param entry the entry to use for the bubble.
*/
public void expandStackAndSelectBubble(BubbleEntry entry) {
+ ProtoLog.d(WM_SHELL_BUBBLES, "opening bubble from notification key=%s mIsStatusBarShade=%b",
+ entry.getKey(), mIsStatusBarShade);
if (mIsStatusBarShade) {
mNotifEntryToExpandOnShadeUnlock = null;
String key = entry.getKey();
Bubble bubble = mBubbleData.getBubbleInStackWithKey(key);
if (bubble != null) {
- mBubbleData.setSelectedBubble(bubble);
- mBubbleData.setExpanded(true);
+ mBubbleData.setSelectedBubbleAndExpandStack(bubble);
} else {
bubble = mBubbleData.getOverflowBubbleWithKey(key);
if (bubble != null) {
@@ -1318,43 +1416,45 @@ public class BubbleController implements ConfigurationChangeListener,
}
String appBubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), user);
- Log.i(TAG, "showOrHideAppBubble, key= " + appBubbleKey + " stackVisibility= "
- + (mStackView != null ? mStackView.getVisibility() : " null ")
- + " statusBarShade=" + mIsStatusBarShade);
PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier());
- if (!isResizableActivity(intent, packageManager, appBubbleKey)) return;
+ if (!isResizableActivity(intent, packageManager, appBubbleKey)) return; // logs errors
Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(appBubbleKey);
+ ProtoLog.d(WM_SHELL_BUBBLES,
+ "showOrHideAppBubble, key=%s existingAppBubble=%s stackVisibility=%s "
+ + "statusBarShade=%s",
+ appBubbleKey, existingAppBubble,
+ (mStackView != null ? mStackView.getVisibility() : "null"),
+ mIsStatusBarShade);
+
if (existingAppBubble != null) {
BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble();
if (isStackExpanded()) {
if (selectedBubble != null && appBubbleKey.equals(selectedBubble.getKey())) {
+ ProtoLog.d(WM_SHELL_BUBBLES, "collapseStack for %s", appBubbleKey);
// App bubble is expanded, lets collapse
- Log.i(TAG, " showOrHideAppBubble, selected bubble is app bubble, collapsing");
collapseStack();
} else {
+ ProtoLog.d(WM_SHELL_BUBBLES, "setSelected for %s", appBubbleKey);
// App bubble is not selected, select it
- Log.i(TAG, " showOrHideAppBubble, expanded, selecting existing app bubble");
mBubbleData.setSelectedBubble(existingAppBubble);
}
} else {
+ ProtoLog.d(WM_SHELL_BUBBLES, "setSelectedBubbleAndExpandStack %s", appBubbleKey);
// App bubble is not selected, select it & expand
- Log.i(TAG, " showOrHideAppBubble, expand and select existing app bubble");
- mBubbleData.setSelectedBubble(existingAppBubble);
- mBubbleData.setExpanded(true);
+ mBubbleData.setSelectedBubbleAndExpandStack(existingAppBubble);
}
} else {
// Check if it exists in the overflow
Bubble b = mBubbleData.getOverflowBubbleWithKey(appBubbleKey);
if (b != null) {
// It's in the overflow, so remove it & reinflate
- Log.i(TAG, " showOrHideAppBubble, expanding app bubble from overflow");
- mBubbleData.removeOverflowBubble(b);
+ mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL);
} else {
// App bubble does not exist, lets add and expand it
- Log.i(TAG, " showOrHideAppBubble, creating and expanding app bubble");
b = Bubble.createAppBubble(intent, user, icon, mMainExecutor);
}
+ ProtoLog.d(WM_SHELL_BUBBLES, "inflateAndAdd %s", appBubbleKey);
b.setShouldAutoExpand(true);
inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
}
@@ -1383,8 +1483,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) {
@@ -1476,6 +1577,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);
@@ -1698,7 +1805,7 @@ public class BubbleController implements ConfigurationChangeListener,
if (groupKey == null) {
return bubbleChildren;
}
- for (Bubble bubble : mBubbleData.getActiveBubbles()) {
+ for (Bubble bubble : mBubbleData.getBubbles()) {
if (bubble.getGroupKey() != null && groupKey.equals(bubble.getGroupKey())) {
bubbleChildren.add(bubble);
}
@@ -1792,6 +1899,15 @@ public class BubbleController implements ConfigurationChangeListener,
}
}
+
+ @Override
+ public void bubbleOverflowChanged(boolean hasBubbles) {
+ if (Flags.enableOptionalBubbleOverflow()) {
+ if (mStackView != null) {
+ mStackView.showOverflow(hasBubbles);
+ }
+ }
+ }
};
/** When bubbles are in the bubble bar, this will be used to notify bubble bar views. */
@@ -1824,6 +1940,11 @@ public class BubbleController implements ConfigurationChangeListener,
}
@Override
+ public void bubbleOverflowChanged(boolean hasBubbles) {
+ // Nothing to do for our views, handled by launcher / in the bubble bar.
+ }
+
+ @Override
public void suppressionChanged(Bubble bubble, boolean isSuppressed) {
if (mLayerView != null) {
// TODO (b/273316505) handle suppression changes, although might not need to
@@ -1862,7 +1983,7 @@ public class BubbleController implements ConfigurationChangeListener,
ProtoLog.d(WM_SHELL_BUBBLES, "mBubbleDataListener#applyUpdate:"
+ " added=%s removed=%b updated=%s orderChanged=%b expansionChanged=%b"
+ " expanded=%b selectionChanged=%b selected=%s"
- + " suppressed=%s unsupressed=%s shouldShowEducation=%b",
+ + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b",
update.addedBubble != null ? update.addedBubble.getKey() : "null",
!update.removedBubbles.isEmpty(),
update.updatedBubble != null ? update.updatedBubble.getKey() : "null",
@@ -1871,13 +1992,17 @@ public class BubbleController implements ConfigurationChangeListener,
update.selectedBubble != null ? update.selectedBubble.getKey() : "null",
update.suppressedBubble != null ? update.suppressedBubble.getKey() : "null",
update.unsuppressedBubble != null ? update.unsuppressedBubble.getKey() : "null",
- update.shouldShowEducation);
+ update.shouldShowEducation, update.showOverflowChanged);
ensureBubbleViewsAndWindowCreated();
// Lazy load overflow bubbles from disk
loadOverflowBubblesFromDisk();
+ if (update.showOverflowChanged) {
+ mBubbleViewCallback.bubbleOverflowChanged(!update.overflowBubbles.isEmpty());
+ }
+
// If bubbles in the overflow have a dot, make sure the overflow shows a dot
updateOverflowButtonDot();
@@ -2129,6 +2254,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) {
@@ -2136,6 +2289,7 @@ public class BubbleController implements ConfigurationChangeListener,
pw.print(prefix); pw.println(" currentUserId= " + mCurrentUserId);
pw.print(prefix); pw.println(" isStatusBarShade= " + mIsStatusBarShade);
pw.print(prefix); pw.println(" isShowingAsBubbleBar= " + isShowingAsBubbleBar());
+ pw.print(prefix); pw.println(" isImeVisible= " + mBubblePositioner.isImeVisible());
pw.println();
mBubbleData.dump(pw);
@@ -2234,15 +2388,19 @@ public class BubbleController implements ConfigurationChangeListener,
private final SingleInstanceRemoteListener<BubbleController, IBubblesListener> mListener;
private final Bubbles.BubbleStateListener mBubbleListener =
new Bubbles.BubbleStateListener() {
+ @Override
+ public void onBubbleStateChange(BubbleBarUpdate update) {
+ Bundle b = new Bundle();
+ b.setClassLoader(BubbleBarUpdate.class.getClassLoader());
+ b.putParcelable(BubbleBarUpdate.BUNDLE_KEY, update);
+ mListener.call(l -> l.onBubbleStateChange(b));
+ }
- @Override
- public void onBubbleStateChange(BubbleBarUpdate update) {
- Bundle b = new Bundle();
- b.setClassLoader(BubbleBarUpdate.class.getClassLoader());
- b.putParcelable(BubbleBarUpdate.BUNDLE_KEY, update);
- mListener.call(l -> l.onBubbleStateChange(b));
- }
- };
+ @Override
+ public void animateBubbleBarLocation(BubbleBarLocation location) {
+ mListener.call(l -> l.animateBubbleBarLocation(location));
+ }
+ };
IBubblesImpl(BubbleController controller) {
mController = controller;
@@ -2257,6 +2415,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
@@ -2270,16 +2430,9 @@ public class BubbleController implements ConfigurationChangeListener,
}
@Override
- public void showBubble(String key, Rect bubbleBarBounds) {
+ public void showBubble(String key, int topOnScreen) {
mMainExecutor.execute(
- () -> mController.expandStackAndSelectBubbleFromLauncher(
- key, bubbleBarBounds));
- }
-
- @Override
- public void removeBubble(String key) {
- mMainExecutor.execute(
- () -> mController.removeBubble(key, Bubbles.DISMISS_USER_GESTURE));
+ () -> mController.expandStackAndSelectBubbleFromLauncher(key, topOnScreen));
}
@Override
@@ -2293,8 +2446,18 @@ public class BubbleController implements ConfigurationChangeListener,
}
@Override
- public void onBubbleDrag(String bubbleKey, boolean isBeingDragged) {
- mMainExecutor.execute(() -> mController.onBubbleDrag(bubbleKey, isBeingDragged));
+ public void startBubbleDrag(String bubbleKey) {
+ mMainExecutor.execute(() -> mController.startBubbleDrag(bubbleKey));
+ }
+
+ @Override
+ public void stopBubbleDrag(BubbleBarLocation location, int topOnScreen) {
+ mMainExecutor.execute(() -> mController.stopBubbleDrag(location, topOnScreen));
+ }
+
+ @Override
+ public void dragBubbleToDismiss(String key) {
+ mMainExecutor.execute(() -> mController.dragBubbleToDismiss(key));
}
@Override
@@ -2302,6 +2465,20 @@ public class BubbleController implements ConfigurationChangeListener,
mMainExecutor.execute(() ->
mController.showUserEducation(new Point(positionX, positionY)));
}
+
+ @Override
+ public void setBubbleBarLocation(BubbleBarLocation location) {
+ mMainExecutor.execute(() ->
+ mController.setBubbleBarLocation(location));
+ }
+
+ @Override
+ public void updateBubbleBarTopOnScreen(int topOnScreen) {
+ mMainExecutor.execute(() -> {
+ mBubblePositioner.setBubbleBarTopOnScreen(topOnScreen);
+ if (mLayerView != null) mLayerView.updateExpandedView();
+ });
+ }
}
private class BubblesImpl implements Bubbles {
@@ -2417,17 +2594,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);
@@ -2612,6 +2778,15 @@ public class BubbleController implements ConfigurationChangeListener,
() -> BubbleController.this.onSensitiveNotificationProtectionStateChanged(
sensitiveNotificationProtectionActive));
}
+
+ @Override
+ public boolean canShowBubbleNotification() {
+ // in bubble bar mode, when the IME is visible we can't animate new bubbles.
+ if (BubbleController.this.isShowingAsBubbleBar()) {
+ return !BubbleController.this.mBubblePositioner.isImeVisible();
+ }
+ return true;
+ }
}
/**
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 6c2f925119f3..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
@@ -77,6 +77,7 @@ public class BubbleData {
boolean suppressedSummaryChanged;
boolean expanded;
boolean shouldShowEducation;
+ boolean showOverflowChanged;
@Nullable BubbleViewProvider selectedBubble;
@Nullable Bubble addedBubble;
@Nullable Bubble updatedBubble;
@@ -109,7 +110,8 @@ public class BubbleData {
|| suppressedBubble != null
|| unsuppressedBubble != null
|| suppressedSummaryChanged
- || suppressedSummaryGroup != null;
+ || suppressedSummaryGroup != null
+ || showOverflowChanged;
}
void bubbleRemoved(Bubble bubbleToRemove, @DismissReason int reason) {
@@ -157,6 +159,8 @@ public class BubbleData {
bubbleBarUpdate.bubbleKeysInOrder.add(bubbles.get(i).getKey());
}
}
+ bubbleBarUpdate.showOverflowChanged = showOverflowChanged;
+ bubbleBarUpdate.showOverflow = !overflowBubbles.isEmpty();
return bubbleBarUpdate;
}
@@ -165,7 +169,7 @@ public class BubbleData {
* used when {@link BubbleController#isShowingAsBubbleBar()} is true.
*/
BubbleBarUpdate getInitialState() {
- BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate();
+ BubbleBarUpdate bubbleBarUpdate = BubbleBarUpdate.createInitialState();
bubbleBarUpdate.shouldShowEducation = shouldShowEducation;
for (int i = 0; i < bubbles.size(); i++) {
bubbleBarUpdate.currentBubbleList.add(bubbles.get(i).asBubbleBarBubble());
@@ -252,10 +256,16 @@ 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() {
- return mStateChange.getInitialState();
+ 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;
}
public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) {
@@ -321,13 +331,16 @@ public class BubbleData {
return mSelectedBubble;
}
- public BubbleOverflow getOverflow() {
- return mOverflow;
+ /**
+ * Returns the key of the selected bubble, or null if no bubble is selected.
+ */
+ @Nullable
+ public String getSelectedBubbleKey() {
+ return mSelectedBubble != null ? mSelectedBubble.getKey() : null;
}
- /** Return a read-only current active bubble lists. */
- public List<Bubble> getActiveBubbles() {
- return Collections.unmodifiableList(mBubbles);
+ public BubbleOverflow getOverflow() {
+ return mOverflow;
}
public void setExpanded(boolean expanded) {
@@ -363,6 +376,19 @@ public class BubbleData {
mSelectedBubble = bubble;
}
+ /**
+ * Sets the selected bubble and expands it.
+ *
+ * <p>This dispatches a single state update for both changes and should be used instead of
+ * calling {@link #setSelectedBubble(BubbleViewProvider)} followed by
+ * {@link #setExpanded(boolean)} immediately after, which will generate 2 separate updates.
+ */
+ public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble) {
+ setSelectedBubbleInternal(bubble);
+ setExpandedInternal(true);
+ dispatchPendingChanges();
+ }
+
public void setSelectedBubble(BubbleViewProvider bubble) {
setSelectedBubbleInternal(bubble);
dispatchPendingChanges();
@@ -395,6 +421,9 @@ public class BubbleData {
if (bubbleToReturn != null) {
// Promoting from overflow
mOverflowBubbles.remove(bubbleToReturn);
+ if (mOverflowBubbles.isEmpty()) {
+ mStateChange.showOverflowChanged = true;
+ }
} else if (mPendingBubbles.containsKey(key)) {
// Update while it was pending
bubbleToReturn = mPendingBubbles.get(key);
@@ -482,19 +511,6 @@ public class BubbleData {
}
/**
- * Explicitly removes a bubble from the overflow, if it exists.
- *
- * @param bubble the bubble to remove.
- */
- public void removeOverflowBubble(Bubble bubble) {
- if (bubble == null) return;
- if (mOverflowBubbles.remove(bubble)) {
- mStateChange.removedOverflowBubble = bubble;
- dispatchPendingChanges();
- }
- }
-
- /**
* Adds a group key indicating that the summary for this group should be suppressed.
*
* @param groupKey the group key of the group whose summary should be suppressed.
@@ -586,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();
@@ -662,13 +678,12 @@ 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) {
if (hasOverflowBubbleWithKey(key)
&& shouldRemoveHiddenBubble) {
-
Bubble b = getOverflowBubbleWithKey(key);
ProtoLog.d(WM_SHELL_BUBBLES, "doRemove - cancel overflow bubble=%s", key);
if (b != null) {
@@ -678,6 +693,7 @@ public class BubbleData {
mOverflowBubbles.remove(b);
mStateChange.bubbleRemoved(b, reason);
mStateChange.removedOverflowBubble = b;
+ mStateChange.showOverflowChanged = mOverflowBubbles.isEmpty();
}
if (hasSuppressedBubbleWithKey(key) && shouldRemoveHiddenBubble) {
Bubble b = getSuppressedBubbleWithKey(key);
@@ -777,6 +793,9 @@ public class BubbleData {
}
ProtoLog.d(WM_SHELL_BUBBLES, "overflowBubble=%s", bubble.getKey());
mLogger.logOverflowAdd(bubble, reason);
+ if (mOverflowBubbles.isEmpty()) {
+ mStateChange.showOverflowChanged = true;
+ }
mOverflowBubbles.remove(bubble);
mOverflowBubbles.add(0, bubble);
mStateChange.addedOverflowBubble = bubble;
@@ -897,6 +916,9 @@ public class BubbleData {
((Bubble) bubble).markAsAccessedAt(mTimeSource.currentTimeMillis());
}
mSelectedBubble = bubble;
+ if (isOverflow) {
+ mShowingOverflow = true;
+ }
mStateChange.selectedBubble = bubble;
mStateChange.selectionChanged = true;
}
@@ -1216,9 +1238,7 @@ public class BubbleData {
public void dump(PrintWriter pw) {
pw.println("BubbleData state:");
pw.print(" selected: ");
- pw.println(mSelectedBubble != null
- ? mSelectedBubble.getKey()
- : "null");
+ pw.println(getSelectedBubbleKey());
pw.print(" expanded: ");
pw.println(mExpanded);
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 74f087b6d8f8..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
@@ -68,6 +68,7 @@ import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.common.AlphaOptimizedButton;
import com.android.wm.shell.common.TriangleShape;
@@ -446,6 +447,8 @@ public class BubbleExpandedView extends LinearLayout {
mManageButton.setVisibility(GONE);
} else {
mTaskView = bubbleTaskView.getTaskView();
+ // reset the insets that might left after TaskView is shown in BubbleBarExpandedView
+ mTaskView.setCaptionInsets(null);
bubbleTaskView.setDelegateListener(mTaskViewListener);
// set a fixed width so it is not recalculated as part of a rotation. the width will be
@@ -479,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() {
@@ -545,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;
@@ -668,6 +685,11 @@ public class BubbleExpandedView extends LinearLayout {
}
}
+ /** Sets the alpha for the pointer. */
+ public void setPointerAlpha(float alpha) {
+ mPointerView.setAlpha(alpha);
+ }
+
/**
* Get alpha from underlying {@code TaskView} if this view is for a bubble.
* Or get alpha for the overflow view if this view is for overflow.
@@ -698,12 +720,14 @@ public class BubbleExpandedView extends LinearLayout {
}
}
- /**
- * Sets the alpha of the background and the pointer view.
- */
+ /** Sets the alpha of the background. */
public void setBackgroundAlpha(float alpha) {
- mPointerView.setAlpha(alpha);
- setAlpha(alpha);
+ if (Flags.enableNewBubbleAnimations()) {
+ setAlpha(alpha);
+ } else {
+ mPointerView.setAlpha(alpha);
+ setAlpha(alpha);
+ }
}
/**
@@ -1045,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/BubbleFlyoutView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java
index 6a5f785504c0..42de401d9db9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleFlyoutView.java
@@ -24,6 +24,7 @@ import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT;
import android.animation.ArgbEvaluator;
import android.content.Context;
+import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
@@ -74,7 +75,7 @@ public class BubbleFlyoutView extends FrameLayout {
private final int mFlyoutElevation;
private final int mBubbleElevation;
- private final int mFloatingBackgroundColor;
+ private int mFloatingBackgroundColor;
private final float mCornerRadius;
private final ViewGroup mFlyoutTextContainer;
@@ -107,6 +108,9 @@ public class BubbleFlyoutView extends FrameLayout {
/** Color of the 'new' dot that the flyout will transform into. */
private int mDotColor;
+ /** Keeps last used night mode flags **/
+ private int mNightModeFlags;
+
/** The outline of the triangle, used for elevation shadows. */
private final Outline mTriangleOutline = new Outline();
@@ -176,11 +180,8 @@ public class BubbleFlyoutView extends FrameLayout {
mFlyoutElevation = res.getDimensionPixelSize(R.dimen.bubble_flyout_elevation);
final TypedArray ta = mContext.obtainStyledAttributes(
- new int[] {
- com.android.internal.R.attr.materialColorSurfaceContainer,
- android.R.attr.dialogCornerRadius});
- mFloatingBackgroundColor = ta.getColor(0, Color.WHITE);
- mCornerRadius = ta.getDimensionPixelSize(1, 0);
+ new int[] {android.R.attr.dialogCornerRadius});
+ mCornerRadius = ta.getDimensionPixelSize(0, 0);
ta.recycle();
// Add padding for the pointer on either side, onDraw will draw it in this space.
@@ -198,19 +199,17 @@ public class BubbleFlyoutView extends FrameLayout {
// Use locale direction so the text is aligned correctly.
setLayoutDirection(LAYOUT_DIRECTION_LOCALE);
- mBgPaint.setColor(mFloatingBackgroundColor);
-
mLeftTriangleShape =
new ShapeDrawable(TriangleShape.createHorizontal(
mPointerSize, mPointerSize, true /* isPointingLeft */));
mLeftTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize);
- mLeftTriangleShape.getPaint().setColor(mFloatingBackgroundColor);
mRightTriangleShape =
new ShapeDrawable(TriangleShape.createHorizontal(
mPointerSize, mPointerSize, false /* isPointingLeft */));
mRightTriangleShape.setBounds(0, 0, mPointerSize, mPointerSize);
- mRightTriangleShape.getPaint().setColor(mFloatingBackgroundColor);
+
+ applyConfigurationColors(getResources().getConfiguration());
}
@Override
@@ -244,6 +243,13 @@ public class BubbleFlyoutView extends FrameLayout {
fade(false /* in */, stackPos, hideDot, afterFadeOut);
}
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ if (applyColorsAccordingToConfiguration(newConfig)) {
+ invalidate();
+ }
+ }
+
/*
* Fade-out above or fade-in from below.
*/
@@ -424,6 +430,42 @@ public class BubbleFlyoutView extends FrameLayout {
}
/**
+ * Resolving and applying colors according to the ui mode, remembering most recent mode.
+ *
+ * @return {@code true} if night mode setting has changed since the last invocation,
+ * {@code false} otherwise
+ */
+ boolean applyColorsAccordingToConfiguration(Configuration configuration) {
+ int nightModeFlags = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
+ boolean flagsChanged = nightModeFlags != mNightModeFlags;
+ if (flagsChanged) {
+ mNightModeFlags = nightModeFlags;
+ applyConfigurationColors(configuration);
+ }
+ return flagsChanged;
+ }
+
+ private void applyConfigurationColors(Configuration configuration) {
+ int nightModeFlags = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
+ boolean isNightModeOn = nightModeFlags == Configuration.UI_MODE_NIGHT_YES;
+ try (TypedArray ta = mContext.obtainStyledAttributes(
+ new int[]{
+ com.android.internal.R.attr.materialColorSurfaceContainer,
+ com.android.internal.R.attr.materialColorOnSurface,
+ com.android.internal.R.attr.materialColorOnSurfaceVariant})) {
+ mFloatingBackgroundColor = ta.getColor(0,
+ isNightModeOn ? Color.BLACK : Color.WHITE);
+ mSenderText.setTextColor(ta.getColor(1,
+ isNightModeOn ? Color.WHITE : Color.BLACK));
+ mMessageText.setTextColor(ta.getColor(2,
+ isNightModeOn ? Color.WHITE : Color.BLACK));
+ mBgPaint.setColor(mFloatingBackgroundColor);
+ mLeftTriangleShape.getPaint().setColor(mFloatingBackgroundColor);
+ mRightTriangleShape.getPaint().setColor(mFloatingBackgroundColor);
+ }
+ }
+
+ /**
* Renders the background, which is either the rounded 'chat bubble' flyout, or some state
* between that and the 'new' dot over the bubbles.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
index 633b01bde4ca..18e04d14c71b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflowContainerView.java
@@ -44,6 +44,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.ContrastColorUtil;
+import com.android.wm.shell.Flags;
import com.android.wm.shell.R;
import java.util.ArrayList;
@@ -195,7 +196,9 @@ public class BubbleOverflowContainerView extends LinearLayout {
}
void updateEmptyStateVisibility() {
- mEmptyState.setVisibility(mOverflowBubbles.isEmpty() ? View.VISIBLE : View.GONE);
+ boolean showEmptyState = mOverflowBubbles.isEmpty()
+ && !Flags.enableOptionalBubbleOverflow();
+ mEmptyState.setVisibility(showEmptyState ? View.VISIBLE : View.GONE);
mRecyclerView.setVisibility(mOverflowBubbles.isEmpty() ? View.GONE : View.VISIBLE);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index a5853d621cb5..2382545ab324 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -32,6 +32,7 @@ import androidx.annotation.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconNormalizer;
import com.android.wm.shell.R;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
/**
* Keeps track of display size, configuration, and specific bubble sizes. One place for all
@@ -75,6 +76,7 @@ public class BubblePositioner {
private int mBubblePaddingTop;
private int mBubbleOffscreenAmount;
private int mStackOffset;
+ private int mBubbleElevation;
private int mExpandedViewMinHeight;
private int mExpandedViewLargeScreenWidth;
@@ -95,7 +97,8 @@ public class BubblePositioner {
private PointF mRestingStackPosition;
private boolean mShowingInBubbleBar;
- private final Rect mBubbleBarBounds = new Rect();
+ private BubbleBarLocation mBubbleBarLocation = BubbleBarLocation.DEFAULT;
+ private int mBubbleBarTopOnScreen;
public BubblePositioner(Context context, WindowManager windowManager) {
mContext = context;
@@ -145,11 +148,13 @@ public class BubblePositioner {
mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
mBubbleOffscreenAmount = res.getDimensionPixelSize(R.dimen.bubble_stack_offscreen);
mStackOffset = res.getDimensionPixelSize(R.dimen.bubble_stack_offset);
+ mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
if (mShowingInBubbleBar) {
- mExpandedViewLargeScreenWidth = isLandscape()
- ? (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_LANDSCAPE_WIDTH_PERCENT)
- : (int) (bounds.width() * EXPANDED_VIEW_BUBBLE_BAR_PORTRAIT_WIDTH_PERCENT);
+ mExpandedViewLargeScreenWidth = Math.min(
+ res.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width),
+ mPositionRect.width() - 2 * mExpandedViewPadding
+ );
} else if (mDeviceConfig.isSmallTablet()) {
mExpandedViewLargeScreenWidth = (int) (bounds.width()
* EXPANDED_VIEW_SMALL_TABLET_WIDTH_PERCENT);
@@ -319,6 +324,11 @@ public class BubblePositioner {
return 0;
}
+ /** Returns whether the IME is visible. */
+ public boolean isImeVisible() {
+ return mImeVisible;
+ }
+
/** Sets whether the IME is visible. **/
public void setImeVisible(boolean visible, int height) {
mImeVisible = visible;
@@ -659,6 +669,29 @@ public class BubblePositioner {
}
/**
+ * Returns the z translation a specific bubble should use. When expanded we keep a slight
+ * translation to ensure proper ordering when animating to / from collapsed state. When
+ * collapsed, only the top two bubbles appear so only their shadows show.
+ */
+ public float getZTranslation(int index, boolean isOverflow, boolean isExpanded) {
+ if (isOverflow) {
+ return 0f; // overflow is lowest
+ }
+ return isExpanded
+ // When expanded use minimal amount to keep order
+ ? getMaxBubbles() - index
+ // When collapsed, only the top two bubbles have elevation
+ : index < NUM_VISIBLE_WHEN_RESTING
+ ? (getMaxBubbles() * mBubbleElevation) - index
+ : 0;
+ }
+
+ /** The elevation to use for bubble UI elements. */
+ public int getBubbleElevation() {
+ return mBubbleElevation;
+ }
+
+ /**
* @return whether the stack is considered on the left side of the screen.
*/
public boolean isStackOnLeft(PointF currentStackPosition) {
@@ -797,11 +830,33 @@ public class BubblePositioner {
mShowingInBubbleBar = showingInBubbleBar;
}
+ public void setBubbleBarLocation(BubbleBarLocation location) {
+ mBubbleBarLocation = location;
+ }
+
+ public BubbleBarLocation getBubbleBarLocation() {
+ return mBubbleBarLocation;
+ }
+
/**
- * Sets the position of the bubble bar in display coordinates.
+ * @return <code>true</code> when bubble bar is on the left and <code>false</code> when on right
*/
- public void setBubbleBarPosition(Rect bubbleBarBounds) {
- mBubbleBarBounds.set(bubbleBarBounds);
+ public boolean isBubbleBarOnLeft() {
+ return mBubbleBarLocation.isOnLeft(mDeviceConfig.isRtl());
+ }
+
+ /**
+ * Set top coordinate of bubble bar on screen
+ */
+ public void setBubbleBarTopOnScreen(int topOnScreen) {
+ mBubbleBarTopOnScreen = topOnScreen;
+ }
+
+ /**
+ * Returns the top coordinate of bubble bar on screen
+ */
+ public int getBubbleBarTopOnScreen() {
+ return mBubbleBarTopOnScreen;
}
/**
@@ -815,14 +870,45 @@ public class BubblePositioner {
* How tall the expanded view should be when showing from the bubble bar.
*/
public int getExpandedViewHeightForBubbleBar(boolean isOverflow) {
- return isOverflow
- ? mOverflowHeight
- : getExpandedViewBottomForBubbleBar() - mInsets.top - mExpandedViewPadding;
+ if (isOverflow) {
+ return mOverflowHeight;
+ } else {
+ return getBubbleBarExpandedViewHeightForLandscape();
+ }
}
+ /**
+ * Calculate the height of expanded view in landscape mode regardless current orientation.
+ * Here is an explanation:
+ * ------------------------ mScreenRect.top
+ * | top inset ↕ |
+ * |-----------------------
+ * | 16dp spacing ↕ |
+ * | --------- | --- expanded view top
+ * | | | | ↑
+ * | | | | ↓ expanded view height
+ * | --------- | --- expanded view bottom
+ * | 16dp spacing ↕ | ↑
+ * | @bubble bar@ | | height of the bubble bar container
+ * ------------------------ | already includes bottom inset and spacing
+ * | bottom inset ↕ | ↓
+ * |----------------------| --- mScreenRect.bottom
+ */
+ private int getBubbleBarExpandedViewHeightForLandscape() {
+ int heightOfBubbleBarContainer =
+ mScreenRect.height() - getExpandedViewBottomForBubbleBar();
+ // getting landscape height from screen rect
+ int expandedViewHeight = Math.min(mScreenRect.width(), mScreenRect.height());
+ expandedViewHeight -= heightOfBubbleBarContainer; /* removing bubble container height */
+ expandedViewHeight -= mInsets.top; /* removing top inset */
+ expandedViewHeight -= mExpandedViewPadding; /* removing spacing */
+ return expandedViewHeight;
+ }
+
+
/** The bottom position of the expanded view when showing above the bubble bar. */
public int getExpandedViewBottomForBubbleBar() {
- return mBubbleBarBounds.top - mExpandedViewPadding;
+ return mBubbleBarTopOnScreen - mExpandedViewPadding;
}
/**
@@ -833,9 +919,22 @@ public class BubblePositioner {
}
/**
- * Returns the display coordinates of the bubble bar.
+ * Get bubble bar expanded view bounds on screen
*/
- public Rect getBubbleBarBounds() {
- return mBubbleBarBounds;
+ public void getBubbleBarExpandedViewBounds(boolean onLeft, boolean isOverflowExpanded,
+ Rect out) {
+ final int padding = getBubbleBarExpandedViewPadding();
+ final int width = getExpandedViewWidthForBubbleBar(isOverflowExpanded);
+ final int height = getExpandedViewHeightForBubbleBar(isOverflowExpanded);
+
+ out.set(0, 0, width, height);
+ int left;
+ if (onLeft) {
+ left = getInsets().left + padding;
+ } else {
+ left = getAvailableRect().right - width - padding;
+ }
+ int top = getExpandedViewBottomForBubbleBar() - height;
+ out.offsetTo(left, top);
}
}
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 474430eb44ab..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
@@ -80,9 +80,9 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.FrameworkStatsLog;
+import com.android.wm.shell.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.animation.PhysicsAnimator;
import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener;
import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix;
import com.android.wm.shell.bubbles.animation.ExpandedAnimationController;
@@ -95,6 +95,7 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.bubbles.DismissView;
import com.android.wm.shell.common.bubbles.RelativeTouchListener;
import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
import java.io.PrintWriter;
import java.math.BigDecimal;
@@ -133,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 */
@@ -144,6 +147,15 @@ public class BubbleStackView extends FrameLayout
*/
private static final int ANIMATE_TEMPORARILY_INVISIBLE_DELAY = 1000;
+ /**
+ * Percent of the bubble that is hidden while stashed.
+ */
+ private static final float PERCENT_HIDDEN_WHEN_STASHED = 0.55f;
+ /**
+ * How long to wait to animate the stack for stashing.
+ */
+ private static final int ANIMATE_STASH_DELAY = 700;
+
private static final PhysicsAnimator.SpringConfig FLYOUT_IME_ANIMATION_SPRING_CONFIG =
new PhysicsAnimator.SpringConfig(
StackAnimationController.IME_ANIMATION_STIFFNESS,
@@ -334,7 +346,7 @@ public class BubbleStackView extends FrameLayout
pw.println("Expanded bubble state:");
pw.println(" expandedBubbleKey: " + mExpandedBubble.getKey());
- final BubbleExpandedView expandedView = mExpandedBubble.getExpandedView();
+ final BubbleExpandedView expandedView = getExpandedView();
if (expandedView != null) {
pw.println(" expandedViewVis: " + expandedView.getVisibility());
@@ -449,17 +461,21 @@ public class BubbleStackView extends FrameLayout
@Override
public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target,
- @NonNull MagnetizedObject draggedObject) {
- if (draggedObject.getUnderlyingObject() instanceof View view) {
+ @NonNull MagnetizedObject<?> draggedObject) {
+ Object underlyingObject = draggedObject.getUnderlyingObject();
+ if (underlyingObject instanceof View) {
+ View view = (View) underlyingObject;
animateDismissBubble(view, true);
}
}
@Override
public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
- @NonNull MagnetizedObject draggedObject,
+ @NonNull MagnetizedObject<?> draggedObject,
float velX, float velY, boolean wasFlungOut) {
- if (draggedObject.getUnderlyingObject() instanceof View view) {
+ Object underlyingObject = draggedObject.getUnderlyingObject();
+ if (underlyingObject instanceof View) {
+ View view = (View) underlyingObject;
animateDismissBubble(view, false);
if (wasFlungOut) {
@@ -474,7 +490,9 @@ public class BubbleStackView extends FrameLayout
@Override
public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target,
@NonNull MagnetizedObject<?> draggedObject) {
- if (draggedObject.getUnderlyingObject() instanceof View view) {
+ Object underlyingObject = draggedObject.getUnderlyingObject();
+ if (underlyingObject instanceof View) {
+ View view = (View) underlyingObject;
mExpandedAnimationController.dismissDraggedOutBubble(
view /* bubble */,
mDismissView.getHeight() /* translationYBy */,
@@ -530,7 +548,8 @@ public class BubbleStackView extends FrameLayout
private OnClickListener mBubbleClickListener = new OnClickListener() {
@Override
public void onClick(View view) {
- mIsDraggingStack = false; // If the touch ended in a click, we're no longer dragging.
+ // If the touch ended in a click, we're no longer dragging.
+ onDraggingEnded();
// Bubble clicks either trigger expansion/collapse or a bubble switch, both of which we
// shouldn't interrupt. These are quick transitions, so it's not worth trying to adjust
@@ -664,7 +683,7 @@ public class BubbleStackView extends FrameLayout
// First, see if the magnetized object consumes the event - if so, we shouldn't move the
// bubble since it's stuck to the target.
if (!passEventToMagnetizedObject(ev)) {
- updateBubbleShadows(true /* showForAllBubbles */);
+ updateBubbleShadows(true /* isExpanded */);
if (mBubbleData.isExpanded()) {
mExpandedAnimationController.dragBubbleOut(
v, viewInitialX + dx, viewInitialY + dy);
@@ -713,10 +732,17 @@ public class BubbleStackView extends FrameLayout
mDismissView.hide();
}
- mIsDraggingStack = false;
+ onDraggingEnded();
// Hide the stack after a delay, if needed.
updateTemporarilyInvisibleAnimation(false /* hideImmediately */);
+ animateStashedState(false /* stashImmediately */);
+ }
+
+ @Override
+ public void onCancel(@NonNull View v, @NonNull MotionEvent ev, float viewInitialX,
+ float viewInitialY) {
+ animateStashedState(false /* stashImmediately */);
}
};
@@ -791,10 +817,11 @@ public class BubbleStackView extends FrameLayout
private float getScrimAlphaForDrag(float dragAmount) {
// dragAmount should be negative as we allow scroll up only
- if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+ BubbleExpandedView expandedView = getExpandedView();
+ if (expandedView != null) {
float alphaRange = BUBBLE_EXPANDED_SCRIM_ALPHA - MIN_SCRIM_ALPHA_FOR_DRAG;
- int dragMax = mExpandedBubble.getExpandedView().getContentHeight();
+ int dragMax = expandedView.getContentHeight();
float dragFraction = dragAmount / dragMax;
return Math.max(BUBBLE_EXPANDED_SCRIM_ALPHA - alphaRange * dragFraction,
@@ -856,6 +883,7 @@ public class BubbleStackView extends FrameLayout
}
};
+ private boolean mShowingOverflow;
private BubbleOverflow mBubbleOverflow;
private StackEducationView mStackEduView;
private StackEducationView.Manager mStackEducationViewManager;
@@ -884,18 +912,17 @@ public class BubbleStackView extends FrameLayout
mMainExecutor = mainExecutor;
mManager = bubbleStackViewManager;
+ mPositioner = bubblePositioner;
mBubbleData = data;
mSysuiProxyProvider = sysuiProxyProvider;
Resources res = getResources();
mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size);
- mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
+ mBubbleElevation = mPositioner.getBubbleElevation();
mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding);
mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
- int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
- mPositioner = bubblePositioner;
final TypedArray ta = mContext.obtainStyledAttributes(
new int[]{android.R.attr.dialogCornerRadius});
@@ -928,12 +955,12 @@ public class BubbleStackView extends FrameLayout
mBubbleContainer = new PhysicsAnimationLayout(context);
mBubbleContainer.setActiveController(mStackAnimationController);
- mBubbleContainer.setElevation(elevation);
+ mBubbleContainer.setElevation(mBubbleElevation);
mBubbleContainer.setClipChildren(false);
addView(mBubbleContainer, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mExpandedViewContainer = new FrameLayout(context);
- mExpandedViewContainer.setElevation(elevation);
+ mExpandedViewContainer.setElevation(mBubbleElevation);
mExpandedViewContainer.setClipChildren(false);
addView(mExpandedViewContainer);
@@ -986,18 +1013,12 @@ public class BubbleStackView extends FrameLayout
mBubbleOverflow = mBubbleData.getOverflow();
- resetOverflowView();
- mBubbleContainer.addView(mBubbleOverflow.getIconView(),
- mBubbleContainer.getChildCount() /* index */,
- new FrameLayout.LayoutParams(mPositioner.getBubbleSize(),
- mPositioner.getBubbleSize()));
- updateOverflow();
- mBubbleOverflow.getIconView().setOnClickListener((View v) -> {
- mBubbleData.setShowingOverflow(true);
- mBubbleData.setSelectedBubble(mBubbleOverflow);
- mBubbleData.setExpanded(true);
- });
-
+ if (Flags.enableOptionalBubbleOverflow()) {
+ showOverflow(mBubbleData.hasOverflowBubbles());
+ } else {
+ mShowingOverflow = true; // if the flags not on this is always true
+ setUpOverflow();
+ }
mScrim = new View(getContext());
mScrim.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
mScrim.setBackgroundDrawable(new ColorDrawable(
@@ -1019,6 +1040,7 @@ public class BubbleStackView extends FrameLayout
WindowManager.class)));
onDisplaySizeChanged();
mExpandedAnimationController.updateResources();
+ mExpandedAnimationController.onOrientationChanged();
mStackAnimationController.updateResources();
mBubbleOverflow.updateResources();
@@ -1089,6 +1111,7 @@ public class BubbleStackView extends FrameLayout
} else {
maybeShowStackEdu();
}
+ onDraggingEnded();
});
animate()
@@ -1100,33 +1123,35 @@ public class BubbleStackView extends FrameLayout
mExpandedViewAlphaAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
- if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+ BubbleExpandedView expandedView = getExpandedView();
+ if (expandedView != null) {
// We need to be Z ordered on top in order for alpha animations to work.
- mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true);
- mExpandedBubble.getExpandedView().setAnimating(true);
+ expandedView.setSurfaceZOrderedOnTop(true);
+ expandedView.setAnimating(true);
mExpandedViewContainer.setVisibility(VISIBLE);
}
}
@Override
public void onAnimationEnd(Animator animation) {
- if (mExpandedBubble != null
- && mExpandedBubble.getExpandedView() != null
+ BubbleExpandedView expandedView = getExpandedView();
+ if (expandedView != null
// The surface needs to be Z ordered on top for alpha values to work on the
// TaskView, and if we're temporarily hidden, we are still on the screen
// with alpha = 0f until we animate back. Stay Z ordered on top so the alpha
// = 0f remains in effect.
&& !mExpandedViewTemporarilyHidden) {
- mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false);
- mExpandedBubble.getExpandedView().setAnimating(false);
+ expandedView.setSurfaceZOrderedOnTop(false);
+ expandedView.setAnimating(false);
}
}
});
mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> {
- if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+ BubbleExpandedView expandedView = getExpandedView();
+ if (expandedView != null) {
float alpha = (float) valueAnimator.getAnimatedValue();
- mExpandedBubble.getExpandedView().setContentAlpha(alpha);
- mExpandedBubble.getExpandedView().setBackgroundAlpha(alpha);
+ expandedView.setContentAlpha(alpha);
+ expandedView.setBackgroundAlpha(alpha);
}
});
@@ -1146,6 +1171,14 @@ public class BubbleStackView extends FrameLayout
}
/**
+ * Reset state related to dragging.
+ */
+ private void onDraggingEnded() {
+ mIsDraggingStack = false;
+ mMagnetizedObject = null;
+ }
+
+ /**
* Sets whether or not the stack should become temporarily invisible by moving off the side of
* the screen.
*
@@ -1204,6 +1237,59 @@ public class BubbleStackView extends FrameLayout
}
};
+ /**
+ * Animates the bubble stack to stash along the edge of the screen.
+ *
+ * @param stashImmediately whether the stash should happen immediately or without delay.
+ */
+ private void animateStashedState(boolean stashImmediately) {
+ if (!Flags.enableBubbleStashing()) return;
+
+ removeCallbacks(mAnimateStashedState);
+
+ postDelayed(mAnimateStashedState, stashImmediately ? 0 : ANIMATE_STASH_DELAY);
+ }
+
+ private final Runnable mAnimateStashedState = () -> {
+ if (mFlyout.getVisibility() != View.VISIBLE
+ && !mIsDraggingStack
+ && !isExpansionAnimating()
+ && !isExpanded()
+ && !isStackEduVisible()) {
+ // To calculate a distance, bubble stack needs to be moved to become stashed,
+ // we need to take into account that the bubble stack is positioned on the edge
+ // of the available screen rect, which can be offset by system bars and cutouts.
+ final float amountOffscreen = mBubbleSize - (mBubbleSize * PERCENT_HIDDEN_WHEN_STASHED);
+ if (mStackAnimationController.isStackOnLeftSide()) {
+ int availableRectOffsetX =
+ mPositioner.getAvailableRect().left - mPositioner.getScreenRect().left;
+ mBubbleContainer
+ .animate()
+ .translationX(-(amountOffscreen + availableRectOffsetX))
+ .start();
+ } else {
+ int availableRectOffsetX =
+ mPositioner.getAvailableRect().right - mPositioner.getScreenRect().right;
+ mBubbleContainer.animate()
+ .translationX(amountOffscreen - availableRectOffsetX)
+ .start();
+ }
+ }
+ };
+
+ private void setUpOverflow() {
+ resetOverflowView();
+ mBubbleContainer.addView(mBubbleOverflow.getIconView(),
+ mBubbleContainer.getChildCount() /* index */,
+ new FrameLayout.LayoutParams(mBubbleSize, mBubbleSize));
+ updateOverflow();
+ mBubbleOverflow.getIconView().setOnClickListener((View v) -> {
+ mBubbleData.setShowingOverflow(true);
+ mBubbleData.setSelectedBubble(mBubbleOverflow);
+ mBubbleData.setExpanded(true);
+ });
+ }
+
private void setUpDismissView() {
if (mDismissView != null) {
removeView(mDismissView);
@@ -1309,7 +1395,7 @@ public class BubbleStackView extends FrameLayout
}
final boolean seen = getPrefBoolean(ManageEducationView.PREF_MANAGED_EDUCATION);
final boolean shouldShow = (!seen || BubbleDebugConfig.forceShowUserEducation(mContext))
- && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null;
+ && getExpandedView() != null;
ProtoLog.d(WM_SHELL_BUBBLES, "Show manage edu=%b", shouldShow);
if (shouldShow && BubbleDebugConfig.neverShowUserEducation(mContext)) {
Log.w(TAG, "Want to show manage edu, but it is forced hidden");
@@ -1336,9 +1422,9 @@ public class BubbleStackView extends FrameLayout
* Show manage education if was not showing before.
*/
private void showManageEdu() {
- if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) return;
- mManageEduView.show(mExpandedBubble.getExpandedView(),
- mStackAnimationController.isStackOnLeftSide());
+ BubbleExpandedView expandedView = getExpandedView();
+ if (expandedView == null) return;
+ mManageEduView.show(expandedView, mStackAnimationController.isStackOnLeftSide());
}
@VisibleForTesting
@@ -1442,24 +1528,66 @@ public class BubbleStackView extends FrameLayout
b.getExpandedView().updateFontSize();
}
}
- if (mBubbleOverflow != null && mBubbleOverflow.getExpandedView() != null) {
+ if (mShowingOverflow && mBubbleOverflow != null
+ && mBubbleOverflow.getExpandedView() != null) {
mBubbleOverflow.getExpandedView().updateFontSize();
}
}
void updateLocale() {
- if (mBubbleOverflow != null && mBubbleOverflow.getExpandedView() != null) {
+ if (mShowingOverflow && mBubbleOverflow != null
+ && mBubbleOverflow.getExpandedView() != null) {
mBubbleOverflow.getExpandedView().updateLocale();
}
}
private void updateOverflow() {
mBubbleOverflow.update();
- mBubbleContainer.reorderView(mBubbleOverflow.getIconView(),
- mBubbleContainer.getChildCount() - 1 /* index */);
+ if (mShowingOverflow) {
+ mBubbleContainer.reorderView(mBubbleOverflow.getIconView(),
+ mBubbleContainer.getChildCount() - 1 /* index */);
+ }
updateOverflowVisibility();
}
+ private void updateOverflowVisibility() {
+ 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) {
+ if (mShowingOverflow && mBubbleOverflow.showDot()) {
+ mBubbleOverflow.getIconView().animateDotScale(expanding ? 1 : 0f, () -> {
+ mBubbleOverflow.setVisible(expanding ? VISIBLE : GONE);
+ });
+ }
+ }
+
+ /** Sets whether the overflow should be visible or not. */
+ public void showOverflow(boolean showOverflow) {
+ if (!Flags.enableOptionalBubbleOverflow()) return;
+ if (mShowingOverflow != showOverflow) {
+ mShowingOverflow = showOverflow;
+ if (showOverflow) {
+ setUpOverflow();
+ } else if (mBubbleOverflow != null) {
+ resetOverflowView();
+ }
+ }
+ }
+
/**
* Handle theme changes.
*/
@@ -1519,7 +1647,10 @@ public class BubbleStackView extends FrameLayout
b.getExpandedView().updateDimensions();
}
}
- mBubbleOverflow.getIconView().setLayoutParams(new LayoutParams(mBubbleSize, mBubbleSize));
+ if (mShowingOverflow) {
+ mBubbleOverflow.getIconView().setLayoutParams(
+ new LayoutParams(mBubbleSize, mBubbleSize));
+ }
mExpandedAnimationController.updateResources();
mStackAnimationController.updateResources();
mDismissView.updateResources();
@@ -1683,7 +1814,7 @@ public class BubbleStackView extends FrameLayout
bubble.getIconView().setContentDescription(getResources().getString(
R.string.bubble_content_description_single, titleStr, appName));
} else {
- final int moreCount = mBubbleContainer.getChildCount() - 1;
+ final int moreCount = getBubbleCount();
bubble.getIconView().setContentDescription(getResources().getString(
R.string.bubble_content_description_stack,
titleStr, appName, moreCount));
@@ -1736,7 +1867,8 @@ public class BubbleStackView extends FrameLayout
View bubbleOverflowIconView =
mBubbleOverflow != null ? mBubbleOverflow.getIconView() : null;
- if (bubbleOverflowIconView != null && !mBubbleData.getBubbles().isEmpty()) {
+ if (mShowingOverflow && bubbleOverflowIconView != null
+ && !mBubbleData.getBubbles().isEmpty()) {
Bubble lastBubble =
mBubbleData.getBubbles().get(mBubbleData.getBubbles().size() - 1);
View lastBubbleIconView = lastBubble.getIconView();
@@ -1814,6 +1946,11 @@ public class BubbleStackView extends FrameLayout
return mExpandedBubble;
}
+ @Nullable
+ private BubbleExpandedView getExpandedView() {
+ return mExpandedBubble != null ? mExpandedBubble.getExpandedView() : null;
+ }
+
// via BubbleData.Listener
@SuppressLint("ClickableViewAccessibility")
void addBubble(Bubble bubble) {
@@ -1854,7 +1991,7 @@ public class BubbleStackView extends FrameLayout
bubble.getIconView().setDotBadgeOnLeft(!mStackOnLeftOrWillBe /* onLeft */);
bubble.getIconView().setOnClickListener(mBubbleClickListener);
bubble.getIconView().setOnTouchListener(mBubbleTouchListener);
- updateBubbleShadows(false /* showForAllBubbles */);
+ updateBubbleShadows(mIsExpanded);
animateInFlyoutForBubble(bubble);
requestUpdate();
logBubbleEvent(bubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
@@ -1912,20 +2049,6 @@ public class BubbleStackView extends FrameLayout
}
}
- private void updateOverflowVisibility() {
- mBubbleOverflow.setVisible((mIsExpanded || mBubbleData.isShowingOverflow())
- ? VISIBLE
- : GONE);
- }
-
- private void updateOverflowDotVisibility(boolean expanding) {
- if (mBubbleOverflow.showDot()) {
- mBubbleOverflow.getIconView().animateDotScale(expanding ? 1 : 0f, () -> {
- mBubbleOverflow.setVisible(expanding ? VISIBLE : GONE);
- });
- }
- }
-
// via BubbleData.Listener
void updateBubble(Bubble bubble) {
animateInFlyoutForBubble(bubble);
@@ -1957,7 +2080,7 @@ public class BubbleStackView extends FrameLayout
if (mIsExpanded || isExpansionAnimating()) {
reorder.run();
updateBadges(false /* setBadgeForCollapsedStack */);
- updateZOrder();
+ updateBubbleShadows(true /* isExpanded */);
} else {
List<View> bubbleViews = bubbles.stream()
.map(b -> b.getIconView()).collect(Collectors.toList());
@@ -2007,13 +2130,11 @@ public class BubbleStackView extends FrameLayout
// If we're expanded, screenshot the currently expanded bubble (before expanding the newly
// selected bubble) so we can animate it out.
- if (mIsExpanded && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null
- && !mExpandedViewTemporarilyHidden) {
- if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
- // Before screenshotting, have the real TaskView show on top of other surfaces
- // so that the screenshot doesn't flicker on top of it.
- mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true);
- }
+ BubbleExpandedView expandedView = getExpandedView();
+ if (mIsExpanded && expandedView != null && !mExpandedViewTemporarilyHidden) {
+ // Before screenshotting, have the real TaskView show on top of other surfaces
+ // so that the screenshot doesn't flicker on top of it.
+ expandedView.setSurfaceZOrderedOnTop(true);
try {
screenshotAnimatingOutBubbleIntoSurface((success) -> {
@@ -2033,11 +2154,18 @@ public class BubbleStackView extends FrameLayout
private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) {
final BubbleViewProvider previouslySelected = mExpandedBubble;
mExpandedBubble = bubbleToSelect;
- mExpandedViewAnimationController.setExpandedView(mExpandedBubble.getExpandedView());
+ mExpandedViewAnimationController.setExpandedView(getExpandedView());
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
@@ -2106,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() {
@@ -2200,7 +2338,7 @@ public class BubbleStackView extends FrameLayout
mBubbleContainer.addView(bubble.getIconView(), index,
new LayoutParams(mPositioner.getBubbleSize(),
mPositioner.getBubbleSize()));
- updateBubbleShadows(false /* showForAllBubbles */);
+ updateBubbleShadows(mIsExpanded);
requestUpdate();
}
}
@@ -2215,7 +2353,6 @@ public class BubbleStackView extends FrameLayout
* not.
*/
void hideCurrentInputMethod() {
- mPositioner.setImeVisible(false, 0);
mManager.hideCurrentInputMethod();
}
@@ -2325,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 */);
@@ -2333,14 +2470,25 @@ public class BubbleStackView extends FrameLayout
beforeExpandedViewAnimation();
showScrim(true, null /* runnable */);
- updateZOrder();
- updateBadges(false /* setBadgeForCollapsedStack */);
+ updateBubbleShadows(mIsExpanded);
mBubbleContainer.setActiveController(mExpandedAnimationController);
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();
+ }
mExpandedAnimationController.expandFromStack(() -> {
- if (mIsExpanded && mExpandedBubble != null
- && mExpandedBubble.getExpandedView() != null) {
+ if (mIsExpanded && getExpandedView() != null) {
maybeShowManageEdu();
}
updateOverflowDotVisibility(true /* expanding */);
@@ -2351,63 +2499,67 @@ public class BubbleStackView extends FrameLayout
} else {
index = getBubbleIndex(mExpandedBubble);
}
- PointF p = mPositioner.getExpandedBubbleXY(index, getState());
+ PointF bubbleXY = mPositioner.getExpandedBubbleXY(index, getState());
final float translationY = mPositioner.getExpandedViewY(mExpandedBubble,
- mPositioner.showBubblesVertically() ? p.y : p.x);
+ mPositioner.showBubblesVertically() ? bubbleXY.y : bubbleXY.x);
mExpandedViewContainer.setTranslationX(0f);
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
? mStackAnimationController.getStackPosition().y
: mStackAnimationController.getStackPosition().x;
final float bubbleWillBeAt = showVertically
- ? p.y
- : p.x;
+ ? bubbleXY.y
+ : bubbleXY.x;
final float distanceAnimated = Math.abs(bubbleWillBeAt - relevantStackPosition);
// Wait for the path animation target to reach its end, and add a small amount of extra time
// if the bubble is moving a lot horizontally.
- long startDelay = 0L;
+ final long startDelay;
// Should not happen since we lay out before expanding, but just in case...
if (getWidth() > 0) {
startDelay = (long)
(ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION * 1.2f
+ (distanceAnimated / getWidth()) * 30);
+ } else {
+ startDelay = 0L;
}
// Set the pivot point for the scale, so the expanded view animates out from the bubble.
if (showVertically) {
float pivotX;
if (mStackOnLeftOrWillBe) {
- pivotX = p.x + mBubbleSize + mExpandedViewPadding;
+ pivotX = bubbleXY.x + mBubbleSize + mExpandedViewPadding;
} else {
- pivotX = p.x - mExpandedViewPadding;
+ pivotX = bubbleXY.x - mExpandedViewPadding;
}
mExpandedViewContainerMatrix.setScale(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
pivotX,
- p.y + mBubbleSize / 2f);
+ bubbleXY.y + mBubbleSize / 2f);
} else {
mExpandedViewContainerMatrix.setScale(
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
- p.x + mBubbleSize / 2f,
- p.y + mBubbleSize + mExpandedViewPadding);
+ bubbleXY.x + mBubbleSize / 2f,
+ bubbleXY.y + mBubbleSize + mExpandedViewPadding);
}
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
- if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
- mExpandedBubble.getExpandedView().setContentAlpha(0f);
- mExpandedBubble.getExpandedView().setBackgroundAlpha(0f);
+ BubbleExpandedView expandedView = getExpandedView();
+ if (expandedView != null) {
+ expandedView.setContentAlpha(0f);
+ expandedView.setBackgroundAlpha(0f);
// We'll be starting the alpha animation after a slight delay, so set this flag early
// here.
- mExpandedBubble.getExpandedView().setAnimating(true);
+ expandedView.setAnimating(true);
}
mDelayedAnimation = () -> {
@@ -2437,10 +2589,9 @@ public class BubbleStackView extends FrameLayout
.withEndActions(() -> {
mExpandedViewContainer.setAnimationMatrix(null);
afterExpandedViewAnimation();
- if (mExpandedBubble != null
- && mExpandedBubble.getExpandedView() != null) {
- mExpandedBubble.getExpandedView()
- .setSurfaceZOrderedOnTop(false);
+ BubbleExpandedView expView = getExpandedView();
+ if (expView != null) {
+ expView.setSurfaceZOrderedOnTop(false);
}
})
.start();
@@ -2448,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");
@@ -2474,15 +2666,17 @@ public class BubbleStackView extends FrameLayout
// Let the expanded animation controller know that it shouldn't animate child adds/reorders
// since we're about to animate collapsed.
mExpandedAnimationController.notifyPreparingToCollapse();
-
+ final PointF collapsePosition = mStackAnimationController
+ .getStackPositionAlongNearestHorizontalEdge();
updateOverflowDotVisibility(false /* expanding */);
final Runnable collapseBackToStack = () ->
mExpandedAnimationController.collapseBackToStack(
- mStackAnimationController.getStackPositionAlongNearestHorizontalEdge(),
+ collapsePosition,
/* fadeBubblesDuringCollapse= */ mRemovingLastBubbleWhileExpanded,
() -> {
mBubbleContainer.setActiveController(mStackAnimationController);
updateOverflowVisibility();
+ animateShadows();
});
final Runnable after = () -> {
@@ -2493,21 +2687,23 @@ public class BubbleStackView extends FrameLayout
mManageEduView.hide();
}
- updateZOrder();
updateBadges(true /* setBadgeForCollapsedStack */);
afterExpandedViewAnimation();
if (previouslySelected != null) {
previouslySelected.setTaskViewVisibility(false);
}
mExpandedViewAnimationController.reset();
+ animateStashedState(false /* stashImmediately */);
};
- mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after);
- if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+ mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after,
+ collapsePosition);
+ BubbleExpandedView expandedView = getExpandedView();
+ if (expandedView != null) {
// When the animation completes, we should no longer be showing the content.
// This won't actually update content visibility immediately, if we are currently
// animating. But updates the internal state for the content to be hidden after
// animation completes.
- mExpandedBubble.getExpandedView().setContentVisibility(false);
+ expandedView.setContentVisibility(false);
}
}
@@ -2599,10 +2795,10 @@ public class BubbleStackView extends FrameLayout
// expanded view animation might not actually set the z ordering for the
// expanded view correctly, because the view may still be temporarily
// hidden. So set it again here.
- BubbleExpandedView bev = mExpandedBubble.getExpandedView();
- if (bev != null) {
- mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false);
- mExpandedBubble.getExpandedView().setAnimating(false);
+ BubbleExpandedView expandedView = getExpandedView();
+ if (expandedView != null) {
+ expandedView.setSurfaceZOrderedOnTop(false);
+ expandedView.setAnimating(false);
}
})
.start();
@@ -2674,13 +2870,13 @@ public class BubbleStackView extends FrameLayout
if (mIsExpanded) {
mExpandedViewAnimationController.animateForImeVisibilityChange(visible);
- if (mPositioner.showBubblesVertically()
- && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+ BubbleExpandedView expandedView = getExpandedView();
+ if (mPositioner.showBubblesVertically() && expandedView != null) {
float selectedY = mPositioner.getExpandedBubbleXY(getState().selectedIndex,
getState()).y;
float newExpandedViewTop = mPositioner.getExpandedViewY(mExpandedBubble, selectedY);
- mExpandedBubble.getExpandedView().setImeVisible(visible);
- if (!mExpandedBubble.getExpandedView().isUsingMaxHeight()) {
+ expandedView.setImeVisible(visible);
+ if (!expandedView.isUsingMaxHeight()) {
mExpandedViewContainer.animate().translationY(newExpandedViewTop);
}
List<Animator> animList = new ArrayList<>();
@@ -2901,6 +3097,7 @@ public class BubbleStackView extends FrameLayout
}
// Hide the stack after a delay, if needed.
updateTemporarilyInvisibleAnimation(false /* hideImmediately */);
+ animateStashedState(true /* stashImmediately */);
};
// Suppress the dot when we are animating the flyout.
@@ -2993,6 +3190,13 @@ public class BubbleStackView extends FrameLayout
outRect.left -= mBubbleTouchPadding;
outRect.right += mBubbleTouchPadding;
outRect.bottom += mBubbleTouchPadding;
+ if (Flags.enableBubbleStashing()) {
+ if (mStackOnLeftOrWillBe) {
+ outRect.right += mBubbleTouchPadding;
+ } else {
+ outRect.left -= mBubbleTouchPadding;
+ }
+ }
}
} else {
mBubbleContainer.getBoundsOnScreen(outRect);
@@ -3029,7 +3233,8 @@ public class BubbleStackView extends FrameLayout
// This should not happen, since the manage menu is only visible when there's an expanded
// bubble. If we end up in this state, just hide the menu immediately.
- if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
+ BubbleExpandedView expandedView = getExpandedView();
+ if (expandedView == null) {
mManageMenu.setVisibility(View.INVISIBLE);
mManageMenuScrim.setVisibility(INVISIBLE);
mSysuiProxyProvider.getSysuiProxy().onManageMenuExpandChanged(false /* show */);
@@ -3075,8 +3280,8 @@ public class BubbleStackView extends FrameLayout
}
}
- if (mExpandedBubble.getExpandedView().getTaskView() != null) {
- mExpandedBubble.getExpandedView().getTaskView().setObscuredTouchRect(mShowingManage
+ if (expandedView.getTaskView() != null) {
+ expandedView.getTaskView().setObscuredTouchRect(mShowingManage
? new Rect(0, 0, getWidth(), getHeight())
: null);
}
@@ -3086,8 +3291,8 @@ public class BubbleStackView extends FrameLayout
// When the menu is open, it should be at these coordinates. The menu pops out to the right
// in LTR and to the left in RTL.
- mExpandedBubble.getExpandedView().getManageButtonBoundsOnScreen(mTempRect);
- final float margin = mExpandedBubble.getExpandedView().getManageButtonMargin();
+ expandedView.getManageButtonBoundsOnScreen(mTempRect);
+ final float margin = expandedView.getManageButtonMargin();
final float targetX = isLtr
? mTempRect.left - margin
: mTempRect.right + margin - mManageMenu.getWidth();
@@ -3111,9 +3316,10 @@ public class BubbleStackView extends FrameLayout
.withEndActions(() -> {
View child = mManageMenu.getChildAt(0);
child.requestAccessibilityFocus();
- if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+ BubbleExpandedView expView = getExpandedView();
+ if (expView != null) {
// Update the AV's obscured touchable region for the new state.
- mExpandedBubble.getExpandedView().updateObscuredTouchableRegion();
+ expView.updateObscuredTouchableRegion();
}
})
.start();
@@ -3128,9 +3334,10 @@ public class BubbleStackView extends FrameLayout
.spring(DynamicAnimation.TRANSLATION_Y, targetY + menuHeight / 4f)
.withEndActions(() -> {
mManageMenu.setVisibility(View.INVISIBLE);
- if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+ BubbleExpandedView expView = getExpandedView();
+ if (expView != null) {
// Update the AV's obscured touchable region for the new state.
- mExpandedBubble.getExpandedView().updateObscuredTouchableRegion();
+ expView.updateObscuredTouchableRegion();
}
})
.start();
@@ -3157,9 +3364,8 @@ public class BubbleStackView extends FrameLayout
private void updateExpandedBubble() {
mExpandedViewContainer.removeAllViews();
- if (mIsExpanded && mExpandedBubble != null
- && mExpandedBubble.getExpandedView() != null) {
- BubbleExpandedView bev = mExpandedBubble.getExpandedView();
+ BubbleExpandedView bev = getExpandedView();
+ if (mIsExpanded && bev != null) {
bev.setContentVisibility(false);
bev.setAnimating(!mIsExpansionAnimating);
mExpandedViewContainerMatrix.setScaleX(0f);
@@ -3187,9 +3393,8 @@ public class BubbleStackView extends FrameLayout
}
private void updateManageButtonListener() {
- if (mIsExpanded && mExpandedBubble != null
- && mExpandedBubble.getExpandedView() != null) {
- BubbleExpandedView bev = mExpandedBubble.getExpandedView();
+ BubbleExpandedView bev = getExpandedView();
+ if (mIsExpanded && bev != null) {
bev.setManageClickListener((view) -> {
showManageMenu(true /* show */);
});
@@ -3206,14 +3411,13 @@ public class BubbleStackView extends FrameLayout
* expanded bubble.
*/
private void screenshotAnimatingOutBubbleIntoSurface(Consumer<Boolean> onComplete) {
- if (!mIsExpanded || mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
+ final BubbleExpandedView animatingOutExpandedView = getExpandedView();
+ if (!mIsExpanded || animatingOutExpandedView == null) {
// You can't animate null.
onComplete.accept(false);
return;
}
- final BubbleExpandedView animatingOutExpandedView = mExpandedBubble.getExpandedView();
-
// Release the previous screenshot if it hasn't been released already.
if (mAnimatingOutBubbleBuffer != null) {
releaseAnimatingOutBubbleBuffer();
@@ -3245,8 +3449,7 @@ public class BubbleStackView extends FrameLayout
mAnimatingOutSurfaceContainer.setTranslationX(translationX);
mAnimatingOutSurfaceContainer.setTranslationY(0);
- final int[] taskViewLocation =
- mExpandedBubble.getExpandedView().getTaskViewLocationOnScreen();
+ final int[] taskViewLocation = animatingOutExpandedView.getTaskViewLocationOnScreen();
final int[] surfaceViewLocation = mAnimatingOutSurfaceView.getLocationOnScreen();
// Translate the surface to overlap the real TaskView.
@@ -3308,15 +3511,15 @@ public class BubbleStackView extends FrameLayout
int[] paddings = mPositioner.getExpandedViewContainerPadding(
mStackAnimationController.isStackOnLeftSide(), isOverflowExpanded);
mExpandedViewContainer.setPadding(paddings[0], paddings[1], paddings[2], paddings[3]);
- if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
+ BubbleExpandedView expandedView = getExpandedView();
+ if (expandedView != null) {
PointF p = mPositioner.getExpandedBubbleXY(getBubbleIndex(mExpandedBubble),
getState());
mExpandedViewContainer.setTranslationY(mPositioner.getExpandedViewY(mExpandedBubble,
mPositioner.showBubblesVertically() ? p.y : p.x));
mExpandedViewContainer.setTranslationX(0f);
- mExpandedBubble.getExpandedView().updateTaskViewContentWidth();
- mExpandedBubble.getExpandedView().updateView(
- mExpandedViewContainer.getLocationOnScreen());
+ expandedView.updateTaskViewContentWidth();
+ expandedView.updateView(mExpandedViewContainer.getLocationOnScreen());
updatePointerPosition(false /* forIme */);
}
@@ -3327,19 +3530,23 @@ public class BubbleStackView extends FrameLayout
* Updates whether each of the bubbles should show shadows. When collapsed & resting, only the
* visible bubbles (top 2) will show a shadow. When the stack is being dragged, everything
* shows a shadow. When an individual bubble is dragged out, it should show a shadow.
- */
- private void updateBubbleShadows(boolean showForAllBubbles) {
- int bubbleCount = getBubbleCount();
- for (int i = 0; i < bubbleCount; i++) {
- final float z = (mPositioner.getMaxBubbles() * mBubbleElevation) - i;
- BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
- boolean isDraggedOut = mMagnetizedObject != null
+ * The bubble overflow is a special case and never has a shadow as it's ordered below the
+ * rest of the bubbles and isn't visible unless the stack is expanded.
+ *
+ * @param isExpanded whether the stack will be expanded or not when the shadows are applied.
+ */
+ private void updateBubbleShadows(boolean isExpanded) {
+ final int childCount = mBubbleContainer.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
+ final boolean isOverflow = BubbleOverflow.KEY.equals(bv.getKey());
+ final boolean isDraggedOut = mMagnetizedObject != null
&& mMagnetizedObject.getUnderlyingObject().equals(bv);
- if (showForAllBubbles || isDraggedOut) {
- bv.setZ(z);
+ if (isDraggedOut) {
+ // If it's dragged out, it's above all the other bubbles
+ bv.setZ((mPositioner.getMaxBubbles() * mBubbleElevation) + 1);
} else {
- final float tz = i < NUM_VISIBLE_WHEN_RESTING ? z : 0f;
- bv.setZ(tz);
+ bv.setZ(mPositioner.getZTranslation(i, isOverflow, isExpanded));
}
}
}
@@ -3360,16 +3567,6 @@ public class BubbleStackView extends FrameLayout
}
}
- private void updateZOrder() {
- int bubbleCount = getBubbleCount();
- for (int i = 0; i < bubbleCount; i++) {
- BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
- bv.setZ(i < NUM_VISIBLE_WHEN_RESTING
- ? (mPositioner.getMaxBubbles() * mBubbleElevation) - i
- : 0f);
- }
- }
-
private void updateBadges(boolean setBadgeForCollapsedStack) {
int bubbleCount = getBubbleCount();
for (int i = 0; i < bubbleCount; i++) {
@@ -3395,7 +3592,8 @@ public class BubbleStackView extends FrameLayout
* the pointer is animated to the location.
*/
private void updatePointerPosition(boolean forIme) {
- if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
+ BubbleExpandedView expandedView = getExpandedView();
+ if (mExpandedBubble == null || expandedView == null) {
return;
}
int index = getBubbleIndex(mExpandedBubble);
@@ -3406,7 +3604,7 @@ public class BubbleStackView extends FrameLayout
float bubblePosition = mPositioner.showBubblesVertically()
? position.y
: position.x;
- mExpandedBubble.getExpandedView().setPointerPosition(bubblePosition,
+ expandedView.setPointerPosition(bubblePosition,
mStackOnLeftOrWillBe, forIme /* animate */);
}
@@ -3414,8 +3612,9 @@ public class BubbleStackView extends FrameLayout
* @return the number of bubbles in the stack view.
*/
public int getBubbleCount() {
- // Subtract 1 for the overflow button that is always in the bubble container.
- return mBubbleContainer.getChildCount() - 1;
+ final int childCount = mBubbleContainer.getChildCount();
+ // Subtract 1 for the overflow button if it's showing.
+ return mShowingOverflow ? childCount - 1 : childCount;
}
/**
@@ -3427,7 +3626,7 @@ public class BubbleStackView extends FrameLayout
*/
int getBubbleIndex(@Nullable BubbleViewProvider provider) {
if (provider == null) {
- return 0;
+ return -1;
}
return mBubbleContainer.indexOfChild(provider.getIconView());
}
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 26077cf7057b..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
@@ -37,8 +37,9 @@ import android.window.ScreenCapture.SynchronousScreenCaptureListener;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
+import com.android.wm.shell.shared.annotations.ExternalThread;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@@ -60,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 {
@@ -81,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. */
@@ -296,6 +297,15 @@ public interface Bubbles {
boolean sensitiveNotificationProtectionActive);
/**
+ * Determines whether Bubbles can show notifications.
+ *
+ * <p>Normally bubble notifications are shown by Bubbles, but in some cases the bubble
+ * notification is suppressed and should be shown by the Notifications pipeline as regular
+ * notifications.
+ */
+ boolean canShowBubbleNotification();
+
+ /**
* A listener to be notified of bubble state changes, used by launcher to render bubbles in
* its process.
*/
@@ -304,6 +314,12 @@ public interface Bubbles {
* Called when the bubbles state changes.
*/
void onBubbleStateChange(BubbleBarUpdate update);
+
+ /**
+ * Called when bubble bar should temporarily be animated to a new location.
+ * Does not result in a state change.
+ */
+ void animateBubbleBarLocation(BubbleBarLocation location);
}
/** Listener to find out about stack expansion / collapse events. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
index 7a5afec934f5..1db556c04180 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
@@ -19,6 +19,7 @@ package com.android.wm.shell.bubbles;
import android.content.Intent;
import android.graphics.Rect;
import com.android.wm.shell.bubbles.IBubblesListener;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
/**
* Interface that is exposed to remote callers (launcher) to manipulate the bubbles feature when
@@ -30,16 +31,21 @@ interface IBubbles {
oneway void unregisterBubbleListener(in IBubblesListener listener) = 2;
- oneway void showBubble(in String key, in Rect bubbleBarBounds) = 3;
+ oneway void showBubble(in String key, in int topOnScreen) = 3;
- oneway void removeBubble(in String key) = 4;
+ oneway void dragBubbleToDismiss(in String key) = 4;
oneway void removeAllBubbles() = 5;
oneway void collapseBubbles() = 6;
- oneway void onBubbleDrag(in String key, in boolean isBeingDragged) = 7;
+ oneway void startBubbleDrag(in String key) = 7;
oneway void showUserEducation(in int positionX, in int positionY) = 8;
+ oneway void setBubbleBarLocation(in BubbleBarLocation location) = 9;
+
+ oneway void updateBubbleBarTopOnScreen(in int topOnScreen) = 10;
+
+ oneway void stopBubbleDrag(in BubbleBarLocation location, in int topOnScreen) = 11;
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl
index e48f8d5f1c84..14d29cd887bb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubblesListener.aidl
@@ -15,8 +15,9 @@
*/
package com.android.wm.shell.bubbles;
-import android.os.Bundle;
+import android.os.Bundle;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
/**
* Listener interface that Launcher attaches to SystemUI to get bubbles callbacks.
*/
@@ -26,4 +27,10 @@ oneway interface IBubblesListener {
* Called when the bubbles state changes.
*/
void onBubbleStateChange(in Bundle update);
+
+ /**
+ * Called when bubble bar should temporarily be animated to a new location.
+ * Does not result in a state change.
+ */
+ void animateBubbleBarLocation(in BubbleBarLocation location);
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
index 7798aa753aa2..f925eaef2c77 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
@@ -34,12 +34,12 @@ import androidx.dynamicanimation.animation.SpringForce;
import com.android.wm.shell.R;
import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.animation.PhysicsAnimator;
import com.android.wm.shell.bubbles.BadgedImageView;
import com.android.wm.shell.bubbles.BubbleOverflow;
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.BubbleStackView;
import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
import com.google.android.collect.Sets;
@@ -356,7 +356,6 @@ public class ExpandedAnimationController
MagnetizedObject.MagnetListener listener) {
mLayout.cancelAnimationsOnView(bubble);
- bubble.setTranslationZ(Short.MAX_VALUE);
mMagnetizedBubbleDraggingOut = new MagnetizedObject<View>(
mLayout.getContext(), bubble,
DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
@@ -460,6 +459,7 @@ public class ExpandedAnimationController
/**
* Snaps a bubble back to its position within the bubble row, and animates the rest of the
* bubbles to accommodate it if it was previously dragged out past the threshold.
+ * Only happens while the stack is expanded.
*/
public void snapBubbleBack(View bubbleView, float velX, float velY) {
if (mLayout == null) {
@@ -467,10 +467,14 @@ public class ExpandedAnimationController
}
final int index = mLayout.indexOfChild(bubbleView);
final PointF p = mPositioner.getExpandedBubbleXY(index, mBubbleStackView.getState());
+ // overflow is not draggable so it's never the overflow
+ final float zTranslation = mPositioner.getZTranslation(index,
+ false /* isOverflow */,
+ true /* isExpanded */);
animationForChildAtIndex(index)
- .position(p.x, p.y)
+ .position(p.x, p.y, zTranslation)
.withPositionStartVelocities(velX, velY)
- .start(() -> bubbleView.setTranslationZ(0f) /* after */);
+ .start();
mMagnetizedBubbleDraggingOut = null;
@@ -509,6 +513,7 @@ public class ExpandedAnimationController
return Sets.newHashSet(
DynamicAnimation.TRANSLATION_X,
DynamicAnimation.TRANSLATION_Y,
+ DynamicAnimation.TRANSLATION_Z,
DynamicAnimation.SCALE_X,
DynamicAnimation.SCALE_Y,
DynamicAnimation.ALPHA);
@@ -614,6 +619,14 @@ public class ExpandedAnimationController
}
}
+ /**
+ * Call to update the bubble positions after an orientation change.
+ */
+ public void onOrientationChanged() {
+ if (mLayout == null) return;
+ updateBubblePositions();
+ }
+
private void updateBubblePositions() {
if (mAnimatingExpand || mAnimatingCollapse) {
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java
index 8a33780bc8d5..41755293f382 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationController.java
@@ -15,6 +15,8 @@
*/
package com.android.wm.shell.bubbles.animation;
+import android.graphics.PointF;
+
import com.android.wm.shell.bubbles.BubbleExpandedView;
/**
@@ -55,8 +57,9 @@ public interface ExpandedViewAnimationController {
* @param startStackCollapse runnable that is triggered when bubbles can start moving back to
* their collapsed location
* @param after runnable to run after animation is complete
+ * @param collapsePosition the position on screen the stack will collapse to
*/
- void animateCollapse(Runnable startStackCollapse, Runnable after);
+ void animateCollapse(Runnable startStackCollapse, Runnable after, PointF collapsePosition);
/**
* Animate the view back to fully expanded state.
@@ -69,6 +72,22 @@ public interface ExpandedViewAnimationController {
void animateForImeVisibilityChange(boolean visible);
/**
+ * Whether this controller should also animate the expansion for the bubble
+ */
+ boolean shouldAnimateExpansion();
+
+ /**
+ * Animate the expansion of the bubble.
+ *
+ * @param startDelayMillis how long to delay starting the expansion animation
+ * @param after runnable to run after the animation is complete
+ * @param collapsePosition the position on screen the stack will collapse to (and expand from)
+ * @param bubblePosition the position of the bubble on screen that the view is associated with
+ */
+ void animateExpansion(long startDelayMillis, Runnable after, PointF collapsePosition,
+ PointF bubblePosition);
+
+ /**
* Reset the view to fully expanded state
*/
void reset();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java
index e43609fe8ff0..aa4129a14dbc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedViewAnimationControllerImpl.java
@@ -28,6 +28,7 @@ import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
+import android.graphics.PointF;
import android.view.HapticFeedbackConstants;
import android.view.ViewConfiguration;
@@ -187,9 +188,11 @@ public class ExpandedViewAnimationControllerImpl implements ExpandedViewAnimatio
}
@Override
- public void animateCollapse(Runnable startStackCollapse, Runnable after) {
- ProtoLog.d(WM_SHELL_BUBBLES, "expandedView animate collapse swipeVel=%f minFlingVel=%d",
- mSwipeUpVelocity, mMinFlingVelocity);
+ public void animateCollapse(Runnable startStackCollapse, Runnable after,
+ PointF collapsePosition) {
+ ProtoLog.d(WM_SHELL_BUBBLES, "expandedView animate collapse swipeVel=%f minFlingVel=%d"
+ + " collapsePosition=%f,%f", mSwipeUpVelocity, mMinFlingVelocity,
+ collapsePosition.x, collapsePosition.y);
if (mExpandedView != null) {
// Mark it as animating immediately to avoid updates to the view before animation starts
mExpandedView.setAnimating(true);
@@ -274,6 +277,17 @@ public class ExpandedViewAnimationControllerImpl implements ExpandedViewAnimatio
}
@Override
+ public boolean shouldAnimateExpansion() {
+ return false;
+ }
+
+ @Override
+ public void animateExpansion(long startDelayMillis, Runnable after, PointF collapsePosition,
+ PointF bubblePosition) {
+ // TODO - animate
+ }
+
+ @Override
public void reset() {
ProtoLog.d(WM_SHELL_BUBBLES, "reset expandedView collapsed state");
if (mExpandedView == null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java
index ed00da848a14..06305f02e41c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayout.java
@@ -378,6 +378,8 @@ public class PhysicsAnimationLayout extends FrameLayout {
}
final int oldIndex = indexOfChild(view);
+ if (oldIndex == index) return;
+
super.removeView(view);
if (view.getParent() != null) {
// View still has a parent. This could have been added as a transient view.
@@ -417,7 +419,8 @@ public class PhysicsAnimationLayout extends FrameLayout {
// be animating in this case, even if the physics animations haven't been started yet.
final boolean isTranslation =
property.equals(DynamicAnimation.TRANSLATION_X)
- || property.equals(DynamicAnimation.TRANSLATION_Y);
+ || property.equals(DynamicAnimation.TRANSLATION_Y)
+ || property.equals(DynamicAnimation.TRANSLATION_Z);
if (isTranslation && targetAnimator != null && targetAnimator.isRunning()) {
return true;
}
@@ -493,6 +496,8 @@ public class PhysicsAnimationLayout extends FrameLayout {
return "TRANSLATION_X";
} else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
return "TRANSLATION_Y";
+ } else if (property.equals(DynamicAnimation.TRANSLATION_Z)) {
+ return "TRANSLATION_Z";
} else if (property.equals(DynamicAnimation.SCALE_X)) {
return "SCALE_X";
} else if (property.equals(DynamicAnimation.SCALE_Y)) {
@@ -596,6 +601,8 @@ public class PhysicsAnimationLayout extends FrameLayout {
return R.id.translation_x_dynamicanimation_tag;
} else if (property.equals(DynamicAnimation.TRANSLATION_Y)) {
return R.id.translation_y_dynamicanimation_tag;
+ } else if (property.equals(DynamicAnimation.TRANSLATION_Z)) {
+ return R.id.translation_z_dynamicanimation_tag;
} else if (property.equals(DynamicAnimation.SCALE_X)) {
return R.id.scale_x_dynamicanimation_tag;
} else if (property.equals(DynamicAnimation.SCALE_Y)) {
@@ -761,6 +768,12 @@ public class PhysicsAnimationLayout extends FrameLayout {
return property(DynamicAnimation.TRANSLATION_X, translationX, endActions);
}
+ /** Animate the view's translationZ value to the provided value. */
+ public PhysicsPropertyAnimator translationZ(float translationZ, Runnable... endActions) {
+ mPathAnimator = null; // We aren't using the path anymore if we're translating.
+ return property(DynamicAnimation.TRANSLATION_Z, translationZ, endActions);
+ }
+
/** Set the view's translationX value to 'from', then animate it to the given value. */
public PhysicsPropertyAnimator translationX(
float from, float to, Runnable... endActions) {
@@ -783,13 +796,14 @@ public class PhysicsAnimationLayout extends FrameLayout {
/**
* Animate the view's translationX and translationY values, and call the end actions only
- * once both TRANSLATION_X and TRANSLATION_Y animations have completed.
+ * once both TRANSLATION_X, TRANSLATION_Y and TRANSLATION_Z animations have completed.
*/
- public PhysicsPropertyAnimator position(
- float translationX, float translationY, Runnable... endActions) {
+ public PhysicsPropertyAnimator position(float translationX, float translationY,
+ float translationZ, Runnable... endActions) {
mPositionEndActions = endActions;
translationX(translationX);
- return translationY(translationY);
+ translationY(translationY);
+ return translationZ(translationZ);
}
/**
@@ -843,10 +857,13 @@ public class PhysicsAnimationLayout extends FrameLayout {
private void clearTranslationValues() {
mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_X);
mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_Y);
+ mAnimatedProperties.remove(DynamicAnimation.TRANSLATION_Z);
mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_X);
mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_Y);
+ mInitialPropertyValues.remove(DynamicAnimation.TRANSLATION_Z);
mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_X);
mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_Y);
+ mEndActionForProperty.remove(DynamicAnimation.TRANSLATION_Z);
}
/** Animate the view's scaleX value to the provided value. */
@@ -937,15 +954,19 @@ public class PhysicsAnimationLayout extends FrameLayout {
}, propertiesArray);
}
- // If we used position-specific end actions, we'll need to listen for both TRANSLATION_X
- // and TRANSLATION_Y animations ending, and call them once both have finished.
+ // If we used position-specific end actions, we'll need to listen for TRANSLATION_X
+ // TRANSLATION_Y and TRANSLATION_Z animations ending, and call them once both have
+ // finished.
if (mPositionEndActions != null) {
final SpringAnimation translationXAnim =
getSpringAnimationFromView(DynamicAnimation.TRANSLATION_X, mView);
final SpringAnimation translationYAnim =
getSpringAnimationFromView(DynamicAnimation.TRANSLATION_Y, mView);
- final Runnable waitForBothXAndY = () -> {
- if (!translationXAnim.isRunning() && !translationYAnim.isRunning()) {
+ final SpringAnimation translationZAnim =
+ getSpringAnimationFromView(DynamicAnimation.TRANSLATION_Z, mView);
+ final Runnable waitForXYZ = () -> {
+ if (!translationXAnim.isRunning() && !translationYAnim.isRunning()
+ && !translationZAnim.isRunning()) {
if (mPositionEndActions != null) {
for (Runnable callback : mPositionEndActions) {
callback.run();
@@ -957,9 +978,11 @@ public class PhysicsAnimationLayout extends FrameLayout {
};
mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_X,
- new Runnable[]{waitForBothXAndY});
+ new Runnable[]{waitForXYZ});
mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_Y,
- new Runnable[]{waitForBothXAndY});
+ new Runnable[]{waitForXYZ});
+ mEndActionsForProperty.put(DynamicAnimation.TRANSLATION_Z,
+ new Runnable[]{waitForXYZ});
}
if (mPathAnimator != null) {
@@ -970,9 +993,10 @@ public class PhysicsAnimationLayout extends FrameLayout {
for (DynamicAnimation.ViewProperty property : properties) {
// Don't start translation animations if we're using a path animator, the update
// listeners added to that animator will take care of that.
- if (mPathAnimator != null
- && (property.equals(DynamicAnimation.TRANSLATION_X)
- || property.equals(DynamicAnimation.TRANSLATION_Y))) {
+ boolean isTranslationProperty = property.equals(DynamicAnimation.TRANSLATION_X)
+ || property.equals(DynamicAnimation.TRANSLATION_Y)
+ || property.equals(DynamicAnimation.TRANSLATION_Z);
+ if (mPathAnimator != null && isTranslationProperty) {
return;
}
@@ -1004,6 +1028,7 @@ public class PhysicsAnimationLayout extends FrameLayout {
if (mPathAnimator != null) {
animatedProperties.add(DynamicAnimation.TRANSLATION_X);
animatedProperties.add(DynamicAnimation.TRANSLATION_Y);
+ animatedProperties.add(DynamicAnimation.TRANSLATION_Z);
}
return animatedProperties;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
index bb0dd95b042f..47d4d07500d5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
@@ -38,12 +38,12 @@ import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.wm.shell.R;
-import com.android.wm.shell.animation.PhysicsAnimator;
import com.android.wm.shell.bubbles.BadgedImageView;
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.BubbleStackView;
import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
import com.google.android.collect.Sets;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
index 8946f41e96a7..8e58db198b13 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -43,12 +43,12 @@ import android.widget.FrameLayout;
import androidx.annotation.Nullable;
import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.animation.PhysicsAnimator;
import com.android.wm.shell.bubbles.BubbleOverflow;
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.BubbleViewProvider;
import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix;
import com.android.wm.shell.common.magnetictarget.MagnetizedObject.MagneticTarget;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
/**
* Helper class to animate a {@link BubbleBarExpandedView} on a bubble.
@@ -166,13 +166,8 @@ public class BubbleBarAnimationHelper {
bbev.setTaskViewAlpha(0f);
bbev.setVisibility(VISIBLE);
- // Set the pivot point for the scale, so the view animates out from the bubble bar.
- Rect bubbleBarBounds = mPositioner.getBubbleBarBounds();
- mExpandedViewContainerMatrix.setScale(
- 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
- 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT,
- bubbleBarBounds.centerX(),
- bubbleBarBounds.top);
+ setScaleFromBubbleBar(mExpandedViewContainerMatrix,
+ 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT);
bbev.setAnimationMatrix(mExpandedViewContainerMatrix);
@@ -214,8 +209,8 @@ public class BubbleBarAnimationHelper {
}
bbev.setScaleX(1f);
bbev.setScaleY(1f);
- mExpandedViewContainerMatrix.setScaleX(1f);
- mExpandedViewContainerMatrix.setScaleY(1f);
+
+ setScaleFromBubbleBar(mExpandedViewContainerMatrix, 1f);
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
@@ -240,6 +235,14 @@ public class BubbleBarAnimationHelper {
mExpandedViewAlphaAnimator.reverse();
}
+ private void setScaleFromBubbleBar(AnimatableScaleMatrix matrix, float scale) {
+ // Set the pivot point for the scale, so the view animates out from the bubble bar.
+ Rect availableRect = mPositioner.getAvailableRect();
+ float pivotX = mPositioner.isBubbleBarOnLeft() ? availableRect.left : availableRect.right;
+ float pivotY = mPositioner.getBubbleBarTopOnScreen();
+ matrix.setScale(scale, scale, pivotX, pivotY);
+ }
+
/**
* Animate the expanded bubble when it is being dragged
*/
@@ -477,7 +480,7 @@ public class BubbleBarAnimationHelper {
private Point getExpandedViewRestPosition(Size size) {
final int padding = mPositioner.getBubbleBarExpandedViewPadding();
Point point = new Point();
- if (mLayerView.isOnLeft()) {
+ if (mPositioner.isBubbleBarOnLeft()) {
point.x = mPositioner.getInsets().left + padding;
} else {
point.x = mPositioner.getAvailableRect().width() - size.getWidth() - padding;
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/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt
index 7d37d6068dfb..fa1091c63d00 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt
@@ -19,6 +19,7 @@ package com.android.wm.shell.bubbles.bar
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.View
+import com.android.wm.shell.bubbles.BubblePositioner
import com.android.wm.shell.common.bubbles.DismissView
import com.android.wm.shell.common.bubbles.RelativeTouchListener
import com.android.wm.shell.common.magnetictarget.MagnetizedObject
@@ -29,7 +30,9 @@ class BubbleBarExpandedViewDragController(
private val expandedView: BubbleBarExpandedView,
private val dismissView: DismissView,
private val animationHelper: BubbleBarAnimationHelper,
- private val onDismissed: () -> Unit
+ private val bubblePositioner: BubblePositioner,
+ private val pinController: BubbleExpandedViewPinController,
+ private val dragListener: DragListener
) {
var isStuckToDismiss: Boolean = false
@@ -45,11 +48,11 @@ class BubbleBarExpandedViewDragController(
magnetizedExpandedView.magnetListener = MagnetListener()
magnetizedExpandedView.animateStuckToTarget =
{
- target: MagnetizedObject.MagneticTarget,
- _: Float,
- _: Float,
- _: Boolean,
- after: (() -> Unit)? ->
+ target: MagnetizedObject.MagneticTarget,
+ _: Float,
+ _: Float,
+ _: Boolean,
+ after: (() -> Unit)? ->
animationHelper.animateIntoTarget(target, after)
}
@@ -73,13 +76,25 @@ class BubbleBarExpandedViewDragController(
}
}
+ /** Listener to get notified about drag events */
+ interface DragListener {
+ /**
+ * Bubble bar was released
+ *
+ * @param inDismiss `true` if view was release in dismiss target
+ */
+ fun onReleased(inDismiss: Boolean)
+ }
+
private inner class HandleDragListener : RelativeTouchListener() {
private var isMoving = false
override fun onDown(v: View, ev: MotionEvent): Boolean {
// While animating, don't allow new touch events
- return !expandedView.isAnimating
+ if (expandedView.isAnimating) return false
+ pinController.onDragStart(bubblePositioner.isBubbleBarOnLeft)
+ return true
}
override fun onMove(
@@ -97,6 +112,7 @@ class BubbleBarExpandedViewDragController(
expandedView.translationX = expandedViewInitialTranslationX + dx
expandedView.translationY = expandedViewInitialTranslationY + dy
dismissView.show()
+ pinController.onDragUpdate(ev.rawX, ev.rawY)
}
override fun onUp(
@@ -113,11 +129,14 @@ class BubbleBarExpandedViewDragController(
}
override fun onCancel(v: View, ev: MotionEvent, viewInitialX: Float, viewInitialY: Float) {
+ isStuckToDismiss = false
finishDrag()
}
private fun finishDrag() {
if (!isStuckToDismiss) {
+ pinController.onDragEnd()
+ dragListener.onReleased(inDismiss = false)
animationHelper.animateToRestPosition()
dismissView.hide()
}
@@ -127,30 +146,31 @@ class BubbleBarExpandedViewDragController(
private inner class MagnetListener : MagnetizedObject.MagnetListener {
override fun onStuckToTarget(
- target: MagnetizedObject.MagneticTarget,
- draggedObject: MagnetizedObject<*>
+ target: MagnetizedObject.MagneticTarget,
+ draggedObject: MagnetizedObject<*>
) {
isStuckToDismiss = true
+ pinController.onStuckToDismissTarget()
}
override fun onUnstuckFromTarget(
- target: MagnetizedObject.MagneticTarget,
- draggedObject: MagnetizedObject<*>,
- velX: Float,
- velY: Float,
- wasFlungOut: Boolean
+ target: MagnetizedObject.MagneticTarget,
+ draggedObject: MagnetizedObject<*>,
+ velX: Float,
+ velY: Float,
+ wasFlungOut: Boolean
) {
isStuckToDismiss = false
animationHelper.animateUnstuckFromDismissView(target)
}
override fun onReleasedInTarget(
- target: MagnetizedObject.MagneticTarget,
- draggedObject: MagnetizedObject<*>
+ target: MagnetizedObject.MagneticTarget,
+ draggedObject: MagnetizedObject<*>
) {
- onDismissed()
+ dragListener.onReleased(inDismiss = true)
+ pinController.onDragEnd()
dismissView.hide()
}
}
}
-
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java
index 2b7a0706b4de..d54a6b002e43 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarHandleView.java
@@ -37,15 +37,11 @@ import com.android.wm.shell.R;
*/
public class BubbleBarHandleView extends View {
private static final long COLOR_CHANGE_DURATION = 120;
-
- // The handle view is currently rendered as 3 evenly spaced dots.
- private int mDotSize;
- private int mDotSpacing;
// Path used to draw the dots
private final Path mPath = new Path();
- private @ColorInt int mHandleLightColor;
- private @ColorInt int mHandleDarkColor;
+ private final @ColorInt int mHandleLightColor;
+ private final @ColorInt int mHandleDarkColor;
private @Nullable ObjectAnimator mColorChangeAnim;
public BubbleBarHandleView(Context context) {
@@ -63,10 +59,8 @@ public class BubbleBarHandleView extends View {
public BubbleBarHandleView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- mDotSize = getResources().getDimensionPixelSize(
- R.dimen.bubble_bar_expanded_view_caption_dot_size);
- mDotSpacing = getResources().getDimensionPixelSize(
- R.dimen.bubble_bar_expanded_view_caption_dot_spacing);
+ final int handleHeight = getResources().getDimensionPixelSize(
+ R.dimen.bubble_bar_expanded_view_handle_height);
mHandleLightColor = ContextCompat.getColor(getContext(),
R.color.bubble_bar_expanded_view_handle_light);
mHandleDarkColor = ContextCompat.getColor(getContext(),
@@ -76,27 +70,13 @@ public class BubbleBarHandleView extends View {
setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
- final int handleCenterX = view.getWidth() / 2;
final int handleCenterY = view.getHeight() / 2;
- final int handleTotalWidth = mDotSize * 3 + mDotSpacing * 2;
- final int handleLeft = handleCenterX - handleTotalWidth / 2;
- final int handleTop = handleCenterY - mDotSize / 2;
- final int handleBottom = handleTop + mDotSize;
- RectF dot1 = new RectF(
- handleLeft, handleTop,
- handleLeft + mDotSize, handleBottom);
- RectF dot2 = new RectF(
- dot1.right + mDotSpacing, handleTop,
- dot1.right + mDotSpacing + mDotSize, handleBottom
- );
- RectF dot3 = new RectF(
- dot2.right + mDotSpacing, handleTop,
- dot2.right + mDotSpacing + mDotSize, handleBottom
- );
+ final int handleTop = handleCenterY - handleHeight / 2;
+ final int handleBottom = handleTop + handleHeight;
+ final int radius = handleHeight / 2;
+ RectF handle = new RectF(/* left = */ 0, handleTop, view.getWidth(), handleBottom);
mPath.reset();
- mPath.addOval(dot1, Path.Direction.CW);
- mPath.addOval(dot2, Path.Direction.CW);
- mPath.addOval(dot3, Path.Direction.CW);
+ mPath.addRoundRect(handle, radius, radius, Path.Direction.CW);
outline.setPath(mPath);
}
});
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 42799d975e1b..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
@@ -33,7 +33,8 @@ import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.FrameLayout;
-import com.android.wm.shell.R;
+import androidx.annotation.NonNull;
+
import com.android.wm.shell.bubbles.Bubble;
import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.BubbleData;
@@ -42,6 +43,9 @@ import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.BubbleViewProvider;
import com.android.wm.shell.bubbles.DeviceConfig;
import com.android.wm.shell.bubbles.DismissViewUtils;
+import com.android.wm.shell.bubbles.bar.BubbleBarExpandedViewDragController.DragListener;
+import com.android.wm.shell.common.bubbles.BaseBubblePinController;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import com.android.wm.shell.common.bubbles.DismissView;
import kotlin.Unit;
@@ -68,6 +72,7 @@ public class BubbleBarLayerView extends FrameLayout
private final BubbleBarAnimationHelper mAnimationHelper;
private final BubbleEducationViewController mEducationViewController;
private final View mScrimView;
+ private final BubbleExpandedViewPinController mBubbleExpandedViewPinController;
@Nullable
private BubbleViewProvider mExpandedBubble;
@@ -112,7 +117,22 @@ public class BubbleBarLayerView extends FrameLayout
setUpDismissView();
- setOnClickListener(view -> hideMenuOrCollapse());
+ mBubbleExpandedViewPinController = new BubbleExpandedViewPinController(
+ context, this, mPositioner);
+ mBubbleExpandedViewPinController.setListener(
+ new BaseBubblePinController.LocationChangeListener() {
+ @Override
+ public void onChange(@NonNull BubbleBarLocation bubbleBarLocation) {
+ mBubbleController.animateBubbleBarLocation(bubbleBarLocation);
+ }
+
+ @Override
+ public void onRelease(@NonNull BubbleBarLocation location) {
+ mBubbleController.setBubbleBarLocation(location);
+ }
+ });
+
+ setOnClickListener(view -> hideModalOrCollapse());
}
@Override
@@ -155,12 +175,6 @@ public class BubbleBarLayerView extends FrameLayout
return mIsExpanded;
}
- // TODO(b/313661121) - when dragging is implemented, check user setting first
- /** Whether the expanded view is positioned on the left or right side of the screen. */
- public boolean isOnLeft() {
- return getLayoutDirection() == LAYOUT_DIRECTION_RTL;
- }
-
/** Shows the expanded view of the provided bubble. */
public void showExpandedView(BubbleViewProvider b) {
BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView();
@@ -203,19 +217,22 @@ public class BubbleBarLayerView extends FrameLayout
@Override
public void onBackPressed() {
- hideMenuOrCollapse();
+ hideModalOrCollapse();
}
});
+ DragListener dragListener = inDismiss -> {
+ if (inDismiss && mExpandedBubble != null) {
+ mBubbleController.dismissBubble(mExpandedBubble.getKey(), DISMISS_USER_GESTURE);
+ }
+ };
mDragController = new BubbleBarExpandedViewDragController(
mExpandedView,
mDismissView,
mAnimationHelper,
- () -> {
- mBubbleController.dismissBubble(mExpandedBubble.getKey(),
- DISMISS_USER_GESTURE);
- return Unit.INSTANCE;
- });
+ mPositioner,
+ mBubbleExpandedViewPinController,
+ dragListener);
addView(mExpandedView, new LayoutParams(width, height, Gravity.LEFT));
}
@@ -324,40 +341,40 @@ public class BubbleBarLayerView extends FrameLayout
}
mDismissView = new DismissView(getContext());
DismissViewUtils.setup(mDismissView);
- int elevation = getResources().getDimensionPixelSize(R.dimen.bubble_elevation);
-
addView(mDismissView);
- mDismissView.setElevation(elevation);
}
- /** 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. */
- private void updateExpandedView() {
- if (mExpandedView == null) return;
+ public void updateExpandedView() {
+ if (mExpandedView == null || mExpandedBubble == null) return;
boolean isOverflowExpanded = mExpandedBubble.getKey().equals(BubbleOverflow.KEY);
- final int padding = mPositioner.getBubbleBarExpandedViewPadding();
- final int width = mPositioner.getExpandedViewWidthForBubbleBar(isOverflowExpanded);
- final int height = mPositioner.getExpandedViewHeightForBubbleBar(isOverflowExpanded);
+ mPositioner.getBubbleBarExpandedViewBounds(mPositioner.isBubbleBarOnLeft(),
+ isOverflowExpanded, mTempRect);
FrameLayout.LayoutParams lp = (LayoutParams) mExpandedView.getLayoutParams();
- lp.width = width;
- lp.height = height;
+ lp.width = mTempRect.width();
+ lp.height = mTempRect.height();
mExpandedView.setLayoutParams(lp);
- if (isOnLeft()) {
- mExpandedView.setX(mPositioner.getInsets().left + padding);
- } else {
- mExpandedView.setX(mPositioner.getAvailableRect().width() - width - padding);
- }
- mExpandedView.setY(mPositioner.getExpandedViewBottomForBubbleBar() - height);
+ mExpandedView.setX(mTempRect.left);
+ mExpandedView.setY(mTempRect.top);
mExpandedView.updateLocation();
}
@@ -386,4 +403,5 @@ public class BubbleBarLayerView extends FrameLayout
outRegion.op(mTempRect, Region.Op.UNION);
}
}
+
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
index 81e7582e0dba..02918db124e3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
@@ -29,8 +29,8 @@ import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.wm.shell.R;
-import com.android.wm.shell.animation.PhysicsAnimator;
import com.android.wm.shell.bubbles.Bubble;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
import java.util.ArrayList;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt
index ee552ae204b8..e108f7be48c7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt
@@ -28,7 +28,6 @@ import androidx.core.view.doOnLayout
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.SpringForce
import com.android.wm.shell.R
-import com.android.wm.shell.animation.PhysicsAnimator
import com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION
import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES
import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME
@@ -37,6 +36,7 @@ import com.android.wm.shell.bubbles.BubbleViewProvider
import com.android.wm.shell.bubbles.setup
import com.android.wm.shell.common.bubbles.BubblePopupDrawable
import com.android.wm.shell.common.bubbles.BubblePopupView
+import com.android.wm.shell.shared.animation.PhysicsAnimator
import kotlin.math.roundToInt
/** Manages bubble education presentation and animation */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt
new file mode 100644
index 000000000000..651bf022e07d
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinController.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.bar
+
+import android.content.Context
+import android.graphics.Point
+import android.graphics.Rect
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.FrameLayout
+import androidx.core.view.updateLayoutParams
+import com.android.wm.shell.R
+import com.android.wm.shell.bubbles.BubblePositioner
+import com.android.wm.shell.common.bubbles.BaseBubblePinController
+import com.android.wm.shell.common.bubbles.BubbleBarLocation
+
+/**
+ * Controller to manage pinning bubble bar to left or right when dragging starts from the bubble bar
+ * expanded view
+ */
+class BubbleExpandedViewPinController(
+ private val context: Context,
+ private val container: FrameLayout,
+ private val positioner: BubblePositioner
+) : BaseBubblePinController({ positioner.availableRect.let { Point(it.width(), it.height()) } }) {
+
+ private var dropTargetView: View? = null
+ private val tempRect: Rect by lazy(LazyThreadSafetyMode.NONE) { Rect() }
+
+ private val exclRectWidth: Float by lazy {
+ context.resources.getDimension(R.dimen.bubble_bar_dismiss_zone_width)
+ }
+
+ private val exclRectHeight: Float by lazy {
+ context.resources.getDimension(R.dimen.bubble_bar_dismiss_zone_height)
+ }
+
+ override fun getExclusionRectWidth(): Float {
+ return exclRectWidth
+ }
+
+ override fun getExclusionRectHeight(): Float {
+ return exclRectHeight
+ }
+
+ override fun createDropTargetView(): View {
+ return LayoutInflater.from(context)
+ .inflate(R.layout.bubble_bar_drop_target, container, false /* attachToRoot */)
+ .also { view: View ->
+ dropTargetView = view
+ // Add at index 0 to ensure it does not cover the bubble
+ container.addView(view, 0)
+ }
+ }
+
+ override fun getDropTargetView(): View? {
+ return dropTargetView
+ }
+
+ override fun removeDropTargetView(view: View) {
+ container.removeView(view)
+ dropTargetView = null
+ }
+
+ override fun updateLocation(location: BubbleBarLocation) {
+ val view = dropTargetView ?: return
+ positioner.getBubbleBarExpandedViewBounds(
+ location.isOnLeft(view.isLayoutRtl),
+ false /* isOverflowExpanded */,
+ tempRect
+ )
+ view.updateLayoutParams<FrameLayout.LayoutParams> {
+ width = tempRect.width()
+ height = tempRect.height()
+ }
+ view.x = tempRect.left.toFloat()
+ view.y = tempRect.top.toFloat()
+ }
+}
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/DevicePostureController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java
index 8b4ac1a8dc79..d17e8620ff12 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DevicePostureController.java
@@ -107,7 +107,7 @@ public class DevicePostureController {
DeviceStateManager.class);
if (deviceStateManager != null) {
deviceStateManager.registerCallback(mMainExecutor, state -> onDevicePostureChanged(
- mDeviceStateToPostureMap.get(state, DEVICE_POSTURE_UNKNOWN)));
+ mDeviceStateToPostureMap.get(state.getIdentifier(), DEVICE_POSTURE_UNKNOWN)));
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
index b828aac39040..2873d58439cd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
@@ -28,7 +28,7 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.BinderThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ShellInit;
import java.util.concurrent.CopyOnWriteArrayList;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
index 8353900be0ef..dcbc72ab0d32 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
@@ -34,7 +34,7 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.BinderThread;
import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ShellInit;
import java.util.ArrayList;
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 ca06024a9adb..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
@@ -30,7 +30,7 @@ import android.view.inputmethod.ImeTracker;
import androidx.annotation.BinderThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ShellInit;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -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/MultiInstanceHelper.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
index 4c34971c4fb1..9e8dfb5f0c6f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/MultiInstanceHelper.kt
@@ -21,11 +21,9 @@ import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.os.UserHandle
-import android.view.WindowManager
import android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI
import com.android.internal.annotations.VisibleForTesting
import com.android.wm.shell.R
-import com.android.wm.shell.protolog.ShellProtoLogGroup
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL
import com.android.wm.shell.util.KtProtoLog
import java.util.Arrays
@@ -37,7 +35,8 @@ class MultiInstanceHelper @JvmOverloads constructor(
private val context: Context,
private val packageManager: PackageManager,
private val staticAppsSupportingMultiInstance: Array<String> = context.resources
- .getStringArray(R.array.config_appsSupportMultiInstancesSplit)) {
+ .getStringArray(R.array.config_appsSupportMultiInstancesSplit),
+ private val supportsMultiInstanceProperty: Boolean) {
/**
* Returns whether a specific component desires to be launched in multiple instances.
@@ -59,6 +58,11 @@ class MultiInstanceHelper @JvmOverloads constructor(
}
}
+ if (!supportsMultiInstanceProperty) {
+ // If not checking the multi-instance properties, then return early
+ return false;
+ }
+
// Check the activity property first
try {
val activityProp = packageManager.getProperty(
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/SyncTransactionQueue.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java
index 4c0281dcc517..e261d92bda5c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SyncTransactionQueue.java
@@ -16,6 +16,8 @@
package com.android.wm.shell.common;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL;
+
import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.os.RemoteException;
@@ -26,6 +28,7 @@ import android.window.WindowContainerTransaction;
import android.window.WindowContainerTransactionCallback;
import android.window.WindowOrganizer;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.transition.LegacyTransitions;
import java.util.ArrayList;
@@ -204,6 +207,7 @@ public final class SyncTransactionQueue {
@Override
public void onTransactionReady(int id,
@NonNull SurfaceControl.Transaction t) {
+ ProtoLog.v(WM_SHELL, "SyncTransactionQueue.onTransactionReady(): syncId=%d", id);
mMainExecutor.execute(() -> {
synchronized (mQueue) {
if (mId != id) {
@@ -223,6 +227,8 @@ public final class SyncTransactionQueue {
Slog.e(TAG, "Error sending callback to legacy transition: " + mId, e);
}
} else {
+ ProtoLog.v(WM_SHELL,
+ "SyncTransactionQueue.onTransactionReady(): syncId=%d apply", id);
t.apply();
t.close();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
index e4cf6d13cb1f..ef33b3830e45 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
@@ -48,6 +48,7 @@ import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
import android.view.inputmethod.ImeTracker;
+import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
import android.window.InputTransferToken;
@@ -348,11 +349,11 @@ public class SystemWindows {
public void resized(ClientWindowFrames frames, boolean reportDraw,
MergedConfiguration newMergedConfiguration, InsetsState insetsState,
boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId,
- boolean dragResizing) {}
+ boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) {}
@Override
public void insetsControlChanged(InsetsState insetsState,
- InsetsSourceControl[] activeControls) {}
+ InsetsSourceControl.Array activeControls) {}
@Override
public void showInsets(int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) {}
@@ -388,9 +389,6 @@ public class SystemWindows {
public void dispatchDragEvent(DragEvent event) {}
@Override
- public void updatePointerIcon(float x, float y) {}
-
- @Override
public void dispatchWindowShown() {}
@Override
@@ -408,5 +406,10 @@ public class SystemWindows {
// ignore
}
}
+
+ @Override
+ public void dumpWindow(ParcelFileDescriptor pfd) {
+
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
index 53683c67d825..43c92cab6a68 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
@@ -33,7 +33,7 @@ import android.view.Surface;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ShellInit;
import java.lang.annotation.Retention;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java
deleted file mode 100644
index 4009ad21b9b8..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.android.wm.shell.common.annotations;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-import javax.inject.Qualifier;
-
-/**
- * Annotates a method that or qualifies a provider runs aligned to the Choreographer SF vsync
- * instead of the app vsync.
- */
-@Documented
-@Inherited
-@Qualifier
-@Retention(RetentionPolicy.RUNTIME)
-public @interface ChoreographerSfVsync {} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java
deleted file mode 100644
index 7560f71d1f98..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.android.wm.shell.common.annotations;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-import javax.inject.Qualifier;
-
-/** Annotates a method or class that is called from an external thread to the Shell threads. */
-@Documented
-@Inherited
-@Qualifier
-@Retention(RetentionPolicy.RUNTIME)
-public @interface ExternalThread {} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java
deleted file mode 100644
index 0479f8780c79..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.android.wm.shell.common.annotations;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-import javax.inject.Qualifier;
-
-/** Annotates a method or qualifies a provider that runs on the Shell animation-thread */
-@Documented
-@Inherited
-@Qualifier
-@Retention(RetentionPolicy.RUNTIME)
-public @interface ShellAnimationThread {} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java
deleted file mode 100644
index 423f4ce3bfd4..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.android.wm.shell.common.annotations;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-import javax.inject.Qualifier;
-
-/** Annotates a method or qualifies a provider that runs on the Shell main-thread */
-@Documented
-@Inherited
-@Qualifier
-@Retention(RetentionPolicy.RUNTIME)
-public @interface ShellMainThread {} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt
new file mode 100644
index 000000000000..eec24683db8a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt
@@ -0,0 +1,210 @@
+/*
+ * 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.bubbles
+
+import android.graphics.Point
+import android.graphics.RectF
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorListenerAdapter
+import androidx.core.animation.ObjectAnimator
+import com.android.wm.shell.common.bubbles.BaseBubblePinController.LocationChangeListener
+import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT
+import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT
+
+/**
+ * Base class for common logic shared between different bubble views to support pinning bubble bar
+ * to left or right edge of screen.
+ *
+ * Handles drag events and allows a [LocationChangeListener] to be registered that is notified when
+ * location of the bubble bar should change.
+ *
+ * Shows a drop target when releasing a view would update the [BubbleBarLocation].
+ */
+abstract class BaseBubblePinController(private val screenSizeProvider: () -> Point) {
+
+ private var initialLocationOnLeft = false
+ private var onLeft = false
+ private var dismissZone: RectF? = null
+ private var stuckToDismissTarget = false
+ private var screenCenterX = 0
+ private var listener: LocationChangeListener? = null
+ private var dropTargetAnimator: ObjectAnimator? = null
+
+ /**
+ * Signal the controller that dragging interaction has started.
+ *
+ * @param initialLocationOnLeft side of the screen where bubble bar is pinned to
+ */
+ fun onDragStart(initialLocationOnLeft: Boolean) {
+ this.initialLocationOnLeft = initialLocationOnLeft
+ onLeft = initialLocationOnLeft
+ screenCenterX = screenSizeProvider.invoke().x / 2
+ dismissZone = getExclusionRect()
+ }
+
+ /** View has moved to [x] and [y] screen coordinates */
+ fun onDragUpdate(x: Float, y: Float) {
+ if (dismissZone?.contains(x, y) == true) return
+
+ val wasOnLeft = onLeft
+ onLeft = x < screenCenterX
+ if (wasOnLeft != onLeft) {
+ onLocationChange(if (onLeft) LEFT else RIGHT)
+ } else if (stuckToDismissTarget) {
+ // Moved out of the dismiss view back to initial side, if we have a drop target, show it
+ getDropTargetView()?.apply { animateIn() }
+ }
+ // Make sure this gets cleared
+ stuckToDismissTarget = false
+ }
+
+ /** Signal the controller that view has been dragged to dismiss view. */
+ fun onStuckToDismissTarget() {
+ stuckToDismissTarget = true
+ // Notify that location may be reset
+ val shouldResetLocation = onLeft != initialLocationOnLeft
+ if (shouldResetLocation) {
+ onLeft = initialLocationOnLeft
+ listener?.onChange(if (onLeft) LEFT else RIGHT)
+ }
+ getDropTargetView()?.apply {
+ animateOut {
+ if (shouldResetLocation) {
+ updateLocation(if (onLeft) LEFT else RIGHT)
+ }
+ }
+ }
+ }
+
+ /** Signal the controller that dragging interaction has finished. */
+ fun onDragEnd() {
+ getDropTargetView()?.let { view -> view.animateOut { removeDropTargetView(view) } }
+ dismissZone = null
+ listener?.onRelease(if (onLeft) LEFT else RIGHT)
+ }
+
+ /**
+ * [LocationChangeListener] that is notified when dragging interaction has resulted in bubble
+ * bar to be pinned on the other edge
+ */
+ fun setListener(listener: LocationChangeListener?) {
+ this.listener = listener
+ }
+
+ /** Get width for exclusion rect where dismiss takes over drag */
+ protected abstract fun getExclusionRectWidth(): Float
+ /** Get height for exclusion rect where dismiss takes over drag */
+ protected abstract fun getExclusionRectHeight(): Float
+
+ /** Create the drop target view and attach it to the parent */
+ protected abstract fun createDropTargetView(): View
+
+ /** Get the drop target view if it exists */
+ protected abstract fun getDropTargetView(): View?
+
+ /** Remove the drop target view */
+ protected abstract fun removeDropTargetView(view: View)
+
+ /** Update size and location of the drop target view */
+ protected abstract fun updateLocation(location: BubbleBarLocation)
+
+ private fun onLocationChange(location: BubbleBarLocation) {
+ showDropTarget(location)
+ listener?.onChange(location)
+ }
+
+ private fun getExclusionRect(): RectF {
+ val rect = RectF(0f, 0f, getExclusionRectWidth(), getExclusionRectHeight())
+ // Center it around the bottom center of the screen
+ val screenBottom = screenSizeProvider.invoke().y
+ rect.offsetTo(screenCenterX - rect.width() / 2, screenBottom - rect.height())
+ return rect
+ }
+
+ private fun showDropTarget(location: BubbleBarLocation) {
+ val targetView = getDropTargetView() ?: createDropTargetView().apply { alpha = 0f }
+ if (targetView.alpha > 0) {
+ targetView.animateOut {
+ updateLocation(location)
+ targetView.animateIn()
+ }
+ } else {
+ updateLocation(location)
+ targetView.animateIn()
+ }
+ }
+
+ private fun View.animateIn() {
+ dropTargetAnimator?.cancel()
+ dropTargetAnimator =
+ ObjectAnimator.ofFloat(this, View.ALPHA, 1f)
+ .setDuration(DROP_TARGET_ALPHA_IN_DURATION)
+ .addEndAction { dropTargetAnimator = null }
+ dropTargetAnimator?.start()
+ }
+
+ private fun View.animateOut(endAction: Runnable? = null) {
+ dropTargetAnimator?.cancel()
+ dropTargetAnimator =
+ ObjectAnimator.ofFloat(this, View.ALPHA, 0f)
+ .setDuration(DROP_TARGET_ALPHA_OUT_DURATION)
+ .addEndAction {
+ endAction?.run()
+ dropTargetAnimator = null
+ }
+ dropTargetAnimator?.start()
+ }
+
+ private fun <T : Animator> T.addEndAction(runnable: Runnable): T {
+ addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ runnable.run()
+ }
+ }
+ )
+ return this
+ }
+
+ /** Receive updates on location changes */
+ interface LocationChangeListener {
+ /**
+ * Bubble bar has been dragged to a new [BubbleBarLocation]. And the drag is still in
+ * progress.
+ *
+ * Triggered when drag gesture passes the middle of the screen and before touch up. Can be
+ * triggered multiple times per gesture.
+ *
+ * @param location new location as a result of the ongoing drag operation
+ */
+ fun onChange(location: BubbleBarLocation) {}
+
+ /**
+ * Bubble bar has been released in the [BubbleBarLocation].
+ *
+ * @param location final location of the bubble bar once drag is released
+ */
+ fun onRelease(location: BubbleBarLocation)
+ }
+
+ companion object {
+ @VisibleForTesting const val DROP_TARGET_ALPHA_IN_DURATION = 150L
+ @VisibleForTesting const val DROP_TARGET_ALPHA_OUT_DURATION = 100L
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.aidl
new file mode 100644
index 000000000000..3c5beeb48806
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.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.bubbles;
+
+parcelable BubbleBarLocation; \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.kt
new file mode 100644
index 000000000000..f0bdfdef1073
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarLocation.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.bubbles
+
+import android.os.Parcel
+import android.os.Parcelable
+
+/**
+ * The location of the bubble bar.
+ */
+enum class BubbleBarLocation : Parcelable {
+ /**
+ * Place bubble bar at the default location for the chosen system language.
+ * If an RTL language is used, it is on the left. Otherwise on the right.
+ */
+ DEFAULT,
+ /** Default bubble bar location is overridden. Place bubble bar on the left. */
+ LEFT,
+ /** Default bubble bar location is overridden. Place bubble bar on the right. */
+ RIGHT;
+
+ /**
+ * Returns whether bubble bar is pinned to the left edge or right edge.
+ */
+ fun isOnLeft(isRtl: Boolean): Boolean {
+ if (this == DEFAULT) {
+ return isRtl
+ }
+ return this == LEFT
+ }
+
+ override fun describeContents(): Int {
+ return 0
+ }
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ dest.writeString(name)
+ }
+
+ companion object {
+ @JvmField
+ val CREATOR = object : Parcelable.Creator<BubbleBarLocation> {
+ override fun createFromParcel(parcel: Parcel): BubbleBarLocation {
+ return parcel.readString()?.let { valueOf(it) } ?: DEFAULT
+ }
+
+ override fun newArray(size: Int) = arrayOfNulls<BubbleBarLocation>(size)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java
index fc627a8dcb36..ec3c6013e544 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleBarUpdate.java
@@ -18,6 +18,7 @@ package com.android.wm.shell.common.bubbles;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.graphics.Point;
import android.os.Parcel;
import android.os.Parcelable;
@@ -33,6 +34,7 @@ public class BubbleBarUpdate implements Parcelable {
public static final String BUNDLE_KEY = "update";
+ public final boolean initialState;
public boolean expandedChanged;
public boolean expanded;
public boolean shouldShowEducation;
@@ -46,6 +48,12 @@ public class BubbleBarUpdate implements Parcelable {
public String suppressedBubbleKey;
@Nullable
public String unsupressedBubbleKey;
+ @Nullable
+ public BubbleBarLocation bubbleBarLocation;
+ @Nullable
+ public Point expandedViewDropTargetSize;
+ public boolean showOverflowChanged;
+ public boolean showOverflow;
// This is only populated if bubbles have been removed.
public List<RemovedBubble> removedBubbles = new ArrayList<>();
@@ -56,10 +64,17 @@ public class BubbleBarUpdate implements Parcelable {
// This is only populated the first time a listener is connected so it gets the current state.
public List<BubbleInfo> currentBubbleList = new ArrayList<>();
+
public BubbleBarUpdate() {
+ this(false);
+ }
+
+ private BubbleBarUpdate(boolean initialState) {
+ this.initialState = initialState;
}
public BubbleBarUpdate(Parcel parcel) {
+ initialState = parcel.readBoolean();
expandedChanged = parcel.readBoolean();
expanded = parcel.readBoolean();
shouldShowEducation = parcel.readBoolean();
@@ -71,10 +86,16 @@ public class BubbleBarUpdate implements Parcelable {
suppressedBubbleKey = parcel.readString();
unsupressedBubbleKey = parcel.readString();
removedBubbles = parcel.readParcelableList(new ArrayList<>(),
- RemovedBubble.class.getClassLoader());
+ RemovedBubble.class.getClassLoader(), RemovedBubble.class);
parcel.readStringList(bubbleKeysInOrder);
currentBubbleList = parcel.readParcelableList(new ArrayList<>(),
- BubbleInfo.class.getClassLoader());
+ BubbleInfo.class.getClassLoader(), BubbleInfo.class);
+ bubbleBarLocation = parcel.readParcelable(BubbleBarLocation.class.getClassLoader(),
+ BubbleBarLocation.class);
+ expandedViewDropTargetSize = parcel.readParcelable(Point.class.getClassLoader(),
+ Point.class);
+ showOverflowChanged = parcel.readBoolean();
+ showOverflow = parcel.readBoolean();
}
/**
@@ -89,12 +110,17 @@ public class BubbleBarUpdate implements Parcelable {
|| !bubbleKeysInOrder.isEmpty()
|| suppressedBubbleKey != null
|| unsupressedBubbleKey != null
- || !currentBubbleList.isEmpty();
+ || !currentBubbleList.isEmpty()
+ || bubbleBarLocation != null
+ || showOverflowChanged;
}
+ @NonNull
@Override
public String toString() {
- return "BubbleBarUpdate{ expandedChanged=" + expandedChanged
+ return "BubbleBarUpdate{"
+ + " initialState=" + initialState
+ + " expandedChanged=" + expandedChanged
+ " expanded=" + expanded
+ " selectedBubbleKey=" + selectedBubbleKey
+ " shouldShowEducation=" + shouldShowEducation
@@ -105,6 +131,10 @@ public class BubbleBarUpdate implements Parcelable {
+ " removedBubbles=" + removedBubbles
+ " bubbles=" + bubbleKeysInOrder
+ " currentBubbleList=" + currentBubbleList
+ + " bubbleBarLocation=" + bubbleBarLocation
+ + " expandedViewDropTargetSize=" + expandedViewDropTargetSize
+ + " showOverflowChanged=" + showOverflowChanged
+ + " showOverflow=" + showOverflow
+ " }";
}
@@ -115,6 +145,7 @@ public class BubbleBarUpdate implements Parcelable {
@Override
public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeBoolean(initialState);
parcel.writeBoolean(expandedChanged);
parcel.writeBoolean(expanded);
parcel.writeBoolean(shouldShowEducation);
@@ -126,14 +157,28 @@ public class BubbleBarUpdate implements Parcelable {
parcel.writeParcelableList(removedBubbles, flags);
parcel.writeStringList(bubbleKeysInOrder);
parcel.writeParcelableList(currentBubbleList, flags);
+ parcel.writeParcelable(bubbleBarLocation, flags);
+ parcel.writeParcelable(expandedViewDropTargetSize, flags);
+ parcel.writeBoolean(showOverflowChanged);
+ parcel.writeBoolean(showOverflow);
+ }
+
+ /**
+ * Create update for initial set of values.
+ * <p>
+ * Used when bubble bar is newly created.
+ */
+ public static BubbleBarUpdate createInitialState() {
+ return new BubbleBarUpdate(true);
}
@NonNull
public static final Creator<BubbleBarUpdate> CREATOR =
- new Creator<BubbleBarUpdate>() {
+ new Creator<>() {
public BubbleBarUpdate createFromParcel(Parcel source) {
return new BubbleBarUpdate(source);
}
+
public BubbleBarUpdate[] newArray(int size) {
return new BubbleBarUpdate[size];
}
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/bubbles/DismissView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt
index 9094739d0d88..e06de9e9353c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt
@@ -35,7 +35,7 @@ import androidx.core.content.ContextCompat
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY
import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW
-import com.android.wm.shell.animation.PhysicsAnimator
+import com.android.wm.shell.shared.animation.PhysicsAnimator
/**
* View that handles interactions between DismissCircleView and BubbleStackView.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS
new file mode 100644
index 000000000000..08c70314973e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/OWNERS
@@ -0,0 +1,6 @@
+# WM shell sub-module bubble owner
+madym@google.com
+atsjenk@google.com
+liranb@google.com
+sukeshram@google.com
+mpodolian@google.com
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/magnetictarget/MagnetizedObject.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
index 11e477716eb0..123d4dc49199 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
@@ -28,7 +28,7 @@ import android.view.ViewConfiguration
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringForce
-import com.android.wm.shell.animation.PhysicsAnimator
+import com.android.wm.shell.shared.animation.PhysicsAnimator
import kotlin.math.abs
import kotlin.math.hypot
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/PhoneSizeSpecSource.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt
index 18c7bdd6d5ba..7eb0f267b312 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PhoneSizeSpecSource.kt
@@ -18,7 +18,6 @@ package com.android.wm.shell.common.pip
import android.content.Context
import android.content.res.Resources
-import android.os.SystemProperties
import android.util.Size
import com.android.wm.shell.R
import java.io.PrintWriter
@@ -36,30 +35,81 @@ class PhoneSizeSpecSource(
private var mOverrideMinSize: Size? = null
- /** Default and minimum percentages for the PIP size logic. */
- private val mDefaultSizePercent: Float
- private val mMinimumSizePercent: Float
+ /**
+ * Default percentages for the PIP size logic.
+ * 1. Determine max widths
+ * Subtract width of system UI and default padding from the shortest edge of the device.
+ * This is the max width.
+ * 2. Calculate Default and Mins
+ * Default is mSystemPreferredDefaultSizePercent of max-width/height.
+ * Min is mSystemPreferredMinimumSizePercent of it.
+ *
+ * NOTE: Do not use this directly, use the mPreferredDefaultSizePercent getter instead.
+ */
+ private var mSystemPreferredDefaultSizePercent = 0.6f
+ /** Minimum percentages for the PIP size logic. */
+ private var mSystemPreferredMinimumSizePercent = 0.5f
+
+ /** Threshold to categorize the Display as square, calculated as min(w, h) / max(w, h). */
+ private var mSquareDisplayThresholdForSystemPreferredSize = 0.95f
+ /**
+ * Default percentages for the PIP size logic when the Display is square-ish.
+ * This is used instead when the display is square-ish, like fold-ables when unfolded,
+ * to make sure that default PiP does not cover the hinge (halfway of the display).
+ * 1. Determine max widths
+ * Subtract width of system UI and default padding from the shortest edge of the device.
+ * This is the max width.
+ * 2. Calculate Default and Mins
+ * Default is mSystemPreferredDefaultSizePercent of max-width/height.
+ * Min is mSystemPreferredMinimumSizePercent of it.
+ *
+ * NOTE: Do not use this directly, use the mPreferredDefaultSizePercent getter instead.
+ */
+ private var mSystemPreferredDefaultSizePercentForSquareDisplay = 0.5f
+ /** Minimum percentages for the PIP size logic. */
+ private var mSystemPreferredMinimumSizePercentForSquareDisplay = 0.4f
+
+ private val mIsSquareDisplay
+ get() = minOf(pipDisplayLayoutState.displayLayout.width(),
+ pipDisplayLayoutState.displayLayout.height()).toFloat() /
+ maxOf(pipDisplayLayoutState.displayLayout.width(),
+ pipDisplayLayoutState.displayLayout.height()) >
+ mSquareDisplayThresholdForSystemPreferredSize
+ private val mPreferredDefaultSizePercent
+ get() = if (mIsSquareDisplay) mSystemPreferredDefaultSizePercentForSquareDisplay else
+ mSystemPreferredDefaultSizePercent
+
+ private val mPreferredMinimumSizePercent
+ get() = if (mIsSquareDisplay) mSystemPreferredMinimumSizePercentForSquareDisplay else
+ mSystemPreferredMinimumSizePercent
/** Aspect ratio that the PIP size spec logic optimizes for. */
private var mOptimizedAspectRatio = 0f
init {
- mDefaultSizePercent = SystemProperties
- .get("com.android.wm.shell.pip.phone.def_percentage", "0.6").toFloat()
- mMinimumSizePercent = SystemProperties
- .get("com.android.wm.shell.pip.phone.min_percentage", "0.5").toFloat()
-
reloadResources()
}
private fun reloadResources() {
- val res: Resources = context.getResources()
+ val res: Resources = context.resources
mDefaultMinSize = res.getDimensionPixelSize(
R.dimen.default_minimal_size_pip_resizable_task)
mOverridableMinSize = res.getDimensionPixelSize(
R.dimen.overridable_minimal_size_pip_resizable_task)
+ mSystemPreferredDefaultSizePercent = res.getFloat(
+ R.dimen.config_pipSystemPreferredDefaultSizePercent)
+ mSystemPreferredMinimumSizePercent = res.getFloat(
+ R.dimen.config_pipSystemPreferredMinimumSizePercent)
+
+ mSquareDisplayThresholdForSystemPreferredSize = res.getFloat(
+ R.dimen.config_pipSquareDisplayThresholdForSystemPreferredSize)
+ mSystemPreferredDefaultSizePercentForSquareDisplay = res.getFloat(
+ R.dimen.config_pipSystemPreferredDefaultSizePercentForSquareDisplay)
+ mSystemPreferredMinimumSizePercentForSquareDisplay = res.getFloat(
+ R.dimen.config_pipSystemPreferredMinimumSizePercentForSquareDisplay)
+
val requestedOptAspRatio = res.getFloat(R.dimen.config_pipLargeScreenOptimizedAspectRatio)
// make sure the optimized aspect ratio is valid with a default value to fall back to
mOptimizedAspectRatio = if (requestedOptAspRatio > 1) {
@@ -128,7 +178,7 @@ class PhoneSizeSpecSource(
return minSize
}
val maxSize = getMaxSize(aspectRatio)
- val defaultWidth = Math.max(Math.round(maxSize.width * mDefaultSizePercent),
+ val defaultWidth = Math.max(Math.round(maxSize.width * mPreferredDefaultSizePercent),
minSize.width)
val defaultHeight = Math.round(defaultWidth / aspectRatio)
return Size(defaultWidth, defaultHeight)
@@ -146,8 +196,8 @@ class PhoneSizeSpecSource(
return adjustOverrideMinSizeToAspectRatio(aspectRatio)!!
}
val maxSize = getMaxSize(aspectRatio)
- var minWidth = Math.round(maxSize.width * mMinimumSizePercent)
- var minHeight = Math.round(maxSize.height * mMinimumSizePercent)
+ var minWidth = Math.round(maxSize.width * mPreferredMinimumSizePercent)
+ var minHeight = Math.round(maxSize.height * mPreferredMinimumSizePercent)
// make sure the calculated min size is not smaller than the allowed default min size
if (aspectRatio > 1f) {
@@ -244,8 +294,8 @@ class PhoneSizeSpecSource(
pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize)
pw.println(innerPrefix + "mOverridableMinSize=" + mOverridableMinSize)
pw.println(innerPrefix + "mDefaultMinSize=" + mDefaultMinSize)
- pw.println(innerPrefix + "mDefaultSizePercent=" + mDefaultSizePercent)
- pw.println(innerPrefix + "mMinimumSizePercent=" + mMinimumSizePercent)
+ pw.println(innerPrefix + "mDefaultSizePercent=" + mPreferredDefaultSizePercent)
+ pw.println(innerPrefix + "mMinimumSizePercent=" + mPreferredMinimumSizePercent)
pw.println(innerPrefix + "mOptimizedAspectRatio=" + mOptimizedAspectRatio)
}
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
index b87c2f6ebad5..7ceaaea3962f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
@@ -125,6 +125,7 @@ public class PipBoundsState {
private @Nullable Runnable mOnMinimalSizeChangeCallback;
private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback;
private List<Consumer<Rect>> mOnPipExclusionBoundsChangeCallbacks = new ArrayList<>();
+ private List<Consumer<Float>> mOnAspectRatioChangedCallbacks = new ArrayList<>();
// the size of the current bounds relative to the max size spec
private float mBoundsScale;
@@ -297,7 +298,12 @@ public class PipBoundsState {
/** Set the PIP aspect ratio. */
public void setAspectRatio(float aspectRatio) {
- mAspectRatio = aspectRatio;
+ if (Float.compare(mAspectRatio, aspectRatio) != 0) {
+ mAspectRatio = aspectRatio;
+ for (Consumer<Float> callback : mOnAspectRatioChangedCallbacks) {
+ callback.accept(mAspectRatio);
+ }
+ }
}
/** Get the PIP aspect ratio. */
@@ -527,6 +533,23 @@ public class PipBoundsState {
mOnPipExclusionBoundsChangeCallbacks.remove(onPipExclusionBoundsChangeCallback);
}
+ /** Adds callback to listen on aspect ratio change. */
+ public void addOnAspectRatioChangedCallback(
+ @NonNull Consumer<Float> onAspectRatioChangedCallback) {
+ if (!mOnAspectRatioChangedCallbacks.contains(onAspectRatioChangedCallback)) {
+ mOnAspectRatioChangedCallbacks.add(onAspectRatioChangedCallback);
+ onAspectRatioChangedCallback.accept(mAspectRatio);
+ }
+ }
+
+ /** Removes callback to listen on aspect ratio change. */
+ public void removeOnAspectRatioChangedCallback(
+ @NonNull Consumer<Float> onAspectRatioChangedCallback) {
+ if (mOnAspectRatioChangedCallbacks.contains(onAspectRatioChangedCallback)) {
+ mOnAspectRatioChangedCallbacks.remove(onAspectRatioChangedCallback);
+ }
+ }
+
public LauncherState getLauncherState() {
return mLauncherState;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDisplayLayoutState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDisplayLayoutState.java
index ed42117a55af..d5e47187dac2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDisplayLayoutState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDisplayLayoutState.java
@@ -115,6 +115,12 @@ public class PipDisplayLayoutState {
mDisplayLayout.rotateTo(mContext.getResources(), targetRotation);
}
+ /** Returns the current display rotation of this layout state. */
+ @Surface.Rotation
+ public int getRotation() {
+ return mDisplayLayout.rotation();
+ }
+
/** Get the current display id */
public int getDisplayId() {
return mDisplayId;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java
index 317e48e19c13..c421dec025f2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java
@@ -28,7 +28,7 @@ import androidx.annotation.Nullable;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
import java.io.PrintWriter;
import java.util.Map;
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 1e30d8feb132..a09720dd6a70 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
@@ -16,11 +16,15 @@
package com.android.wm.shell.common.pip
import android.app.ActivityTaskManager
+import android.app.AppGlobals
import android.app.RemoteAction
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
import android.util.Log
import android.util.Pair
@@ -135,7 +139,48 @@ 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 = 0
+ var top = 0
+ if (appBoundsAspRatio < aspectRatio) {
+ width = appBounds.width()
+ height = Math.round(width / aspectRatio)
+ top = (appBounds.height() - height) / 2
+ } else {
+ height = appBounds.height()
+ width = Math.round(height * aspectRatio)
+ left = (appBounds.width() - width) / 2
+ }
+ return Rect(left, top, left + width, top + height)
+ }
+
+ private var isPip2ExperimentEnabled: Boolean? = null
+
+ /**
+ * Returns true if PiP2 implementation should be used. Besides the trunk stable flag,
+ * system property can be used to override this read only flag during development.
+ * It's currently limited to phone form factor, i.e., not enabled on ARC / TV.
+ */
@JvmStatic
- val isPip2ExperimentEnabled: Boolean
- get() = Flags.enablePip2Implementation()
+ fun isPip2ExperimentEnabled(): Boolean {
+ if (isPip2ExperimentEnabled == null) {
+ val isArc = AppGlobals.getPackageManager().hasSystemFeature(
+ "org.chromium.arc", 0)
+ val isTv = AppGlobals.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_LEANBACK, 0)
+ isPip2ExperimentEnabled = SystemProperties.getBoolean(
+ "persist.wm_shell.pip2", false) ||
+ (Flags.enablePip2Implementation() && !isArc && !isTv)
+ }
+ return isPip2ExperimentEnabled as Boolean
+ }
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
index f9a286ec804f..bc6ed1f63c8a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java
@@ -84,6 +84,8 @@ public class DividerSnapAlgorithm {
private final int mMinimalSizeResizableTask;
private final int mTaskHeightInMinimizedMode;
private final float mFixedRatio;
+ /** Allows split ratios to calculated dynamically instead of using {@link #mFixedRatio}. */
+ private final boolean mAllowFlexibleSplitRatios;
private boolean mIsHorizontalDivision;
/** The first target which is still splitting the screen */
@@ -144,6 +146,8 @@ public class DividerSnapAlgorithm {
com.android.internal.R.fraction.docked_stack_divider_fixed_ratio, 1, 1);
mMinimalSizeResizableTask = res.getDimensionPixelSize(
com.android.internal.R.dimen.default_minimal_size_resizable_task);
+ mAllowFlexibleSplitRatios = res.getBoolean(
+ com.android.internal.R.bool.config_flexibleSplitRatios);
mTaskHeightInMinimizedMode = isHomeResizable ? res.getDimensionPixelSize(
com.android.internal.R.dimen.task_height_of_minimized_mode) : 0;
calculateTargets(isHorizontalDivision, dockSide);
@@ -349,6 +353,9 @@ public class DividerSnapAlgorithm {
? mDisplayHeight - mInsets.bottom
: mDisplayWidth - mInsets.right;
int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2;
+ if (mAllowFlexibleSplitRatios) {
+ size = Math.max(size, mMinimalSizeResizableTask);
+ }
int topPosition = start + size;
int bottomPosition = end - size - mDividerSize;
addNonDismissingTargets(isHorizontalDivision, topPosition, bottomPosition, dividerMax);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
index a87116ea4670..c2242a8b87fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
@@ -185,7 +185,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
nextTarget = snapAlgorithm.getDismissStartTarget();
}
if (nextTarget != null) {
- mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), nextTarget);
+ mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), nextTarget);
return true;
}
return super.performAccessibilityAction(host, action, args);
@@ -336,6 +336,11 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
setTouching();
mStartPos = touchPos;
mMoving = false;
+ // This triggers initialization of things like the resize veil in preparation for
+ // showing it when the user moves the divider past the slop, and has to be done
+ // before onStartDragging() which starts the jank interaction tracing
+ mSplitLayout.updateDividerBounds(mSplitLayout.getDividerPosition(),
+ false /* shouldUseParallaxEffect */);
mSplitLayout.onStartDragging();
break;
case MotionEvent.ACTION_MOVE:
@@ -345,9 +350,9 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
mMoving = true;
}
if (mMoving) {
- final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos;
+ final int position = mSplitLayout.getDividerPosition() + touchPos - mStartPos;
mLastDraggingPosition = position;
- mSplitLayout.updateDivideBounds(position);
+ mSplitLayout.updateDividerBounds(position, true /* shouldUseParallaxEffect */);
}
break;
case MotionEvent.ACTION_UP:
@@ -363,7 +368,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
final float velocity = isLeftRightSplit
? mVelocityTracker.getXVelocity()
: mVelocityTracker.getYVelocity();
- final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos;
+ final int position = mSplitLayout.getDividerPosition() + touchPos - mStartPos;
final DividerSnapAlgorithm.SnapTarget snapTarget =
mSplitLayout.findSnapTarget(position, velocity, false /* hardDismiss */);
mSplitLayout.snapToTarget(position, snapTarget);
@@ -472,12 +477,12 @@ public class DividerView extends FrameLayout implements View.OnTouchListener {
mInteractive = interactive;
mHideHandle = hideHandle;
if (!mInteractive && mHideHandle && mMoving) {
- final int position = mSplitLayout.getDividePosition();
- mSplitLayout.flingDividePosition(
+ final int position = mSplitLayout.getDividerPosition();
+ mSplitLayout.flingDividerPosition(
mLastDraggingPosition,
position,
mSplitLayout.FLING_RESIZE_DURATION,
- () -> mSplitLayout.setDividePosition(position, true /* applyLayoutChange */));
+ () -> mSplitLayout.setDividerPosition(position, true /* applyLayoutChange */));
mMoving = false;
}
releaseTouching();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
index dae62ac74483..de016d3ae400 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java
@@ -18,6 +18,7 @@ package com.android.wm.shell.common.split;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
@@ -30,7 +31,6 @@ import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Configuration;
-import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@@ -56,7 +56,13 @@ import com.android.wm.shell.common.SurfaceUtils;
import java.util.function.Consumer;
/**
- * Handles split decor like showing resizing hint for a specific split.
+ * Handles additional layers over a running task in a split pair, for example showing a veil with an
+ * app icon when the task is being resized (usually to hide weird layouts while the app is being
+ * stretched). One SplitDecorManager is initialized on each window.
+ * <br>
+ * Currently, we show a veil when:
+ * a) Task is resizing down from a fullscreen window.
+ * b) Task is being stretched past its original bounds.
*/
public class SplitDecorManager extends WindowlessWindowManager {
private static final String TAG = SplitDecorManager.class.getSimpleName();
@@ -77,7 +83,11 @@ public class SplitDecorManager extends WindowlessWindowManager {
private boolean mShown;
private boolean mIsResizing;
- private final Rect mOldBounds = new Rect();
+ /** The original bounds of the main task, captured at the beginning of a resize transition. */
+ private final Rect mOldMainBounds = new Rect();
+ /** The original bounds of the side task, captured at the beginning of a resize transition. */
+ private final Rect mOldSideBounds = new Rect();
+ /** The current bounds of the main task, mid-resize. */
private final Rect mResizingBounds = new Rect();
private final Rect mTempRect = new Rect();
private ValueAnimator mFadeAnimator;
@@ -109,7 +119,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
}
/** Inflates split decor surface on the root surface. */
- public void inflate(Context context, SurfaceControl rootLeash, Rect rootBounds) {
+ public void inflate(Context context, SurfaceControl rootLeash) {
if (mIconLeash != null && mViewHost != null) {
return;
}
@@ -128,13 +138,12 @@ public class SplitDecorManager extends WindowlessWindowManager {
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
0 /* width */, 0 /* height */, TYPE_APPLICATION_OVERLAY,
FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT);
- lp.width = rootBounds.width();
- lp.height = rootBounds.height();
+ lp.width = mIconSize;
+ lp.height = mIconSize;
lp.token = new Binder();
lp.setTitle(TAG);
lp.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
- // TODO(b/189839391): Set INPUT_FEATURE_NO_INPUT_CHANNEL after WM supports
- // TRUSTED_OVERLAY for windowless window without input channel.
+ lp.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL;
mViewHost.setView(rootLayout, lp);
}
@@ -184,29 +193,38 @@ public class SplitDecorManager extends WindowlessWindowManager {
mResizingIconView = null;
mIsResizing = false;
mShown = false;
- mOldBounds.setEmpty();
+ mOldMainBounds.setEmpty();
+ mOldSideBounds.setEmpty();
mResizingBounds.setEmpty();
}
/** Showing resizing hint. */
public void onResizing(ActivityManager.RunningTaskInfo resizingTask, Rect newBounds,
Rect sideBounds, SurfaceControl.Transaction t, int offsetX, int offsetY,
- boolean immediately) {
+ boolean immediately, float[] veilColor) {
if (mResizingIconView == null) {
return;
}
if (!mIsResizing) {
mIsResizing = true;
- mOldBounds.set(newBounds);
+ mOldMainBounds.set(newBounds);
+ mOldSideBounds.set(sideBounds);
}
mResizingBounds.set(newBounds);
mOffsetX = offsetX;
mOffsetY = offsetY;
- final boolean show =
- newBounds.width() > mOldBounds.width() || newBounds.height() > mOldBounds.height();
- final boolean update = show != mShown;
+ // Show a veil when:
+ // a) Task is resizing down from a fullscreen window.
+ // b) Task is being stretched past its original bounds.
+ final boolean isResizingDownFromFullscreen =
+ mOldSideBounds.width() <= 1 || mOldSideBounds.height() <= 1;
+ final boolean isStretchingPastOriginalBounds =
+ newBounds.width() > mOldMainBounds.width()
+ || newBounds.height() > mOldMainBounds.height();
+ final boolean showVeil = isResizingDownFromFullscreen || isStretchingPastOriginalBounds;
+ final boolean update = showVeil != mShown;
if (update && mFadeAnimator != null && mFadeAnimator.isRunning()) {
// If we need to animate and animator still running, cancel it before we ensure both
// background and icon surfaces are non null for next animation.
@@ -216,18 +234,18 @@ public class SplitDecorManager extends WindowlessWindowManager {
if (mBackgroundLeash == null) {
mBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
RESIZING_BACKGROUND_SURFACE_NAME, mSurfaceSession);
- t.setColor(mBackgroundLeash, getResizingBackgroundColor(resizingTask))
+ t.setColor(mBackgroundLeash, veilColor)
.setLayer(mBackgroundLeash, Integer.MAX_VALUE - 1);
}
if (mGapBackgroundLeash == null && !immediately) {
final boolean isLandscape = newBounds.height() == sideBounds.height();
- final int left = isLandscape ? mOldBounds.width() : 0;
- final int top = isLandscape ? 0 : mOldBounds.height();
+ final int left = isLandscape ? mOldMainBounds.width() : 0;
+ final int top = isLandscape ? 0 : mOldMainBounds.height();
mGapBackgroundLeash = SurfaceUtils.makeColorLayer(mHostLeash,
GAP_BACKGROUND_SURFACE_NAME, mSurfaceSession);
// Fill up another side bounds area.
- t.setColor(mGapBackgroundLeash, getResizingBackgroundColor(resizingTask))
+ t.setColor(mGapBackgroundLeash, veilColor)
.setLayer(mGapBackgroundLeash, Integer.MAX_VALUE - 2)
.setPosition(mGapBackgroundLeash, left, top)
.setWindowCrop(mGapBackgroundLeash, sideBounds.width(), sideBounds.height());
@@ -251,12 +269,12 @@ public class SplitDecorManager extends WindowlessWindowManager {
if (update) {
if (immediately) {
- t.setVisibility(mBackgroundLeash, show);
- t.setVisibility(mIconLeash, show);
+ t.setVisibility(mBackgroundLeash, showVeil);
+ t.setVisibility(mIconLeash, showVeil);
} else {
- startFadeAnimation(show, false, null);
+ startFadeAnimation(showVeil, false, null);
}
- mShown = show;
+ mShown = showVeil;
}
}
@@ -309,7 +327,8 @@ public class SplitDecorManager extends WindowlessWindowManager {
mIsResizing = false;
mOffsetX = 0;
mOffsetY = 0;
- mOldBounds.setEmpty();
+ mOldMainBounds.setEmpty();
+ mOldSideBounds.setEmpty();
mResizingBounds.setEmpty();
if (mFadeAnimator != null && mFadeAnimator.isRunning()) {
if (!mShown) {
@@ -346,14 +365,14 @@ public class SplitDecorManager extends WindowlessWindowManager {
/** Screenshot host leash and attach on it if meet some conditions */
public void screenshotIfNeeded(SurfaceControl.Transaction t) {
- if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) {
+ if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) {
if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
mScreenshotAnimator.cancel();
} else if (mScreenshot != null) {
t.remove(mScreenshot);
}
- mTempRect.set(mOldBounds);
+ mTempRect.set(mOldMainBounds);
mTempRect.offsetTo(0, 0);
mScreenshot = ScreenshotUtils.takeScreenshot(t, mHostLeash, mTempRect,
Integer.MAX_VALUE - 1);
@@ -364,7 +383,7 @@ public class SplitDecorManager extends WindowlessWindowManager {
public void setScreenshotIfNeeded(SurfaceControl screenshot, SurfaceControl.Transaction t) {
if (screenshot == null || !screenshot.isValid()) return;
- if (!mShown && mIsResizing && !mOldBounds.equals(mResizingBounds)) {
+ if (!mShown && mIsResizing && !mOldMainBounds.equals(mResizingBounds)) {
if (mScreenshotAnimator != null && mScreenshotAnimator.isRunning()) {
mScreenshotAnimator.cancel();
} else if (mScreenshot != null) {
@@ -465,9 +484,4 @@ public class SplitDecorManager extends WindowlessWindowManager {
mIcon = null;
}
}
-
- private static float[] getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) {
- final int taskBgColor = taskInfo.taskDescription.getBackgroundColor();
- return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).getComponents();
- }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
index 6b2d544c192a..8ced76fd23af 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java
@@ -78,7 +78,7 @@ import java.util.function.Consumer;
/**
* Records and handles layout of splits. Helps to calculate proper bounds when configuration or
- * divide position changes.
+ * divider position changes.
*/
public final class SplitLayout implements DisplayInsetsController.OnInsetsChangedListener {
private static final String TAG = "SplitLayout";
@@ -278,7 +278,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
return mSplitWindowManager == null ? null : mSplitWindowManager.getSurfaceControl();
}
- int getDividePosition() {
+ int getDividerPosition() {
return mDividerPosition;
}
@@ -489,20 +489,20 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public void setDividerAtBorder(boolean start) {
final int pos = start ? mDividerSnapAlgorithm.getDismissStartTarget().position
: mDividerSnapAlgorithm.getDismissEndTarget().position;
- setDividePosition(pos, false /* applyLayoutChange */);
+ setDividerPosition(pos, false /* applyLayoutChange */);
}
/**
* Updates bounds with the passing position. Usually used to update recording bounds while
* performing animation or dragging divider bar to resize the splits.
*/
- void updateDivideBounds(int position) {
+ void updateDividerBounds(int position, boolean shouldUseParallaxEffect) {
updateBounds(position);
mSplitLayoutHandler.onLayoutSizeChanging(this, mSurfaceEffectPolicy.mParallaxOffset.x,
- mSurfaceEffectPolicy.mParallaxOffset.y);
+ mSurfaceEffectPolicy.mParallaxOffset.y, shouldUseParallaxEffect);
}
- void setDividePosition(int position, boolean applyLayoutChange) {
+ void setDividerPosition(int position, boolean applyLayoutChange) {
mDividerPosition = position;
updateBounds(mDividerPosition);
if (applyLayoutChange) {
@@ -511,14 +511,14 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
}
/**
- * Updates divide position and split bounds base on the ratio within root bounds. Falls back
+ * Updates divider position and split bounds base on the ratio within root bounds. Falls back
* to middle position if the provided SnapTarget is not supported.
*/
public void setDivideRatio(@PersistentSnapPosition int snapPosition) {
final DividerSnapAlgorithm.SnapTarget snapTarget = mDividerSnapAlgorithm.findSnapTarget(
snapPosition);
- setDividePosition(snapTarget != null
+ setDividerPosition(snapTarget != null
? snapTarget.position
: mDividerSnapAlgorithm.getMiddleTarget().position,
false /* applyLayoutChange */);
@@ -546,24 +546,24 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
}
/**
- * Sets new divide position and updates bounds correspondingly. Notifies listener if the new
+ * Sets new divider position and updates bounds correspondingly. Notifies listener if the new
* target indicates dismissing split.
*/
public void snapToTarget(int currentPosition, DividerSnapAlgorithm.SnapTarget snapTarget) {
switch (snapTarget.snapPosition) {
case SNAP_TO_START_AND_DISMISS:
- flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
+ flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
() -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */,
EXIT_REASON_DRAG_DIVIDER));
break;
case SNAP_TO_END_AND_DISMISS:
- flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
+ flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
() -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */,
EXIT_REASON_DRAG_DIVIDER));
break;
default:
- flingDividePosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
- () -> setDividePosition(snapTarget.position, true /* applyLayoutChange */));
+ flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION,
+ () -> setDividerPosition(snapTarget.position, true /* applyLayoutChange */));
break;
}
}
@@ -615,19 +615,24 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
public void flingDividerToDismiss(boolean toEnd, int reason) {
final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position
: mDividerSnapAlgorithm.getDismissStartTarget().position;
- flingDividePosition(getDividePosition(), target, FLING_EXIT_DURATION,
+ flingDividerPosition(getDividerPosition(), target, FLING_EXIT_DURATION,
() -> mSplitLayoutHandler.onSnappedToDismiss(toEnd, reason));
}
/** Fling divider from current position to center position. */
- public void flingDividerToCenter() {
+ public void flingDividerToCenter(@Nullable Runnable finishCallback) {
final int pos = mDividerSnapAlgorithm.getMiddleTarget().position;
- flingDividePosition(getDividePosition(), pos, FLING_ENTER_DURATION,
- () -> setDividePosition(pos, true /* applyLayoutChange */));
+ flingDividerPosition(getDividerPosition(), pos, FLING_ENTER_DURATION,
+ () -> {
+ setDividerPosition(pos, true /* applyLayoutChange */);
+ if (finishCallback != null) {
+ finishCallback.run();
+ }
+ });
}
@VisibleForTesting
- void flingDividePosition(int from, int to, int duration,
+ void flingDividerPosition(int from, int to, int duration,
@Nullable Runnable flingFinishedCallback) {
if (from == to) {
if (flingFinishedCallback != null) {
@@ -647,7 +652,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
.setDuration(duration);
mDividerFlingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
mDividerFlingAnimator.addUpdateListener(
- animation -> updateDivideBounds((int) animation.getAnimatedValue()));
+ animation -> updateDividerBounds(
+ (int) animation.getAnimatedValue(), false /* shouldUseParallaxEffect */)
+ );
mDividerFlingAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
@@ -897,7 +904,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
* @see #applySurfaceChanges(SurfaceControl.Transaction, SurfaceControl, SurfaceControl,
* SurfaceControl, SurfaceControl, boolean)
*/
- void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY);
+ void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY,
+ boolean shouldUseParallaxEffect);
/**
* Calls when finish resizing the split bounds.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
index f9259e79472e..e8226051b672 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java
@@ -16,8 +16,6 @@
package com.android.wm.shell.common.split;
-import static android.content.pm.LauncherApps.ShortcutQuery.FLAG_MATCH_ALL_KINDS_WITH_ALL_PINNED;
-
import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -26,25 +24,18 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT
import android.app.ActivityManager;
import android.app.PendingIntent;
-import android.content.ComponentName;
import android.content.Intent;
-import android.content.pm.LauncherApps;
-import android.content.pm.ShortcutInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
+import android.graphics.Color;
import android.graphics.Rect;
-import android.os.UserHandle;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.util.ArrayUtils;
import com.android.wm.shell.Flags;
import com.android.wm.shell.ShellTaskOrganizer;
-import java.util.Arrays;
-import java.util.List;
-
/** Helper utility class for split screen components to use. */
public class SplitScreenUtils {
/** Reverse the split position. */
@@ -137,4 +128,10 @@ public class SplitScreenUtils {
return isLandscape;
}
}
+
+ /** Returns the specified background color that matches a RunningTaskInfo. */
+ public static Color getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) {
+ final int taskBgColor = taskInfo.taskDescription.getBackgroundColor();
+ return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor);
+ }
}
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/AppCompatUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt
new file mode 100644
index 000000000000..6781d08c9904
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/AppCompatUtils.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+
+@file:JvmName("AppCompatUtils")
+
+package com.android.wm.shell.compatui
+
+import android.app.TaskInfo
+fun isSingleTopActivityTranslucent(task: TaskInfo) =
+ task.isTopActivityTransparent && task.numActivities == 1
+
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
index fa2e23647a39..713d04bce4e8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
@@ -24,8 +24,8 @@ import android.provider.DeviceConfig;
import com.android.wm.shell.R;
import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.dagger.WMSingleton;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
import javax.inject.Inject;
@@ -194,6 +194,10 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi
return mHideSizeCompatRestartButtonTolerance;
}
+ int getDefaultHideRestartButtonTolerance() {
+ return MAX_PERCENTAGE_VAL;
+ }
+
boolean getHasSeenLetterboxEducation(int userId) {
return mLetterboxEduSharedPreferences
.getBoolean(dontShowLetterboxEduKey(userId), /* default= */ false);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index 86571cf9c622..bfac24b81d2f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -20,7 +20,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.AppCompatTaskInfo.CameraCompatControlState;
+import android.app.CameraCompatTaskInfo.CameraCompatControlState;
import android.app.TaskInfo;
import android.content.ComponentName;
import android.content.Context;
@@ -188,6 +188,11 @@ public class CompatUIController implements OnDisplaysChangedListener,
*/
private boolean mHasShownUserAspectRatioSettingsButton = false;
+ /**
+ * This is true when the rechability education is displayed for the first time.
+ */
+ private boolean mIsFirstReachabilityEducationRunning;
+
public CompatUIController(@NonNull Context context,
@NonNull ShellInit shellInit,
@NonNull ShellController shellController,
@@ -252,9 +257,35 @@ public class CompatUIController implements OnDisplaysChangedListener,
removeLayouts(taskInfo.taskId);
return;
}
-
+ // We're showing the first reachability education so we ignore incoming TaskInfo
+ // until the education flow has completed or we double tap.
+ if (mIsFirstReachabilityEducationRunning) {
+ return;
+ }
+ if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) {
+ if (taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled) {
+ createOrUpdateLetterboxEduLayout(taskInfo, taskListener);
+ } else if (!taskInfo.appCompatTaskInfo.isFromLetterboxDoubleTap) {
+ // In this case the app is letterboxed and the letterbox education
+ // is disabled. In this case we need to understand if it's the first
+ // time we show the reachability education. When this is happening
+ // we need to ignore all the incoming TaskInfo until the education
+ // completes. If we come from a double tap we follow the normal flow.
+ final boolean topActivityPillarboxed =
+ taskInfo.appCompatTaskInfo.isTopActivityPillarboxed();
+ final boolean isFirstTimeHorizontalReachabilityEdu = topActivityPillarboxed
+ && !mCompatUIConfiguration.hasSeenHorizontalReachabilityEducation(taskInfo);
+ final boolean isFirstTimeVerticalReachabilityEdu = !topActivityPillarboxed
+ && !mCompatUIConfiguration.hasSeenVerticalReachabilityEducation(taskInfo);
+ if (isFirstTimeHorizontalReachabilityEdu || isFirstTimeVerticalReachabilityEdu) {
+ mIsFirstReachabilityEducationRunning = true;
+ mCompatUIConfiguration.setSeenLetterboxEducation(taskInfo.userId);
+ createOrUpdateReachabilityEduLayout(taskInfo, taskListener);
+ return;
+ }
+ }
+ }
createOrUpdateCompatLayout(taskInfo, taskListener);
- createOrUpdateLetterboxEduLayout(taskInfo, taskListener);
createOrUpdateRestartDialogLayout(taskInfo, taskListener);
if (mCompatUIConfiguration.getHasSeenLetterboxEducation(taskInfo.userId)) {
createOrUpdateReachabilityEduLayout(taskInfo, taskListener);
@@ -589,6 +620,7 @@ public class CompatUIController implements OnDisplaysChangedListener,
private void onInitialReachabilityEduDismissed(@NonNull TaskInfo taskInfo,
@NonNull ShellTaskOrganizer.TaskListener taskListener) {
// We need to update the UI otherwise it will not be shown until the user relaunches the app
+ mIsFirstReachabilityEducationRunning = false;
createOrUpdateUserAspectRatioSettingsLayout(taskInfo, taskListener);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
index a0986fa601f2..2b0bd3272ed2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java
@@ -16,10 +16,10 @@
package com.android.wm.shell.compatui;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
import android.annotation.IdRes;
-import android.app.AppCompatTaskInfo.CameraCompatControlState;
+import android.app.CameraCompatTaskInfo.CameraCompatControlState;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index dbf7186def8a..3ab1fad2b203 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -16,16 +16,16 @@
package com.android.wm.shell.compatui;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
+import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.AppCompatTaskInfo;
-import android.app.AppCompatTaskInfo.CameraCompatControlState;
+import android.app.CameraCompatTaskInfo.CameraCompatControlState;
import android.app.TaskInfo;
import android.content.Context;
import android.graphics.Rect;
@@ -81,7 +81,12 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
mCallback = callback;
mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
- mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState;
+ if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) {
+ // Don't show the SCM button for freeform tasks
+ mHasSizeCompat &= !taskInfo.isFreeform();
+ }
+ mCameraCompatControlState =
+ taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState;
mCompatUIHintsState = compatUIHintsState;
mCompatUIConfiguration = compatUIConfiguration;
mOnRestartButtonClicked = onRestartButtonClicked;
@@ -135,7 +140,12 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
final boolean prevHasSizeCompat = mHasSizeCompat;
final int prevCameraCompatControlState = mCameraCompatControlState;
mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
- mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState;
+ if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) {
+ // Don't show the SCM button for freeform tasks
+ mHasSizeCompat &= !taskInfo.isFreeform();
+ }
+ mCameraCompatControlState =
+ taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState;
if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) {
return false;
@@ -217,14 +227,30 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
@VisibleForTesting
boolean shouldShowSizeCompatRestartButton(@NonNull TaskInfo taskInfo) {
- if (!Flags.allowHideScmButton()) {
+ // Always show button if display is phone sized.
+ if (!Flags.allowHideScmButton() || taskInfo.configuration.smallestScreenWidthDp
+ < LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP) {
+ return true;
+ }
+
+ final int letterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxWidth;
+ final int letterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxHeight;
+ final Rect stableBounds = getTaskStableBounds();
+ final int appWidth = stableBounds.width();
+ final int appHeight = stableBounds.height();
+ // App is floating, should always show restart button.
+ if (appWidth > letterboxWidth && appHeight > letterboxHeight) {
return true;
}
- final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo;
- final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds();
- final int letterboxArea = computeArea(appCompatTaskInfo.topActivityLetterboxWidth,
- appCompatTaskInfo.topActivityLetterboxHeight);
- final int taskArea = computeArea(taskBounds.width(), taskBounds.height());
+ // If app fills the width of the display, don't show restart button (for landscape apps)
+ // if device has a custom tolerance value.
+ if (mHideScmTolerance != mCompatUIConfiguration.getDefaultHideRestartButtonTolerance()
+ && appWidth == letterboxWidth) {
+ return false;
+ }
+
+ final int letterboxArea = letterboxWidth * letterboxHeight;
+ final int taskArea = appWidth * appHeight;
if (letterboxArea == 0 || taskArea == 0) {
return false;
}
@@ -232,13 +258,6 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
return percentageAreaOfLetterboxInTask < mHideScmTolerance;
}
- private int computeArea(int width, int height) {
- if (width == 0 || height == 0) {
- return 0;
- }
- return width * height;
- }
-
private void updateVisibilityOfViews() {
if (mLayout == null) {
return;
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/compatui/UserAspectRatioSettingsWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java
index 7c280994042b..8fb4bdbea933 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java
@@ -237,7 +237,8 @@ class UserAspectRatioSettingsWindowManager extends CompatUIWindowManagerAbstract
final int letterboxWidth = taskInfo.topActivityLetterboxWidth;
// App is not visibly letterboxed if it covers status bar/bottom insets or matches the
// stable bounds, so don't show the button
- if (stableBounds.height() <= letterboxHeight && stableBounds.width() <= letterboxWidth) {
+ if (stableBounds.height() <= letterboxHeight && stableBounds.width() <= letterboxWidth
+ && !taskInfo.isUserFullscreenOverrideEnabled) {
return false;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
index 216da070754b..011093718671 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
@@ -31,10 +31,9 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.SystemWindows;
import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.dagger.pip.TvPipModule;
-import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.splitscreen.tv.TvSplitScreenController;
import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm;
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 8b2ec0a35685..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
@@ -29,6 +29,7 @@ import android.window.SystemPerformanceHinter;
import com.android.internal.logging.UiEventLogger;
import com.android.launcher3.icons.IconProvider;
+import com.android.window.flags.Flags;
import com.android.wm.shell.ProtoLogController;
import com.android.wm.shell.R;
import com.android.wm.shell.RootDisplayAreaOrganizer;
@@ -57,10 +58,6 @@ import com.android.wm.shell.common.SystemWindows;
import com.android.wm.shell.common.TabletopModeController;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.annotations.ShellAnimationThread;
-import com.android.wm.shell.common.annotations.ShellBackgroundThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
import com.android.wm.shell.common.pip.PhonePipKeepClearAlgorithm;
import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
@@ -75,7 +72,6 @@ import com.android.wm.shell.compatui.CompatUIConfiguration;
import com.android.wm.shell.compatui.CompatUIController;
import com.android.wm.shell.compatui.CompatUIShellCommandHandler;
import com.android.wm.shell.desktopmode.DesktopMode;
-import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.displayareahelper.DisplayAreaHelper;
@@ -91,6 +87,13 @@ 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;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellSplashscreenThread;
import com.android.wm.shell.splitscreen.SplitScreen;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.startingsurface.StartingSurface;
@@ -105,7 +108,7 @@ import com.android.wm.shell.taskview.TaskViewFactory;
import com.android.wm.shell.taskview.TaskViewFactoryController;
import com.android.wm.shell.taskview.TaskViewTransitions;
import com.android.wm.shell.transition.HomeTransitionObserver;
-import com.android.wm.shell.transition.ShellTransitions;
+import com.android.wm.shell.transition.MixedTransitionHandler;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
import com.android.wm.shell.unfold.UnfoldAnimationController;
@@ -326,7 +329,8 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
static MultiInstanceHelper provideMultiInstanceHelper(Context context) {
- return new MultiInstanceHelper(context, context.getPackageManager());
+ return new MultiInstanceHelper(context, context.getPackageManager(),
+ Flags.supportsMultiInstanceSystemUi());
}
//
@@ -616,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
@@ -673,6 +678,22 @@ public abstract class WMShellBaseModule {
return new TaskViewTransitions(transitions);
}
+ // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
+ @BindsOptionalOf
+ @DynamicOverride
+ abstract MixedTransitionHandler optionalMixedTransitionHandler();
+
+ @WMSingleton
+ @Provides
+ static Optional<MixedTransitionHandler> provideMixedTransitionHandler(
+ @DynamicOverride Optional<MixedTransitionHandler> mixedTransitionHandler
+ ) {
+ if (mixedTransitionHandler.isPresent()) {
+ return mixedTransitionHandler;
+ }
+ return Optional.empty();
+ }
+
//
// Keyguard transitions (optional feature)
//
@@ -683,10 +704,12 @@ public abstract class WMShellBaseModule {
ShellInit shellInit,
ShellController shellController,
Transitions transitions,
+ TaskStackListenerImpl taskStackListener,
@ShellMainThread Handler mainHandler,
@ShellMainThread ShellExecutor mainExecutor) {
return new KeyguardTransitionHandler(
- shellInit, shellController, transitions, mainHandler, mainExecutor);
+ shellInit, shellController, transitions, taskStackListener, mainHandler,
+ mainExecutor);
}
@WMSingleton
@@ -846,8 +869,10 @@ public abstract class WMShellBaseModule {
static ShellController provideShellController(Context context,
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
+ DisplayInsetsController displayInsetsController,
@ShellMainThread ShellExecutor mainExecutor) {
- return new ShellController(context, shellInit, shellCommandHandler, mainExecutor);
+ return new ShellController(context, shellInit, shellCommandHandler,
+ displayInsetsController, mainExecutor);
}
//
@@ -868,13 +893,13 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
- static Optional<DesktopTasksController> providesDesktopTasksController(
+ static Optional<DesktopTasksController> providesDesktopTasksController(Context context,
@DynamicOverride Optional<Lazy<DesktopTasksController>> desktopTasksController) {
// Use optional-of-lazy for the dependency that this provider relies on.
// Lazy ensures that this provider will not be the cause the dependency is created
// when it will not be returned due to the condition below.
return desktopTasksController.flatMap((lazy)-> {
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.of(lazy.get());
}
return Optional.empty();
@@ -887,13 +912,13 @@ public abstract class WMShellBaseModule {
@WMSingleton
@Provides
- static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository(
+ static Optional<DesktopModeTaskRepository> provideDesktopTaskRepository(Context context,
@DynamicOverride Optional<Lazy<DesktopModeTaskRepository>> desktopModeTaskRepository) {
// Use optional-of-lazy for the dependency that this provider relies on.
// Lazy ensures that this provider will not be the cause the dependency is created
// when it will not be returned due to the condition below.
return desktopModeTaskRepository.flatMap((lazy)-> {
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
return Optional.of(lazy.get());
}
return Optional.empty();
@@ -901,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
//
@@ -930,6 +968,7 @@ public abstract class WMShellBaseModule {
Optional<OneHandedController> oneHandedControllerOptional,
Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
Optional<ActivityEmbeddingController> activityEmbeddingOptional,
+ Optional<MixedTransitionHandler> mixedTransitionHandler,
Transitions transitions,
StartingWindowController startingWindow,
ProtoLogController protoLogController,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
index 0cc545a7724a..c5644a8f6876 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
@@ -33,11 +33,11 @@ import androidx.annotation.Nullable;
import com.android.wm.shell.R;
import com.android.wm.shell.common.HandlerExecutor;
import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ExternalMainThread;
-import com.android.wm.shell.common.annotations.ShellAnimationThread;
-import com.android.wm.shell.common.annotations.ShellBackgroundThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
+import com.android.wm.shell.shared.annotations.ExternalMainThread;
+import com.android.wm.shell.shared.annotations.ShellAnimationThread;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellSplashscreenThread;
import dagger.Module;
import dagger.Provides;
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 fb3c35b6a1e3..87bd84017dee 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
@@ -29,6 +29,7 @@ import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.statusbar.IStatusBarService;
import com.android.launcher3.icons.IconProvider;
+import com.android.window.flags.Flags;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.WindowManagerShellWrapper;
@@ -52,14 +53,14 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.annotations.ShellAnimationThread;
-import com.android.wm.shell.common.annotations.ShellBackgroundThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.dagger.back.ShellBackAnimationModule;
import com.android.wm.shell.dagger.pip.PipModule;
-import com.android.wm.shell.desktopmode.DesktopModeStatus;
+import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
+import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.desktopmode.DesktopTasksController;
+import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
+import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver;
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
@@ -75,6 +76,10 @@ import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.annotations.ShellAnimationThread;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
@@ -82,6 +87,7 @@ import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.taskview.TaskViewTransitions;
import com.android.wm.shell.transition.DefaultMixedHandler;
import com.android.wm.shell.transition.HomeTransitionObserver;
+import com.android.wm.shell.transition.MixedTransitionHandler;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
import com.android.wm.shell.unfold.UnfoldAnimationController;
@@ -115,9 +121,9 @@ import java.util.Optional;
*/
@Module(
includes = {
- WMShellBaseModule.class,
- PipModule.class,
- ShellBackAnimationModule.class,
+ WMShellBaseModule.class,
+ PipModule.class,
+ ShellBackAnimationModule.class,
})
public abstract class WMShellModule {
@@ -215,7 +221,7 @@ public abstract class WMShellModule {
Transitions transitions,
Optional<DesktopTasksController> desktopTasksController,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
return new DesktopModeWindowDecorViewModel(
context,
mainExecutor,
@@ -239,6 +245,7 @@ public abstract class WMShellModule {
mainChoreographer,
taskOrganizer,
displayController,
+ rootTaskDisplayAreaOrganizer,
syncQueue,
transitions);
}
@@ -271,8 +278,8 @@ public abstract class WMShellModule {
ShellInit init = FreeformComponents.isFreeformEnabled(context)
? shellInit
: null;
- return new FreeformTaskListener(init, shellTaskOrganizer, desktopModeTaskRepository,
- windowDecorViewModel);
+ return new FreeformTaskListener(context, init, shellTaskOrganizer,
+ desktopModeTaskRepository, windowDecorViewModel);
}
@WMSingleton
@@ -367,8 +374,9 @@ public abstract class WMShellModule {
//
@WMSingleton
+ @DynamicOverride
@Provides
- static DefaultMixedHandler provideDefaultMixedHandler(
+ static MixedTransitionHandler provideMixedTransitionHandler(
ShellInit shellInit,
Optional<SplitScreenController> splitScreenOptional,
@Nullable PipTransitionController pipTransitionController,
@@ -509,25 +517,47 @@ public abstract class WMShellModule {
ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
DragToDesktopTransitionHandler dragToDesktopTransitionHandler,
@DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
+ DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
LaunchAdjacentController launchAdjacentController,
RecentsTransitionHandler recentsTransitionHandler,
MultiInstanceHelper multiInstanceHelper,
- @ShellMainThread ShellExecutor mainExecutor
- ) {
+ @ShellMainThread ShellExecutor mainExecutor,
+ Optional<DesktopTasksLimiter> desktopTasksLimiter,
+ Optional<RecentTasksController> recentTasksController) {
return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController,
displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer,
dragAndDropController, transitions, enterDesktopTransitionHandler,
exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler,
- dragToDesktopTransitionHandler, desktopModeTaskRepository, launchAdjacentController,
- recentsTransitionHandler, multiInstanceHelper, mainExecutor);
+ dragToDesktopTransitionHandler, desktopModeTaskRepository,
+ desktopModeLoggerTransitionObserver, launchAdjacentController,
+ recentsTransitionHandler, multiInstanceHelper,
+ mainExecutor, desktopTasksLimiter, recentTasksController.orElse(null));
}
@WMSingleton
@Provides
+ static Optional<DesktopTasksLimiter> provideDesktopTasksLimiter(
+ Context context,
+ Transitions transitions,
+ @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository,
+ ShellTaskOrganizer shellTaskOrganizer) {
+ if (!DesktopModeStatus.canEnterDesktopMode(context)
+ || !Flags.enableDesktopWindowingTaskLimit()) {
+ return Optional.empty();
+ }
+ return Optional.of(
+ new DesktopTasksLimiter(
+ transitions, desktopModeTaskRepository, shellTaskOrganizer));
+ }
+
+
+ @WMSingleton
+ @Provides
static DragToDesktopTransitionHandler provideDragToDesktopTransitionHandler(
Context context,
Transitions transitions,
- RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+ Optional<DesktopTasksLimiter> desktopTasksLimiter) {
return new DragToDesktopTransitionHandler(context, transitions,
rootTaskDisplayAreaOrganizer);
}
@@ -535,7 +565,8 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler(
- Transitions transitions) {
+ Transitions transitions,
+ Optional<DesktopTasksLimiter> desktopTasksLimiter) {
return new EnterDesktopTaskTransitionHandler(transitions);
}
@@ -562,6 +593,37 @@ public abstract class WMShellModule {
return new DesktopModeTaskRepository();
}
+ @WMSingleton
+ @Provides
+ static Optional<DesktopTasksTransitionObserver> provideDesktopTasksTransitionObserver(
+ Context context,
+ Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ Transitions transitions,
+ ShellInit shellInit
+ ) {
+ return desktopModeTaskRepository.flatMap(repository ->
+ Optional.of(new DesktopTasksTransitionObserver(
+ context, repository, transitions, shellInit))
+ );
+ }
+
+ @WMSingleton
+ @Provides
+ static DesktopModeLoggerTransitionObserver provideDesktopModeLoggerTransitionObserver(
+ Context context,
+ ShellInit shellInit,
+ Transitions transitions,
+ DesktopModeEventLogger desktopModeEventLogger) {
+ return new DesktopModeLoggerTransitionObserver(
+ context, shellInit, transitions, desktopModeEventLogger);
+ }
+
+ @WMSingleton
+ @Provides
+ static DesktopModeEventLogger provideDesktopModeEventLogger() {
+ return new DesktopModeEventLogger();
+ }
+
//
// Drag and drop
//
@@ -602,7 +664,8 @@ public abstract class WMShellModule {
@Provides
static Object provideIndependentShellComponentsToCreate(
DragAndDropController dragAndDropController,
- DefaultMixedHandler defaultMixedHandler) {
+ Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional
+ ) {
return new Object();
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java
index 795bc1a7113b..d2895b149b2c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java
@@ -16,9 +16,9 @@
package com.android.wm.shell.dagger.back;
-import com.android.wm.shell.back.CrossActivityBackAnimation;
import com.android.wm.shell.back.CrossTaskBackAnimation;
-import com.android.wm.shell.back.CustomizeActivityAnimation;
+import com.android.wm.shell.back.CustomCrossActivityBackAnimation;
+import com.android.wm.shell.back.DefaultCrossActivityBackAnimation;
import com.android.wm.shell.back.ShellBackAnimation;
import com.android.wm.shell.back.ShellBackAnimationRegistry;
@@ -47,7 +47,7 @@ public interface ShellBackAnimationModule {
@Binds
@ShellBackAnimation.CrossActivity
ShellBackAnimation bindCrossActivityShellBackAnimation(
- CrossActivityBackAnimation crossActivityBackAnimation);
+ DefaultCrossActivityBackAnimation defaultCrossActivityBackAnimation);
/** Default cross task back animation */
@Binds
@@ -59,5 +59,5 @@ public interface ShellBackAnimationModule {
@Binds
@ShellBackAnimation.CustomizeActivity
ShellBackAnimation provideCustomizeActivityShellBackAnimation(
- CustomizeActivityAnimation customizeActivityAnimation);
+ CustomCrossActivityBackAnimation customCrossActivityBackAnimation);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
index 1e3d7fb06da2..677fd5deffd3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
@@ -29,7 +29,6 @@ import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.SystemWindows;
import com.android.wm.shell.common.TabletopModeController;
import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.common.pip.PhonePipKeepClearAlgorithm;
import com.android.wm.shell.common.pip.PipAppOpsListener;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
@@ -56,10 +55,12 @@ import com.android.wm.shell.pip.phone.PhonePipMenuController;
import com.android.wm.shell.pip.phone.PipController;
import com.android.wm.shell.pip.phone.PipMotionHelper;
import com.android.wm.shell.pip.phone.PipTouchHandler;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.splitscreen.SplitScreenController;
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.HomeTransitionObserver;
import com.android.wm.shell.transition.Transitions;
import dagger.Module;
@@ -192,11 +193,12 @@ public abstract class Pip1Module {
PipBoundsState pipBoundsState, PipDisplayLayoutState pipDisplayLayoutState,
PipTransitionState pipTransitionState, PhonePipMenuController pipMenuController,
PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
+ HomeTransitionObserver homeTransitionObserver,
Optional<SplitScreenController> splitScreenOptional) {
return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
pipBoundsState, pipDisplayLayoutState, pipTransitionState, pipMenuController,
pipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper,
- splitScreenOptional);
+ homeTransitionObserver, splitScreenOptional);
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 458ea05e620d..696831747865 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -23,21 +23,30 @@ import android.os.Handler;
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.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SystemWindows;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.common.pip.PipMediaController;
+import com.android.wm.shell.common.pip.PipPerfHintController;
+import com.android.wm.shell.common.pip.PipSnapAlgorithm;
import com.android.wm.shell.common.pip.PipUiEventLogger;
import com.android.wm.shell.common.pip.PipUtils;
+import com.android.wm.shell.common.pip.SizeSpecSource;
import com.android.wm.shell.dagger.WMShellBaseModule;
import com.android.wm.shell.dagger.WMSingleton;
import com.android.wm.shell.pip2.phone.PhonePipMenuController;
import com.android.wm.shell.pip2.phone.PipController;
+import com.android.wm.shell.pip2.phone.PipMotionHelper;
import com.android.wm.shell.pip2.phone.PipScheduler;
+import com.android.wm.shell.pip2.phone.PipTouchHandler;
import com.android.wm.shell.pip2.phone.PipTransition;
+import com.android.wm.shell.pip2.phone.PipTransitionState;
+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;
@@ -62,15 +71,19 @@ public abstract class Pip2Module {
PipBoundsState pipBoundsState,
PipBoundsAlgorithm pipBoundsAlgorithm,
Optional<PipController> pipController,
- @NonNull PipScheduler pipScheduler) {
+ PipTouchHandler pipTouchHandler,
+ @NonNull PipScheduler pipScheduler,
+ @NonNull PipTransitionState pipStackListenerController) {
return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
- pipBoundsState, null, pipBoundsAlgorithm, pipScheduler);
+ pipBoundsState, null, pipBoundsAlgorithm, pipScheduler,
+ pipStackListenerController);
}
@WMSingleton
@Provides
static Optional<PipController> providePipController(Context context,
ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
ShellController shellController,
DisplayController displayController,
DisplayInsetsController displayInsetsController,
@@ -78,14 +91,18 @@ public abstract class Pip2Module {
PipBoundsAlgorithm pipBoundsAlgorithm,
PipDisplayLayoutState pipDisplayLayoutState,
PipScheduler pipScheduler,
+ TaskStackListenerImpl taskStackListener,
+ ShellTaskOrganizer shellTaskOrganizer,
+ PipTransitionState pipTransitionState,
@ShellMainThread ShellExecutor mainExecutor) {
if (!PipUtils.isPip2ExperimentEnabled()) {
return Optional.empty();
} else {
return Optional.ofNullable(PipController.create(
- context, shellInit, shellController, displayController, displayInsetsController,
- pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler,
- mainExecutor));
+ context, shellInit, shellCommandHandler, shellController, displayController,
+ displayInsetsController, pipBoundsState, pipBoundsAlgorithm,
+ pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer,
+ pipTransitionState, mainExecutor));
}
}
@@ -93,8 +110,9 @@ public abstract class Pip2Module {
@Provides
static PipScheduler providePipScheduler(Context context,
PipBoundsState pipBoundsState,
- @ShellMainThread ShellExecutor mainExecutor) {
- return new PipScheduler(context, pipBoundsState, mainExecutor);
+ @ShellMainThread ShellExecutor mainExecutor,
+ PipTransitionState pipTransitionState) {
+ return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState);
}
@WMSingleton
@@ -108,4 +126,48 @@ public abstract class Pip2Module {
return new PhonePipMenuController(context, pipBoundsState, pipMediaController,
systemWindows, pipUiEventLogger, mainExecutor, mainHandler);
}
+
+
+ @WMSingleton
+ @Provides
+ static PipTouchHandler providePipTouchHandler(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ PhonePipMenuController menuPhoneController,
+ PipBoundsAlgorithm pipBoundsAlgorithm,
+ @NonNull PipBoundsState pipBoundsState,
+ @NonNull PipTransitionState pipTransitionState,
+ @NonNull PipScheduler pipScheduler,
+ @NonNull SizeSpecSource sizeSpecSource,
+ PipMotionHelper pipMotionHelper,
+ FloatingContentCoordinator floatingContentCoordinator,
+ PipUiEventLogger pipUiEventLogger,
+ @ShellMainThread ShellExecutor mainExecutor,
+ Optional<PipPerfHintController> pipPerfHintControllerOptional) {
+ return new PipTouchHandler(context, shellInit, shellCommandHandler, menuPhoneController,
+ pipBoundsAlgorithm, pipBoundsState, pipTransitionState, pipScheduler,
+ sizeSpecSource, pipMotionHelper, floatingContentCoordinator, pipUiEventLogger,
+ mainExecutor, pipPerfHintControllerOptional);
+ }
+
+ @WMSingleton
+ @Provides
+ static PipMotionHelper providePipMotionHelper(Context context,
+ PipBoundsState pipBoundsState, PhonePipMenuController menuController,
+ PipSnapAlgorithm pipSnapAlgorithm,
+ FloatingContentCoordinator floatingContentCoordinator,
+ PipScheduler pipScheduler,
+ Optional<PipPerfHintController> pipPerfHintControllerOptional,
+ PipBoundsAlgorithm pipBoundsAlgorithm,
+ PipTransitionState pipTransitionState) {
+ return new PipMotionHelper(context, pipBoundsState, menuController, pipSnapAlgorithm,
+ floatingContentCoordinator, pipScheduler, pipPerfHintControllerOptional,
+ pipBoundsAlgorithm, pipTransitionState);
+ }
+
+ @WMSingleton
+ @Provides
+ static PipTransitionState providePipTransitionState(@ShellMainThread Handler handler) {
+ return new PipTransitionState(handler);
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
index 54c2aeab4976..8d1b15c1e631 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
@@ -29,7 +29,6 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.SystemWindows;
import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.common.pip.LegacySizeSpecSource;
import com.android.wm.shell.common.pip.PipAppOpsListener;
import com.android.wm.shell.common.pip.PipDisplayLayoutState;
@@ -53,6 +52,7 @@ import com.android.wm.shell.pip.tv.TvPipMenuController;
import com.android.wm.shell.pip.tv.TvPipNotificationController;
import com.android.wm.shell.pip.tv.TvPipTaskOrganizer;
import com.android.wm.shell.pip.tv.TvPipTransition;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
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 1071d728a56d..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,7 +18,8 @@ package com.android.wm.shell.desktopmode;
import android.graphics.Region;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.common.desktopmode.DesktopModeTransitionSource;
+import com.android.wm.shell.shared.annotations.ExternalThread;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
@@ -49,8 +50,11 @@ public interface DesktopMode {
/** Called when requested to go to desktop mode from the current focused app. */
- void enterDesktop(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 95d47146e834..9192e6ed3175 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
@@ -20,9 +20,7 @@ import com.android.internal.util.FrameworkStatsLog
import com.android.wm.shell.protolog.ShellProtoLogGroup
import com.android.wm.shell.util.KtProtoLog
-/**
- * Event logger for logging desktop mode session events
- */
+/** Event logger for logging desktop mode session events */
class DesktopModeEventLogger {
/**
* Logs the enter of desktop mode having session id [sessionId] and the reason [enterReason] for
@@ -32,13 +30,16 @@ class DesktopModeEventLogger {
KtProtoLog.v(
ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"DesktopModeLogger: Logging session enter, session: %s reason: %s",
- sessionId, enterReason.name
+ sessionId,
+ enterReason.name
)
- FrameworkStatsLog.write(DESKTOP_MODE_ATOM_ID,
+ FrameworkStatsLog.write(
+ DESKTOP_MODE_ATOM_ID,
/* event */ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__ENTER,
/* enterReason */ enterReason.reason,
/* exitReason */ 0,
- /* session_id */ sessionId)
+ /* session_id */ sessionId
+ )
}
/**
@@ -49,13 +50,16 @@ class DesktopModeEventLogger {
KtProtoLog.v(
ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"DesktopModeLogger: Logging session exit, session: %s reason: %s",
- sessionId, exitReason.name
+ sessionId,
+ exitReason.name
)
- FrameworkStatsLog.write(DESKTOP_MODE_ATOM_ID,
+ FrameworkStatsLog.write(
+ DESKTOP_MODE_ATOM_ID,
/* event */ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EVENT__EXIT,
/* enterReason */ 0,
/* exitReason */ exitReason.reason,
- /* session_id */ sessionId)
+ /* session_id */ sessionId
+ )
}
/**
@@ -66,9 +70,11 @@ class DesktopModeEventLogger {
KtProtoLog.v(
ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"DesktopModeLogger: Logging task added, session: %s taskId: %s",
- sessionId, taskUpdate.instanceId
+ sessionId,
+ taskUpdate.instanceId
)
- FrameworkStatsLog.write(DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
+ FrameworkStatsLog.write(
+ DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
/* task_event */
FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED,
/* instance_id */
@@ -84,7 +90,8 @@ class DesktopModeEventLogger {
/* task_y */
taskUpdate.taskY,
/* session_id */
- sessionId)
+ sessionId
+ )
}
/**
@@ -95,9 +102,11 @@ class DesktopModeEventLogger {
KtProtoLog.v(
ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"DesktopModeLogger: Logging task remove, session: %s taskId: %s",
- sessionId, taskUpdate.instanceId
+ sessionId,
+ taskUpdate.instanceId
)
- FrameworkStatsLog.write(DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
+ FrameworkStatsLog.write(
+ DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
/* task_event */
FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED,
/* instance_id */
@@ -113,7 +122,8 @@ class DesktopModeEventLogger {
/* task_y */
taskUpdate.taskY,
/* session_id */
- sessionId)
+ sessionId
+ )
}
/**
@@ -124,9 +134,11 @@ class DesktopModeEventLogger {
KtProtoLog.v(
ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"DesktopModeLogger: Logging task info changed, session: %s taskId: %s",
- sessionId, taskUpdate.instanceId
+ sessionId,
+ taskUpdate.instanceId
)
- FrameworkStatsLog.write(DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
+ FrameworkStatsLog.write(
+ DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
/* task_event */
FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED,
/* instance_id */
@@ -142,7 +154,8 @@ class DesktopModeEventLogger {
/* task_y */
taskUpdate.taskY,
/* session_id */
- sessionId)
+ sessionId
+ )
}
companion object {
@@ -160,12 +173,8 @@ class DesktopModeEventLogger {
* stats/atoms/desktopmode/desktopmode_extensions_atoms.proto
*/
enum class EnterReason(val reason: Int) {
- UNKNOWN_ENTER(
- FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__UNKNOWN_ENTER
- ),
- OVERVIEW(
- FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__OVERVIEW
- ),
+ UNKNOWN_ENTER(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__UNKNOWN_ENTER),
+ OVERVIEW(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__OVERVIEW),
APP_HANDLE_DRAG(
FrameworkStatsLog.DESKTOP_MODE_UICHANGED__ENTER_REASON__APP_HANDLE_DRAG
),
@@ -178,9 +187,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
+ ),
}
/**
@@ -188,12 +198,8 @@ class DesktopModeEventLogger {
* stats/atoms/desktopmode/desktopmode_extensions_atoms.proto
*/
enum class ExitReason(val reason: Int) {
- UNKNOWN_EXIT(
- FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__UNKNOWN_EXIT
- ),
- DRAG_TO_EXIT(
- FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT
- ),
+ UNKNOWN_EXIT(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__UNKNOWN_EXIT),
+ DRAG_TO_EXIT(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT),
APP_HANDLE_MENU_BUTTON_EXIT(
FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__APP_HANDLE_MENU_BUTTON_EXIT
),
@@ -201,18 +207,14 @@ class DesktopModeEventLogger {
FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__KEYBOARD_SHORTCUT_EXIT
),
RETURN_HOME_OR_OVERVIEW(
- FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME
- ),
- TASK_FINISHED(
- FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__TASK_FINISHED
+ FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__RETURN_HOME_OR_OVERVIEW
),
- SCREEN_OFF(
- FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__SCREEN_OFF
- )
+ TASK_FINISHED(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__TASK_FINISHED),
+ SCREEN_OFF(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__SCREEN_OFF)
}
private const val DESKTOP_MODE_ATOM_ID = FrameworkStatsLog.DESKTOP_MODE_UI_CHANGED
private const val DESKTOP_MODE_TASK_UPDATE_ATOM_ID =
FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE
}
-} \ No newline at end of file
+}
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
new file mode 100644
index 000000000000..641952b28bfb
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -0,0 +1,394 @@
+/*
+ * 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.app.ActivityManager.RunningTaskInfo
+import android.app.ActivityTaskManager.INVALID_TASK_ID
+import android.app.TaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.content.Context
+import android.os.IBinder
+import android.util.SparseArray
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.containsKey
+import androidx.core.util.forEach
+import androidx.core.util.isEmpty
+import androidx.core.util.isNotEmpty
+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
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.util.KtProtoLog
+
+/**
+ * A [Transitions.TransitionObserver] that observes transitions and the proposed changes to log
+ * appropriate desktop mode session log events. This observes transitions related to desktop mode
+ * and other transitions that originate both within and outside shell.
+ */
+class DesktopModeLoggerTransitionObserver(
+ context: Context,
+ shellInit: ShellInit,
+ private val transitions: Transitions,
+ private val desktopModeEventLogger: DesktopModeEventLogger
+) : Transitions.TransitionObserver {
+
+ private val idSequence: InstanceIdSequence by lazy { InstanceIdSequence(Int.MAX_VALUE) }
+
+ init {
+ if (
+ Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.canEnterDesktopMode(context)
+ ) {
+ shellInit.addInitCallback(this::onInit, this)
+ }
+ }
+
+ // A sparse array of visible freeform tasks and taskInfos
+ private val visibleFreeformTaskInfos: SparseArray<TaskInfo> = SparseArray()
+
+ // Caching the taskInfos to handle canceled recents animations, if we identify that the recents
+ // 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
+
+ private val isSessionActive: Boolean
+ get() = loggerInstanceId != null
+
+ private fun setSessionInactive() {
+ loggerInstanceId = null
+ }
+
+ fun onInit() {
+ transitions.registerObserver(this)
+ }
+
+ override fun onTransitionReady(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction
+ ) {
+ // this was a new recents animation
+ if (info.isExitToRecentsTransition() && tasksSavedForRecents.isEmpty()) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopModeLogger: Recents animation running, saving tasks for later"
+ )
+ // TODO (b/326391303) - avoid logging session exit if we can identify a cancelled
+ // recents animation
+
+ // when recents animation is running, all freeform tasks are sent TO_BACK temporarily
+ // if the user ends up at home, we need to update the visible freeform tasks
+ // if the user cancels the animation, the subsequent transition is NONE
+ // if the user opens a new task, the subsequent transition is OPEN with flag
+ tasksSavedForRecents.putAll(visibleFreeformTaskInfos)
+ }
+
+ // figure out what the new state of freeform tasks would be post transition
+ var postTransitionVisibleFreeformTasks = getPostTransitionVisibleFreeformTaskInfos(info)
+
+ // A canceled recents animation is followed by a TRANSIT_NONE transition with no flags, if
+ // that's the case, we might have accidentally logged a session exit and would need to
+ // revaluate again. Add all the tasks back.
+ // This will start a new desktop mode session.
+ if (
+ info.type == WindowManager.TRANSIT_NONE &&
+ info.flags == 0 &&
+ tasksSavedForRecents.isNotEmpty()
+ ) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopModeLogger: Canceled recents animation, restoring tasks"
+ )
+ // restore saved tasks in the updated set and clear for next use
+ postTransitionVisibleFreeformTasks += tasksSavedForRecents
+ tasksSavedForRecents.clear()
+ }
+
+ // identify if we need to log any changes and update the state of visible freeform tasks
+ identifyLogEventAndUpdateState(
+ transitionInfo = info,
+ preTransitionVisibleFreeformTasks = visibleFreeformTaskInfos,
+ postTransitionVisibleFreeformTasks = postTransitionVisibleFreeformTasks
+ )
+ wasPreviousTransitionExitToOverview = info.isExitToRecentsTransition()
+ }
+
+ override fun onTransitionStarting(transition: IBinder) {}
+
+ override fun onTransitionMerged(merged: IBinder, playing: IBinder) {}
+
+ override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {}
+
+ private fun getPostTransitionVisibleFreeformTaskInfos(
+ info: TransitionInfo
+ ): SparseArray<TaskInfo> {
+ // device is sleeping, so no task will be visible anymore
+ if (info.type == WindowManager.TRANSIT_SLEEP) {
+ return SparseArray()
+ }
+
+ // filter changes involving freeform tasks or tasks that were cached in previous state
+ val changesToFreeformWindows =
+ info.changes
+ .filter { it.taskInfo != null && it.requireTaskInfo().taskId != INVALID_TASK_ID }
+ .filter {
+ it.requireTaskInfo().isFreeformWindow() ||
+ visibleFreeformTaskInfos.containsKey(it.requireTaskInfo().taskId)
+ }
+
+ val postTransitionFreeformTasks: SparseArray<TaskInfo> = SparseArray()
+ // start off by adding all existing tasks
+ postTransitionFreeformTasks.putAll(visibleFreeformTaskInfos)
+
+ // the combined set of taskInfos we are interested in this transition change
+ for (change in changesToFreeformWindows) {
+ val taskInfo = change.requireTaskInfo()
+
+ // check if this task existed as freeform window in previous cached state and it's now
+ // changing window modes
+ if (
+ visibleFreeformTaskInfos.containsKey(taskInfo.taskId) &&
+ visibleFreeformTaskInfos.get(taskInfo.taskId).isFreeformWindow() &&
+ !taskInfo.isFreeformWindow()
+ ) {
+ postTransitionFreeformTasks.remove(taskInfo.taskId)
+ // no need to evaluate new visibility of this task, since it's no longer a freeform
+ // window
+ continue
+ }
+
+ // check if the task is visible after this change, otherwise remove it
+ if (isTaskVisibleAfterChange(change)) {
+ postTransitionFreeformTasks.put(taskInfo.taskId, taskInfo)
+ } else {
+ postTransitionFreeformTasks.remove(taskInfo.taskId)
+ }
+ }
+
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopModeLogger: taskInfo map after processing changes %s",
+ postTransitionFreeformTasks.size()
+ )
+
+ return postTransitionFreeformTasks
+ }
+
+ /**
+ * Look at the [TransitionInfo.Change] and figure out if this task will be visible after this
+ * change is processed
+ */
+ private fun isTaskVisibleAfterChange(change: TransitionInfo.Change): Boolean =
+ when {
+ TransitionUtil.isOpeningType(change.mode) -> true
+ TransitionUtil.isClosingType(change.mode) -> false
+ // change mode TRANSIT_CHANGE is only for visible to visible transitions
+ change.mode == WindowManager.TRANSIT_CHANGE -> true
+ else -> false
+ }
+
+ /**
+ * Log the appropriate log event based on the new state of TasksInfos and previously cached
+ * state and update it
+ */
+ private fun identifyLogEventAndUpdateState(
+ transitionInfo: TransitionInfo,
+ preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>,
+ postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>
+ ) {
+ if (
+ postTransitionVisibleFreeformTasks.isEmpty() &&
+ preTransitionVisibleFreeformTasks.isNotEmpty() &&
+ isSessionActive
+ ) {
+ // Sessions is finishing, log task updates followed by an exit event
+ identifyAndLogTaskUpdates(
+ loggerInstanceId!!.id,
+ preTransitionVisibleFreeformTasks,
+ postTransitionVisibleFreeformTasks
+ )
+
+ desktopModeEventLogger.logSessionExit(
+ loggerInstanceId!!.id,
+ getExitReason(transitionInfo)
+ )
+
+ setSessionInactive()
+ } else if (
+ postTransitionVisibleFreeformTasks.isNotEmpty() &&
+ preTransitionVisibleFreeformTasks.isEmpty() &&
+ !isSessionActive
+ ) {
+ // Session is starting, log enter event followed by task updates
+ loggerInstanceId = idSequence.newInstanceId()
+ desktopModeEventLogger.logSessionEnter(
+ loggerInstanceId!!.id,
+ getEnterReason(transitionInfo)
+ )
+
+ identifyAndLogTaskUpdates(
+ loggerInstanceId!!.id,
+ preTransitionVisibleFreeformTasks,
+ postTransitionVisibleFreeformTasks
+ )
+ } else if (isSessionActive) {
+ // Session is neither starting, nor finishing, log task updates if there are any
+ identifyAndLogTaskUpdates(
+ loggerInstanceId!!.id,
+ preTransitionVisibleFreeformTasks,
+ postTransitionVisibleFreeformTasks
+ )
+ }
+
+ // update the state to the new version
+ visibleFreeformTaskInfos.clear()
+ 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))
+ }
+ }
+
+ // find old tasks that were removed
+ preTransitionVisibleFreeformTasks.forEach { taskId, taskInfo ->
+ if (!postTransitionVisibleFreeformTasks.containsKey(taskId)) {
+ desktopModeEventLogger.logTaskRemoved(sessionId, buildTaskUpdateForTask(taskInfo))
+ }
+ }
+ }
+
+ // 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
+ }
+
+ /** Get [EnterReason] for this session 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 =
+ when {
+ transitionInfo.type == WindowManager.TRANSIT_SLEEP -> ExitReason.SCREEN_OFF
+ transitionInfo.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED
+ 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
+ fun addTaskInfosToCachedMap(taskInfo: TaskInfo) {
+ visibleFreeformTaskInfos.set(taskInfo.taskId, taskInfo)
+ }
+
+ @VisibleForTesting fun getLoggerSessionId(): Int? = loggerInstanceId?.id
+
+ @VisibleForTesting
+ fun setLoggerSessionId(id: Int) {
+ loggerInstanceId = InstanceId.fakeInstanceId(id)
+ }
+
+ private fun TransitionInfo.Change.requireTaskInfo(): RunningTaskInfo {
+ return this.taskInfo ?: throw IllegalStateException("Expected TaskInfo in the Change")
+ }
+
+ private fun TaskInfo.isFreeformWindow(): Boolean {
+ return this.windowingMode == WINDOWING_MODE_FREEFORM
+ }
+
+ 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 e1e41ee1e64d..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,13 +16,11 @@
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
-/**
- * Handles the shell commands for the DesktopTasksController.
- */
+/** Handles the shell commands for the DesktopTasksController. */
class DesktopModeShellCommandHandler(private val controller: DesktopTasksController) :
ShellCommandHandler.ShellCommandActionHandler {
@@ -36,7 +34,14 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
true
}
}
-
+ "moveToNextDisplay" -> {
+ if (!runMoveToNextDisplay(args, pw)) {
+ pw.println("Task not found. Please enter a valid taskId.")
+ false
+ } else {
+ true
+ }
+ }
else -> {
pw.println("Invalid command: ${args[0]}")
false
@@ -51,18 +56,40 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
return false
}
- val taskId = try {
- args[1].toInt()
- } catch (e: NumberFormatException) {
- pw.println("Error: task id should be an integer")
+ val taskId =
+ try {
+ args[1].toInt()
+ } catch (e: NumberFormatException) {
+ pw.println("Error: task id should be an integer")
+ return false
+ }
+
+ return controller.moveToDesktop(taskId, transitionSource = UNKNOWN)
+ }
+
+ private fun runMoveToNextDisplay(args: Array<String>, pw: PrintWriter): Boolean {
+ if (args.size < 2) {
+ // First argument is the action name.
+ pw.println("Error: task id should be provided as arguments")
return false
}
- return controller.moveToDesktop(taskId, WindowContainerTransaction())
+ val taskId =
+ try {
+ args[1].toInt()
+ } catch (e: NumberFormatException) {
+ pw.println("Error: task id should be an integer")
+ return false
+ }
+
+ controller.moveToNextDisplay(taskId)
+ return true
}
override fun printShellCommandHelp(pw: PrintWriter, prefix: String) {
pw.println("$prefix moveToDesktop <taskId> ")
pw.println("$prefix Move a task with given id to desktop mode.")
+ pw.println("$prefix moveToNextDisplay <taskId> ")
+ pw.println("$prefix Move a task with given id to next display.")
}
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
deleted file mode 100644
index 22ba70860587..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.desktopmode;
-
-import android.os.SystemProperties;
-
-import com.android.window.flags.Flags;
-
-/**
- * Constants for desktop mode feature
- */
-public class DesktopModeStatus {
-
- /**
- * Flag to indicate whether task resizing is veiled.
- */
- private static final boolean IS_VEILED_RESIZE_ENABLED = SystemProperties.getBoolean(
- "persist.wm.debug.desktop_veiled_resizing", true);
-
- /**
- * Flag to indicate is moving task to another display is enabled.
- */
- public static final boolean IS_DISPLAY_CHANGE_ENABLED = SystemProperties.getBoolean(
- "persist.wm.debug.desktop_change_display", false);
-
-
- /**
- * Flag to indicate that desktop stashing is enabled.
- * When enabled, swiping home from desktop stashes the open apps. Next app that launches,
- * will be added to the desktop.
- */
- private static final boolean IS_STASHING_ENABLED = SystemProperties.getBoolean(
- "persist.wm.debug.desktop_stashing", false);
-
- /**
- * Flag to indicate whether to apply shadows to windows in desktop mode.
- */
- private static final boolean USE_WINDOW_SHADOWS = SystemProperties.getBoolean(
- "persist.wm.debug.desktop_use_window_shadows", true);
-
- /**
- * Flag to indicate whether to apply shadows to the focused window in desktop mode.
- *
- * Note: this flag is only relevant if USE_WINDOW_SHADOWS is false.
- */
- private static final boolean USE_WINDOW_SHADOWS_FOCUSED_WINDOW = SystemProperties.getBoolean(
- "persist.wm.debug.desktop_use_window_shadows_focused_window", false);
-
- /**
- * Flag to indicate whether to apply shadows to windows in desktop mode.
- */
- private static final boolean USE_ROUNDED_CORNERS = SystemProperties.getBoolean(
- "persist.wm.debug.desktop_use_rounded_corners", true);
-
- /**
- * Return {@code true} if desktop windowing is enabled
- */
- public static boolean isEnabled() {
- return Flags.enableDesktopWindowingMode();
- }
-
- /**
- * Return {@code true} if veiled resizing is active. If false, fluid resizing is used.
- */
- public static boolean isVeiledResizeEnabled() {
- return IS_VEILED_RESIZE_ENABLED;
- }
-
- /**
- * Return {@code true} if desktop task stashing is enabled when going home.
- * Allows users to use home screen to add tasks to desktop.
- */
- public static boolean isStashingEnabled() {
- return IS_STASHING_ENABLED;
- }
-
- /**
- * Return whether to use window shadows.
- *
- * @param isFocusedWindow whether the window to apply shadows to is focused
- */
- public static boolean useWindowShadow(boolean isFocusedWindow) {
- return USE_WINDOW_SHADOWS
- || (USE_WINDOW_SHADOWS_FOCUSED_WINDOW && isFocusedWindow);
- }
-
- /**
- * Return whether to use rounded corners for windows.
- */
- public static boolean useRoundedCorners() {
- return USE_ROUNDED_CORNERS;
- }
-}
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 7c8fcbb16711..7d01580ecb6e 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
@@ -16,10 +16,13 @@
package com.android.wm.shell.desktopmode
+import android.graphics.Rect
import android.graphics.Region
import android.util.ArrayMap
import android.util.ArraySet
import android.util.SparseArray
+import android.view.Display.INVALID_DISPLAY
+import android.window.WindowContainerToken
import androidx.core.util.forEach
import androidx.core.util.keyIterator
import androidx.core.util.valueIterator
@@ -29,9 +32,7 @@ import java.io.PrintWriter
import java.util.concurrent.Executor
import java.util.function.Consumer
-/**
- * Keeps track of task data related to desktop mode.
- */
+/** Keeps track of task data related to desktop mode. */
class DesktopModeTaskRepository {
/** Task data that is tracked per display */
@@ -44,16 +45,20 @@ class DesktopModeTaskRepository {
*/
val activeTasks: ArraySet<Int> = ArraySet(),
val visibleTasks: ArraySet<Int> = ArraySet(),
- var stashed: Boolean = false
+ val minimizedTasks: ArraySet<Int> = ArraySet(),
+ // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
+ val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
)
- // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
- private val freeformTasksInZOrder = mutableListOf<Int>()
+ // Token of the current wallpaper activity, used to remove it when the last task is removed
+ var wallpaperActivityToken: WindowContainerToken? = null
private val activeTasksListeners = ArraySet<ActiveTasksListener>()
// Track visible tasks separately because a task may be part of the desktop but not visible.
private val visibleTasksListeners = ArrayMap<VisibleTasksListener, Executor>()
// Track corner/caption regions of desktop tasks, used to determine gesture exclusion
private val desktopExclusionRegions = SparseArray<Region>()
+ // Track last bounds of task before toggled to stable bounds
+ private val boundsBeforeMaximizeByTaskId = SparseArray<Rect>()
private var desktopGestureExclusionListener: Consumer<Region>? = null
private var desktopGestureExclusionExecutor: Executor? = null
@@ -77,20 +82,13 @@ class DesktopModeTaskRepository {
activeTasksListeners.add(activeTasksListener)
}
- /**
- * Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not.
- */
- fun addVisibleTasksListener(
- visibleTasksListener: VisibleTasksListener,
- executor: Executor
- ) {
+ /** Add a [VisibleTasksListener] to be notified when freeform tasks are visible or not. */
+ fun addVisibleTasksListener(visibleTasksListener: VisibleTasksListener, executor: Executor) {
visibleTasksListeners[visibleTasksListener] = executor
displayData.keyIterator().forEach { displayId ->
val visibleTasksCount = getVisibleTaskCount(displayId)
- val stashed = isStashed(displayId)
executor.execute {
visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTasksCount)
- visibleTasksListener.onStashedChanged(displayId, stashed)
}
}
}
@@ -107,9 +105,7 @@ class DesktopModeTaskRepository {
}
}
- /**
- * Create a new merged region representative of all exclusion regions in all desktop tasks.
- */
+ /** Create a new merged region representative of all exclusion regions in all desktop tasks. */
private fun calculateDesktopExclusionRegion(): Region {
val desktopExclusionRegion = Region()
desktopExclusionRegions.valueIterator().forEach { taskExclusionRegion ->
@@ -118,16 +114,12 @@ class DesktopModeTaskRepository {
return desktopExclusionRegion
}
- /**
- * Remove a previously registered [ActiveTasksListener]
- */
+ /** Remove a previously registered [ActiveTasksListener] */
fun removeActiveTasksListener(activeTasksListener: ActiveTasksListener) {
activeTasksListeners.remove(activeTasksListener)
}
- /**
- * Remove a previously registered [VisibleTasksListener]
- */
+ /** Remove a previously registered [VisibleTasksListener] */
fun removeVisibleTasksListener(visibleTasksListener: VisibleTasksListener) {
visibleTasksListeners.remove(visibleTasksListener)
}
@@ -177,39 +169,62 @@ class DesktopModeTaskRepository {
return result
}
- /**
- * Check if a task with the given [taskId] was marked as an active task
- */
+ /** 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 ->
data.activeTasks.contains(taskId)
}
}
- /**
- * Whether a task is visible.
- */
+ /** Whether a task is visible. */
fun isVisibleTask(taskId: Int): Boolean {
return displayData.valueIterator().asSequence().any { data ->
data.visibleTasks.contains(taskId)
}
}
- /**
- * Get a set of the active tasks for given [displayId]
- */
+ /** Return whether the given Task is minimized. */
+ fun isMinimizedTask(taskId: Int): Boolean {
+ return displayData.valueIterator().asSequence().any { data ->
+ data.minimizedTasks.contains(taskId)
+ }
+ }
+
+ /** 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
+ }
+ }
+
+ /** Get a set of the active tasks for given [displayId] */
fun getActiveTasks(displayId: Int): ArraySet<Int> {
return ArraySet(displayData[displayId]?.activeTasks)
}
/**
- * Get a list of freeform tasks, ordered from top-bottom (top at index 0).
+ * Returns whether Desktop Mode is currently showing any tasks, i.e. whether any Desktop Tasks
+ * are visible.
+ */
+ fun isDesktopModeShowing(displayId: Int): Boolean = getVisibleTaskCount(displayId) > 0
+
+ /**
+ * Returns a list of Tasks IDs representing all active non-minimized Tasks on the given display,
+ * ordered from front to back.
*/
- // TODO(b/278084491): pass in display id
- fun getFreeformTasksInZOrder(): List<Int> {
- return freeformTasksInZOrder
+ fun getActiveNonMinimizedTasksOrderedFrontToBack(displayId: Int): List<Int> {
+ val activeTasks = getActiveTasks(displayId)
+ val allTasksInZOrder = getFreeformTasksInZOrder(displayId)
+ return activeTasks
+ // Don't show already minimized Tasks
+ .filter { taskId -> !isMinimizedTask(taskId) }
+ .sortedBy { taskId -> allTasksInZOrder.indexOf(taskId) }
}
+ /** Get a list of freeform tasks, ordered from top-bottom (top at index 0). */
+ fun getFreeformTasksInZOrder(displayId: Int): ArrayList<Int> =
+ ArrayList(displayData[displayId]?.freeformTasksInZOrder ?: emptyList())
+
/**
* Updates whether a freeform task with this id is visible or not and notifies listeners.
*
@@ -222,20 +237,32 @@ class DesktopModeTaskRepository {
val otherDisplays = displayData.keyIterator().asSequence().filter { it != displayId }
for (otherDisplayId in otherDisplays) {
if (displayData[otherDisplayId].visibleTasks.remove(taskId)) {
- notifyVisibleTaskListeners(otherDisplayId,
- displayData[otherDisplayId].visibleTasks.size)
+ notifyVisibleTaskListeners(
+ otherDisplayId,
+ displayData[otherDisplayId].visibleTasks.size
+ )
+ }
+ }
+ } else if (displayId == INVALID_DISPLAY) {
+ // Task has vanished. Check which display to remove the task from.
+ displayData.forEach { displayId, data ->
+ if (data.visibleTasks.remove(taskId)) {
+ notifyVisibleTaskListeners(displayId, data.visibleTasks.size)
}
}
+ return
}
val prevCount = getVisibleTaskCount(displayId)
if (visible) {
displayData.getOrCreate(displayId).visibleTasks.add(taskId)
+ unminimizeTask(displayId, taskId)
} else {
displayData[displayId]?.visibleTasks?.remove(taskId)
}
val newCount = getVisibleTaskCount(displayId)
+ // Check if count changed
if (prevCount != newCount) {
KtProtoLog.d(
WM_SHELL_DESKTOP_MODE,
@@ -244,10 +271,6 @@ class DesktopModeTaskRepository {
visible,
displayId
)
- }
-
- // Check if count changed
- if (prevCount != newCount) {
KtProtoLog.d(
WM_SHELL_DESKTOP_MODE,
"DesktopTaskRepo: visibleTaskCount has changed from %d to %d",
@@ -264,9 +287,7 @@ class DesktopModeTaskRepository {
}
}
- /**
- * Get number of tasks that are marked as visible on given [displayId]
- */
+ /** Get number of tasks that are marked as visible on given [displayId] */
fun getVisibleTaskCount(displayId: Int): Int {
KtProtoLog.d(
WM_SHELL_DESKTOP_MODE,
@@ -276,41 +297,62 @@ class DesktopModeTaskRepository {
return displayData[displayId]?.visibleTasks?.size ?: 0
}
- /**
- * Add (or move if it already exists) the task to the top of the ordered list.
- */
- fun addOrMoveFreeformTaskToTop(taskId: Int) {
+ /** Add (or move if it already exists) the task to the top of the ordered list. */
+ // TODO(b/342417921): Identify if there is additional checks needed to move tasks for
+ // multi-display scenarios.
+ fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) {
KtProtoLog.d(
WM_SHELL_DESKTOP_MODE,
- "DesktopTaskRepo: add or move task to top taskId=%d",
+ "DesktopTaskRepo: add or move task to top: display=%d, taskId=%d",
+ displayId,
taskId
)
- if (freeformTasksInZOrder.contains(taskId)) {
- freeformTasksInZOrder.remove(taskId)
- }
- freeformTasksInZOrder.add(0, taskId)
+ displayData[displayId]?.freeformTasksInZOrder?.remove(taskId)
+ displayData.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId)
}
- /**
- * Remove the task from the ordered list.
- */
- fun removeFreeformTask(taskId: Int) {
+ /** Mark a Task as minimized. */
+ fun minimizeTask(displayId: Int, taskId: Int) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopModeTaskRepository: minimize Task: display=%d, task=%d",
+ displayId,
+ taskId
+ )
+ displayData.getOrCreate(displayId).minimizedTasks.add(taskId)
+ }
+
+ /** Mark a Task as non-minimized. */
+ fun unminimizeTask(displayId: Int, taskId: Int) {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopModeTaskRepository: unminimize Task: display=%d, task=%d",
+ displayId,
+ taskId
+ )
+ displayData[displayId]?.minimizedTasks?.remove(taskId)
+ }
+
+ /** Remove the task from the ordered list. */
+ fun removeFreeformTask(displayId: Int, taskId: Int) {
KtProtoLog.d(
WM_SHELL_DESKTOP_MODE,
- "DesktopTaskRepo: remove freeform task from ordered list taskId=%d",
+ "DesktopTaskRepo: remove freeform task from ordered list: display=%d, taskId=%d",
+ displayId,
taskId
)
- freeformTasksInZOrder.remove(taskId)
+ displayData[displayId]?.freeformTasksInZOrder?.remove(taskId)
+ boundsBeforeMaximizeByTaskId.remove(taskId)
KtProtoLog.d(
WM_SHELL_DESKTOP_MODE,
- "DesktopTaskRepo: remaining freeform tasks: " + freeformTasksInZOrder.toDumpString()
+ "DesktopTaskRepo: remaining freeform tasks: %s",
+ displayData[displayId]?.freeformTasksInZOrder?.toDumpString() ?: ""
)
}
/**
* Updates the active desktop gesture exclusion regions; if desktopExclusionRegions has been
- * accepted by desktopGestureExclusionListener, it will be updated in the
- * appropriate classes.
+ * accepted by desktopGestureExclusionListener, it will be updated in the appropriate classes.
*/
fun updateTaskExclusionRegions(taskId: Int, taskExclusionRegions: Region) {
desktopExclusionRegions.put(taskId, taskExclusionRegions)
@@ -320,9 +362,9 @@ class DesktopModeTaskRepository {
}
/**
- * Removes the desktop gesture exclusion region for the specified task; if exclusionRegion
- * has been accepted by desktopGestureExclusionListener, it will be updated in the
- * appropriate classes.
+ * Removes the desktop gesture exclusion region for the specified task; if exclusionRegion has
+ * been accepted by desktopGestureExclusionListener, it will be updated in the appropriate
+ * classes.
*/
fun removeExclusionRegion(taskId: Int) {
desktopExclusionRegions.delete(taskId)
@@ -331,38 +373,20 @@ class DesktopModeTaskRepository {
}
}
- /**
- * Update stashed status on display with id [displayId]
- */
- fun setStashed(displayId: Int, stashed: Boolean) {
- val data = displayData.getOrCreate(displayId)
- val oldValue = data.stashed
- data.stashed = stashed
- if (oldValue != stashed) {
- KtProtoLog.d(
- WM_SHELL_DESKTOP_MODE,
- "DesktopTaskRepo: mark stashed=%b displayId=%d",
- stashed,
- displayId
- )
- visibleTasksListeners.forEach { (listener, executor) ->
- executor.execute { listener.onStashedChanged(displayId, stashed) }
- }
- }
+ /** Removes and returns the bounds saved before maximizing the given task. */
+ fun removeBoundsBeforeMaximize(taskId: Int): Rect? {
+ return boundsBeforeMaximizeByTaskId.removeReturnOld(taskId)
}
- /**
- * Check if display with id [displayId] has desktop tasks stashed
- */
- fun isStashed(displayId: Int): Boolean {
- return displayData[displayId]?.stashed ?: false
+ /** Saves the bounds of the given task before maximizing. */
+ fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) {
+ boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds))
}
internal fun dump(pw: PrintWriter, prefix: String) {
val innerPrefix = "$prefix "
pw.println("${prefix}DesktopModeTaskRepository")
dumpDisplayData(pw, innerPrefix)
- pw.println("${innerPrefix}freeformTasksInZOrder=${freeformTasksInZOrder.toDumpString()}")
pw.println("${innerPrefix}activeTasksListeners=${activeTasksListeners.size}")
pw.println("${innerPrefix}visibleTasksListeners=${visibleTasksListeners.size}")
}
@@ -373,7 +397,9 @@ class DesktopModeTaskRepository {
pw.println("${prefix}Display $displayId:")
pw.println("${innerPrefix}activeTasks=${data.activeTasks.toDumpString()}")
pw.println("${innerPrefix}visibleTasks=${data.visibleTasks.toDumpString()}")
- pw.println("${innerPrefix}stashed=${data.stashed}")
+ pw.println(
+ "${innerPrefix}freeformTasksInZOrder=${data.freeformTasksInZOrder.toDumpString()}"
+ )
}
}
@@ -381,9 +407,7 @@ class DesktopModeTaskRepository {
* Defines interface for classes that can listen to changes for active tasks in desktop mode.
*/
interface ActiveTasksListener {
- /**
- * Called when the active tasks change in desktop mode.
- */
+ /** Called when the active tasks change in desktop mode. */
fun onActiveTasksChanged(displayId: Int) {}
}
@@ -391,15 +415,8 @@ class DesktopModeTaskRepository {
* Defines interface for classes that can listen to changes for visible tasks in desktop mode.
*/
interface VisibleTasksListener {
- /**
- * Called when the desktop changes the number of visible freeform tasks.
- */
+ /** Called when the desktop changes the number of visible freeform tasks. */
fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {}
-
- /**
- * Called when the desktop stashed status changes.
- */
- fun onStashedChanged(displayId: Int, stashed: Boolean) {}
}
}
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/DesktopModeUiEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt
new file mode 100644
index 000000000000..a9d4e5f3216e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.util.Log
+import com.android.internal.logging.InstanceId
+import com.android.internal.logging.InstanceIdSequence
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
+import com.android.wm.shell.dagger.WMSingleton
+import javax.inject.Inject
+
+/** Log Aster UIEvents for desktop windowing mode. */
+@WMSingleton
+class DesktopModeUiEventLogger
+@Inject
+constructor(
+ private val mUiEventLogger: UiEventLogger,
+ private val mInstanceIdSequence: InstanceIdSequence
+) {
+ /**
+ * Logs an event for a CUI, on a particular package.
+ *
+ * @param uid The user id associated with the package the user is interacting with
+ * @param packageName The name of the package the user is interacting with
+ * @param event The event type to generate
+ */
+ fun log(uid: Int, packageName: String, event: DesktopUiEventEnum) {
+ if (packageName.isEmpty() || uid < 0) {
+ Log.d(TAG, "Skip logging since package name is empty or bad uid")
+ return
+ }
+ mUiEventLogger.log(event, uid, packageName)
+ }
+
+ /** Retrieves a new instance id for a new interaction. */
+ fun getNewInstanceId(): InstanceId = mInstanceIdSequence.newInstanceId()
+
+ /**
+ * Logs an event as part of a particular CUI, on a particular package.
+ *
+ * @param instanceId The id identifying an interaction, potentially taking place across multiple
+ * surfaces. There should be a new id generated for each distinct CUI.
+ * @param uid The user id associated with the package the user is interacting with
+ * @param packageName The name of the package the user is interacting with
+ * @param event The event type to generate
+ */
+ fun logWithInstanceId(
+ instanceId: InstanceId,
+ uid: Int,
+ packageName: String,
+ event: DesktopUiEventEnum
+ ) {
+ if (packageName.isEmpty() || uid < 0) {
+ Log.d(TAG, "Skip logging since package name is empty or bad uid")
+ return
+ }
+ mUiEventLogger.logWithInstanceId(event, uid, packageName, instanceId)
+ }
+
+ companion object {
+ /** Enums for logging desktop windowing mode UiEvents. */
+ enum class DesktopUiEventEnum(private val mId: Int) : UiEventLogger.UiEventEnum {
+
+ @UiEvent(doc = "Resize the window in desktop windowing mode by dragging the edge")
+ DESKTOP_WINDOW_EDGE_DRAG_RESIZE(1721),
+ @UiEvent(doc = "Resize the window in desktop windowing mode by dragging the corner")
+ DESKTOP_WINDOW_CORNER_DRAG_RESIZE(1722),
+ @UiEvent(doc = "Tap on the window header maximize button in desktop windowing mode")
+ DESKTOP_WINDOW_MAXIMIZE_BUTTON_TAP(1723),
+ @UiEvent(doc = "Double tap on window header to maximize it in desktop windowing mode")
+ DESKTOP_WINDOW_HEADER_DOUBLE_TAP_TO_MAXIMIZE(1724);
+
+ override fun getId(): Int = mId
+ }
+
+ private const val TAG = "DesktopModeUiEventLogger"
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
new file mode 100644
index 000000000000..217b1d356122
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt
@@ -0,0 +1,173 @@
+/*
+ * 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.
+ */
+
+@file:JvmName("DesktopModeUtils")
+
+package com.android.wm.shell.desktopmode
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.pm.ActivityInfo.isFixedOrientationLandscape
+import android.content.pm.ActivityInfo.isFixedOrientationPortrait
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.content.res.Configuration.ORIENTATION_PORTRAIT
+import android.graphics.Rect
+import android.os.SystemProperties
+import android.util.Size
+import com.android.wm.shell.common.DisplayLayout
+
+val DESKTOP_MODE_INITIAL_BOUNDS_SCALE: Float =
+ SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
+
+val DESKTOP_MODE_LANDSCAPE_APP_PADDING: Int =
+ SystemProperties.getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25)
+
+/**
+ * Calculates the initial bounds required for an application to fill a scale of the display bounds
+ * without any letterboxing. This is done by taking into account the applications fullscreen size,
+ * aspect ratio, orientation and resizability to calculate an area this is compatible with the
+ * applications previous configuration.
+ */
+fun calculateInitialBounds(
+ displayLayout: DisplayLayout,
+ taskInfo: RunningTaskInfo,
+ scale: Float = DESKTOP_MODE_INITIAL_BOUNDS_SCALE
+): Rect {
+ val screenBounds = Rect(0, 0, displayLayout.width(), displayLayout.height())
+ val appAspectRatio = calculateAspectRatio(taskInfo)
+ val idealSize = calculateIdealSize(screenBounds, scale)
+ // If no top activity exists, apps fullscreen bounds and aspect ratio cannot be calculated.
+ // Instead default to the desired initial bounds.
+ val topActivityInfo =
+ taskInfo.topActivityInfo ?: return positionInScreen(idealSize, screenBounds)
+
+ val initialSize: Size =
+ when (taskInfo.configuration.orientation) {
+ ORIENTATION_LANDSCAPE -> {
+ if (taskInfo.isResizeable) {
+ if (isFixedOrientationPortrait(topActivityInfo.screenOrientation)) {
+ // Respect apps fullscreen width
+ Size(taskInfo.appCompatTaskInfo.topActivityLetterboxWidth, idealSize.height)
+ } else {
+ idealSize
+ }
+ } else {
+ maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio)
+ }
+ }
+ ORIENTATION_PORTRAIT -> {
+ val customPortraitWidthForLandscapeApp =
+ screenBounds.width() - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2)
+ if (taskInfo.isResizeable) {
+ if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
+ // Respect apps fullscreen height and apply custom app width
+ Size(
+ customPortraitWidthForLandscapeApp,
+ taskInfo.appCompatTaskInfo.topActivityLetterboxHeight
+ )
+ } else {
+ idealSize
+ }
+ } else {
+ if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
+ // Apply custom app width and calculate maximum size
+ maximumSizeMaintainingAspectRatio(
+ taskInfo,
+ Size(customPortraitWidthForLandscapeApp, idealSize.height),
+ appAspectRatio
+ )
+ } else {
+ maximumSizeMaintainingAspectRatio(taskInfo, idealSize, appAspectRatio)
+ }
+ }
+ }
+ else -> {
+ idealSize
+ }
+ }
+
+ return positionInScreen(initialSize, screenBounds)
+}
+
+/**
+ * Calculates the largest size that can fit in a given area while maintaining a specific aspect
+ * ratio.
+ */
+private fun maximumSizeMaintainingAspectRatio(
+ taskInfo: RunningTaskInfo,
+ targetArea: Size,
+ aspectRatio: Float
+): Size {
+ val targetHeight = targetArea.height
+ val targetWidth = targetArea.width
+ val finalHeight: Int
+ val finalWidth: Int
+ if (isFixedOrientationPortrait(taskInfo.topActivityInfo!!.screenOrientation)) {
+ val tempWidth = (targetHeight / aspectRatio).toInt()
+ if (tempWidth <= targetWidth) {
+ finalHeight = targetHeight
+ finalWidth = tempWidth
+ } else {
+ finalWidth = targetWidth
+ finalHeight = (finalWidth * aspectRatio).toInt()
+ }
+ } else {
+ val tempWidth = (targetHeight * aspectRatio).toInt()
+ if (tempWidth <= targetWidth) {
+ finalHeight = targetHeight
+ finalWidth = tempWidth
+ } else {
+ finalWidth = targetWidth
+ finalHeight = (finalWidth / aspectRatio).toInt()
+ }
+ }
+ return Size(finalWidth, finalHeight)
+}
+
+/** Calculates the aspect ratio of an activity from its fullscreen bounds. */
+private fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float {
+ if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) {
+ val appLetterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxWidth
+ val appLetterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxHeight
+ return maxOf(appLetterboxWidth, appLetterboxHeight) /
+ minOf(appLetterboxWidth, appLetterboxHeight).toFloat()
+ }
+ val appBounds = taskInfo.configuration.windowConfiguration.appBounds ?: return 1f
+ return maxOf(appBounds.height(), appBounds.width()) /
+ minOf(appBounds.height(), appBounds.width()).toFloat()
+}
+
+/**
+ * Calculates the desired initial bounds for applications in desktop windowing. This is done as a
+ * scale of the screen bounds.
+ */
+private fun calculateIdealSize(screenBounds: Rect, scale: Float): Size {
+ val width = (screenBounds.width() * scale).toInt()
+ val height = (screenBounds.height() * scale).toInt()
+ return Size(width, height)
+}
+
+/** Adjusts bounds to be positioned in the middle of the screen. */
+private fun positionInScreen(desiredSize: Size, screenBounds: Rect): Rect {
+ // TODO(b/325240051): Position apps with bottom heavy offset
+ val heightOffset = (screenBounds.height() - desiredSize.height) / 2
+ val widthOffset = (screenBounds.width() - desiredSize.width) / 2
+ return Rect(
+ widthOffset,
+ heightOffset,
+ desiredSize.width + widthOffset,
+ desiredSize.height + heightOffset
+ )
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
index 7091c4b7210a..ed0d2b87b03f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java
@@ -17,10 +17,10 @@
package com.android.wm.shell.desktopmode;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -35,6 +35,7 @@ import android.graphics.PixelFormat;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.Region;
+import android.graphics.drawable.LayerDrawable;
import android.util.DisplayMetrics;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
@@ -98,6 +99,7 @@ public class DesktopModeVisualIndicator {
* Based on the coordinates of the current drag event, determine which indicator type we should
* display, including no visible indicator.
*/
+ @NonNull
IndicatorType updateIndicatorType(PointF inputCoordinates, int windowingMode) {
final DisplayLayout layout = mDisplayController.getDisplayLayout(mTaskInfo.displayId);
// If we are in freeform, we don't want a visible indicator in the "freeform" drag zone.
@@ -136,18 +138,18 @@ public class DesktopModeVisualIndicator {
Region calculateFullscreenRegion(DisplayLayout layout,
@WindowConfiguration.WindowingMode int windowingMode, int captionHeight) {
final Region region = new Region();
- int edgeTransitionHeight = mContext.getResources().getDimensionPixelSize(
- com.android.wm.shell.R.dimen.desktop_mode_transition_area_height);
+ int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM
+ ? mContext.getResources().getDimensionPixelSize(
+ com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height)
+ : 2 * layout.stableInsets().top;
// A thin, short Rect at the top of the screen.
if (windowingMode == WINDOWING_MODE_FREEFORM) {
int fromFreeformWidth = mContext.getResources().getDimensionPixelSize(
com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_width);
- int fromFreeformHeight = mContext.getResources().getDimensionPixelSize(
- com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height);
region.union(new Rect((layout.width() / 2) - (fromFreeformWidth / 2),
-captionHeight,
(layout.width() / 2) + (fromFreeformWidth / 2),
- fromFreeformHeight));
+ transitionHeight));
}
// A screen-wide, shorter Rect if the task is in fullscreen or split.
if (windowingMode == WINDOWING_MODE_FULLSCREEN
@@ -155,7 +157,7 @@ public class DesktopModeVisualIndicator {
region.union(new Rect(0,
-captionHeight,
layout.width(),
- edgeTransitionHeight));
+ transitionHeight));
}
return region;
}
@@ -184,7 +186,7 @@ public class DesktopModeVisualIndicator {
// In freeform, keep the top corners clear.
int transitionHeight = windowingMode == WINDOWING_MODE_FREEFORM
? mContext.getResources().getDimensionPixelSize(
- com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) :
+ com.android.wm.shell.R.dimen.desktop_mode_split_from_desktop_height) :
-captionHeight;
region.union(new Rect(0, transitionHeight, transitionEdgeWidth, layout.height()));
return region;
@@ -221,6 +223,7 @@ public class DesktopModeVisualIndicator {
mLeash = builder
.setName("Desktop Mode Visual Indicator")
.setContainerLayer()
+ .setCallsite("DesktopModeVisualIndicator.createView")
.build();
t.show(mLeash);
final WindowManager.LayoutParams lp =
@@ -310,12 +313,14 @@ public class DesktopModeVisualIndicator {
private static class VisualIndicatorAnimator extends ValueAnimator {
private static final int FULLSCREEN_INDICATOR_DURATION = 200;
private static final float FULLSCREEN_SCALE_ADJUSTMENT_PERCENT = 0.015f;
- private static final float INDICATOR_FINAL_OPACITY = 0.7f;
+ private static final float INDICATOR_FINAL_OPACITY = 0.35f;
+ private static final int MAXIMUM_OPACITY = 255;
- /** Determines how this animator will interact with the view's alpha:
- * Fade in, fade out, or no change to alpha
+ /**
+ * Determines how this animator will interact with the view's alpha:
+ * Fade in, fade out, or no change to alpha
*/
- private enum AlphaAnimType{
+ private enum AlphaAnimType {
ALPHA_FADE_IN_ANIM, ALPHA_FADE_OUT_ANIM, ALPHA_NO_CHANGE_ANIM
}
@@ -362,10 +367,10 @@ public class DesktopModeVisualIndicator {
* Create animator for visual indicator changing type (i.e., fullscreen to freeform,
* freeform to split, etc.)
*
- * @param view the view for this indicator
+ * @param view the view for this indicator
* @param displayLayout information about the display the transitioning task is currently on
- * @param origType the original indicator type
- * @param newType the new indicator type
+ * @param origType the original indicator type
+ * @param newType the new indicator type
*/
private static VisualIndicatorAnimator animateIndicatorType(@NonNull View view,
@NonNull DisplayLayout displayLayout, IndicatorType origType,
@@ -454,7 +459,11 @@ public class DesktopModeVisualIndicator {
* @param fraction current animation fraction
*/
private void updateIndicatorAlpha(float fraction, View view) {
- view.setAlpha(fraction * INDICATOR_FINAL_OPACITY);
+ final LayerDrawable drawable = (LayerDrawable) view.getBackground();
+ drawable.findDrawableByLayerId(R.id.indicator_stroke)
+ .setAlpha((int) (MAXIMUM_OPACITY * fraction));
+ drawable.findDrawableByLayerId(R.id.indicator_solid)
+ .setAlpha((int) (MAXIMUM_OPACITY * fraction * INDICATOR_FINAL_OPACITY));
}
/**
@@ -462,7 +471,7 @@ public class DesktopModeVisualIndicator {
*/
private static Rect getMaxBounds(Rect startBounds) {
return new Rect((int) (startBounds.left
- - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.width())),
+ - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.width())),
(int) (startBounds.top
- (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * startBounds.height())),
(int) (startBounds.right
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 d1328cabdc99..c5111d68881d 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
@@ -19,6 +19,7 @@ package com.android.wm.shell.desktopmode
import android.app.ActivityManager.RunningTaskInfo
import android.app.ActivityOptions
import android.app.PendingIntent
+import android.app.TaskInfo
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -39,18 +40,20 @@ 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
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.annotation.BinderThread
+import com.android.internal.annotations.VisibleForTesting
import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.window.flags.Flags
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
@@ -59,15 +62,22 @@ 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.annotations.ExternalThread
-import com.android.wm.shell.common.annotations.ShellMainThread
+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
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener
import com.android.wm.shell.draganddrop.DragAndDropController
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.recents.RecentTasksController
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.useDesktopOverrideDensity
+import com.android.wm.shell.shared.annotations.ExternalThread
+import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DESKTOP_MODE
import com.android.wm.shell.sysui.ShellCommandHandler
@@ -77,83 +87,99 @@ import com.android.wm.shell.sysui.ShellSharedConstants
import com.android.wm.shell.transition.OneShotRemoteHandler
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.util.KtProtoLog
+import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
+import com.android.wm.shell.windowdecor.extension.isFullscreen
import java.io.PrintWriter
+import java.util.Optional
import java.util.concurrent.Executor
import java.util.function.Consumer
/** Handles moving tasks in and out of desktop */
class DesktopTasksController(
- private val context: Context,
- shellInit: ShellInit,
- private val shellCommandHandler: ShellCommandHandler,
- private val shellController: ShellController,
- private val displayController: DisplayController,
- private val shellTaskOrganizer: ShellTaskOrganizer,
- private val syncQueue: SyncTransactionQueue,
- private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
- private val dragAndDropController: DragAndDropController,
- private val transitions: Transitions,
- private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler,
- private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler,
- private val toggleResizeDesktopTaskTransitionHandler:
- ToggleResizeDesktopTaskTransitionHandler,
- private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
- private val desktopModeTaskRepository: DesktopModeTaskRepository,
- private val launchAdjacentController: LaunchAdjacentController,
- private val recentsTransitionHandler: RecentsTransitionHandler,
- private val multiInstanceHelper: MultiInstanceHelper,
- @ShellMainThread private val mainExecutor: ShellExecutor
-) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler,
+ private val context: Context,
+ shellInit: ShellInit,
+ private val shellCommandHandler: ShellCommandHandler,
+ private val shellController: ShellController,
+ private val displayController: DisplayController,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
+ private val syncQueue: SyncTransactionQueue,
+ private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+ private val dragAndDropController: DragAndDropController,
+ private val transitions: Transitions,
+ private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler,
+ private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler,
+ private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
+ private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
+ private val desktopModeTaskRepository: DesktopModeTaskRepository,
+ private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver,
+ private val launchAdjacentController: LaunchAdjacentController,
+ private val recentsTransitionHandler: RecentsTransitionHandler,
+ private val multiInstanceHelper: MultiInstanceHelper,
+ @ShellMainThread private val mainExecutor: ShellExecutor,
+ private val desktopTasksLimiter: Optional<DesktopTasksLimiter>,
+ private val recentTasksController: RecentTasksController?
+) :
+ RemoteCallable<DesktopTasksController>,
+ Transitions.TransitionHandler,
DragAndDropController.DragAndDropListener {
private val desktopMode: DesktopModeImpl
private var visualIndicator: DesktopModeVisualIndicator? = null
private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler =
DesktopModeShellCommandHandler(this)
-
- private val mOnAnimationFinishedCallback = Consumer<SurfaceControl.Transaction> {
- t: SurfaceControl.Transaction ->
- visualIndicator?.releaseVisualIndicator(t)
- visualIndicator = null
- }
- private val taskVisibilityListener = object : VisibleTasksListener {
- override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
- launchAdjacentController.launchAdjacentEnabled = visibleTasksCount == 0
+ private val mOnAnimationFinishedCallback =
+ Consumer<SurfaceControl.Transaction> { t: SurfaceControl.Transaction ->
+ visualIndicator?.releaseVisualIndicator(t)
+ visualIndicator = null
}
- }
- private val dragToDesktopStateListener = object : DragToDesktopStateListener {
- override fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) {
- removeVisualIndicator(tx)
+ private val taskVisibilityListener =
+ object : VisibleTasksListener {
+ override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
+ launchAdjacentController.launchAdjacentEnabled = visibleTasksCount == 0
+ }
}
+ private val dragToDesktopStateListener =
+ object : DragToDesktopStateListener {
+ override fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) {
+ removeVisualIndicator(tx)
+ }
- override fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction) {
- removeVisualIndicator(tx)
- }
+ override fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction) {
+ removeVisualIndicator(tx)
+ }
- private fun removeVisualIndicator(tx: SurfaceControl.Transaction) {
- visualIndicator?.releaseVisualIndicator(tx)
- visualIndicator = null
+ private fun removeVisualIndicator(tx: SurfaceControl.Transaction) {
+ visualIndicator?.releaseVisualIndicator(tx)
+ visualIndicator = null
+ }
}
- }
+ private val sysUIPackageName = context.resources.getString(
+ com.android.internal.R.string.config_systemUi)
private val transitionAreaHeight
- get() = context.resources.getDimensionPixelSize(
- com.android.wm.shell.R.dimen.desktop_mode_transition_area_height
- )
+ get() =
+ context.resources.getDimensionPixelSize(
+ com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height
+ )
private val transitionAreaWidth
- get() = context.resources.getDimensionPixelSize(
- com.android.wm.shell.R.dimen.desktop_mode_transition_area_width
- )
+ get() =
+ context.resources.getDimensionPixelSize(
+ com.android.wm.shell.R.dimen.desktop_mode_transition_area_width
+ )
+
+ /** Task id of the task currently being dragged from fullscreen/split. */
+ val draggingTaskId
+ get() = dragToDesktopTransitionHandler.draggingTaskId
private var recentsAnimationRunning = false
private lateinit var splitScreenController: SplitScreenController
init {
desktopMode = DesktopModeImpl()
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(context)) {
shellInit.addInitCallback({ onInit() }, this)
}
}
@@ -161,8 +187,7 @@ class DesktopTasksController(
private fun onInit() {
KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController")
shellCommandHandler.addDumpCallback(this::dump, this)
- shellCommandHandler.addCommandCallback("desktopmode", desktopModeShellCommandHandler,
- this)
+ shellCommandHandler.addCommandCallback("desktopmode", desktopModeShellCommandHandler, this)
shellController.addExternalInterface(
ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE,
{ createExternalInterface() },
@@ -186,6 +211,16 @@ class DesktopTasksController(
dragAndDropController.addListener(this)
}
+ @VisibleForTesting
+ fun getVisualIndicator(): DesktopModeVisualIndicator? {
+ 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)
@@ -205,11 +240,12 @@ class DesktopTasksController(
bringDesktopAppsToFront(displayId, wct)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- // TODO(b/255649902): ensure remote transition is supplied once state is introduced
+ // TODO(b/309014605): ensure remote transition is supplied once state is introduced
val transitionType = if (remoteTransition == null) TRANSIT_NONE else TRANSIT_TO_FRONT
- val handler = remoteTransition?.let {
- OneShotRemoteHandler(transitions.mainExecutor, remoteTransition)
- }
+ val handler =
+ remoteTransition?.let {
+ OneShotRemoteHandler(transitions.mainExecutor, remoteTransition)
+ }
transitions.startTransition(transitionType, wct, handler).also { t ->
handler?.setTransition(t)
}
@@ -218,47 +254,19 @@ class DesktopTasksController(
}
}
- /**
- * Stash desktop tasks on display with id [displayId].
- *
- * When desktop tasks are stashed, launcher home screen icons are fully visible. New apps
- * launched in this state will be added to the desktop. Existing desktop tasks will be brought
- * back to front during the launch.
- */
- fun stashDesktopApps(displayId: Int) {
- if (DesktopModeStatus.isStashingEnabled()) {
- KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: stashDesktopApps")
- desktopModeTaskRepository.setStashed(displayId, true)
- }
- }
-
- /**
- * Clear the stashed state for the given display
- */
- fun hideStashedDesktopApps(displayId: Int) {
- if (DesktopModeStatus.isStashingEnabled()) {
- KtProtoLog.v(
- WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: hideStashedApps displayId=%d",
- displayId
- )
- desktopModeTaskRepository.setStashed(displayId, false)
- }
- }
-
/** Get number of tasks that are marked as visible */
fun getVisibleTaskCount(displayId: Int): Int {
return desktopModeTaskRepository.getVisibleTaskCount(displayId)
}
/** Enter desktop by using the focused task in given `displayId` */
- fun enterDesktop(displayId: Int) {
+ fun moveFocusedTaskToDesktop(displayId: Int, transitionSource: DesktopModeTransitionSource) {
val allFocusedTasks =
shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo ->
taskInfo.isFocused &&
- (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN ||
- taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) &&
- taskInfo.activityType != ACTIVITY_TYPE_HOME
+ (taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN ||
+ taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) &&
+ taskInfo.activityType != ACTIVITY_TYPE_HOME
}
if (allFocusedTasks.isNotEmpty()) {
when (allFocusedTasks.size) {
@@ -266,20 +274,22 @@ class DesktopTasksController(
// Split-screen case where there are two focused tasks, then we find the child
// task to move to desktop.
val splitFocusedTask =
- if (allFocusedTasks[0].taskId == allFocusedTasks[1].parentTaskId)
+ if (allFocusedTasks[0].taskId == allFocusedTasks[1].parentTaskId) {
allFocusedTasks[1]
- else allFocusedTasks[0]
- moveToDesktop(splitFocusedTask)
+ } else {
+ allFocusedTasks[0]
+ }
+ 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(
WM_SHELL_DESKTOP_MODE,
"DesktopTasksController: Cannot enter desktop, expected less " +
- "than 3 focused tasks but found %d",
+ "than 3 focused tasks but found %d",
allFocusedTasks.size
)
}
@@ -289,22 +299,71 @@ class DesktopTasksController(
/** Move a task with given `taskId` to desktop */
fun moveToDesktop(
- taskId: Int,
- wct: WindowContainerTransaction = WindowContainerTransaction()
+ taskId: Int,
+ wct: WindowContainerTransaction = WindowContainerTransaction(),
+ transitionSource: DesktopModeTransitionSource,
): Boolean {
shellTaskOrganizer.getRunningTaskInfo(taskId)?.let {
- task -> moveToDesktop(task, wct)
- } ?: return false
+ moveToDesktop(it, wct, transitionSource)
+ }
+ ?: moveToDesktopFromNonRunningTask(taskId, wct, transitionSource)
return true
}
- /**
- * Move a task to desktop
- */
+ private fun moveToDesktopFromNonRunningTask(
+ taskId: Int,
+ wct: WindowContainerTransaction,
+ transitionSource: DesktopModeTransitionSource,
+ ): Boolean {
+ recentTasksController?.findTaskInBackground(taskId)?.let {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: moveToDesktopFromNonRunningTask taskId=%d",
+ taskId
+ )
+ // TODO(342378842): Instead of using default display, support multiple displays
+ val taskToMinimize =
+ bringDesktopAppsToFrontBeforeShowingNewTask(DEFAULT_DISPLAY, wct, taskId)
+ addMoveToDesktopChangesNonRunningTask(wct, taskId)
+ // TODO(343149901): Add DPI changes for task launch
+ val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
+ addPendingMinimizeTransition(transition, taskToMinimize)
+ return true
+ }
+ ?: return false
+ }
+
+ private fun addMoveToDesktopChangesNonRunningTask(
+ wct: WindowContainerTransaction,
+ taskId: Int
+ ) {
+ val options = ActivityOptions.makeBasic()
+ options.launchWindowingMode = WINDOWING_MODE_FREEFORM
+ wct.startTask(taskId, options.toBundle())
+ }
+
+ /** Move a task to desktop */
fun moveToDesktop(
- task: RunningTaskInfo,
- wct: WindowContainerTransaction = WindowContainerTransaction()
+ task: RunningTaskInfo,
+ wct: WindowContainerTransaction = WindowContainerTransaction(),
+ transitionSource: DesktopModeTransitionSource,
) {
+ if (Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task)) {
+ KtProtoLog.w(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: Cannot enter desktop, " +
+ "translucent top activity found. This is likely a modal dialog."
+ )
+ 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",
@@ -312,32 +371,34 @@ class DesktopTasksController(
)
exitSplitIfApplicable(wct, task)
// Bring other apps to front first
- bringDesktopAppsToFront(task.displayId, wct)
+ val taskToMinimize =
+ bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
addMoveToDesktopChanges(wct, task)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- enterDesktopTaskTransitionHandler.moveToDesktop(wct)
+ val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
+ addPendingMinimizeTransition(transition, taskToMinimize)
} else {
shellTaskOrganizer.applyTransaction(wct)
}
}
/**
- * The first part of the animated drag to desktop transition. This is
- * followed with a call to [finalizeDragToDesktop] or [cancelDragToDesktop].
+ * The first part of the animated drag to desktop transition. This is followed with a call to
+ * [finalizeDragToDesktop] or [cancelDragToDesktop].
*/
fun startDragToDesktop(
- taskInfo: RunningTaskInfo,
- dragToDesktopValueAnimator: MoveToDesktopAnimator,
+ taskInfo: RunningTaskInfo,
+ dragToDesktopValueAnimator: MoveToDesktopAnimator,
) {
KtProtoLog.v(
- WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: startDragToDesktop taskId=%d",
- taskInfo.taskId
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: startDragToDesktop taskId=%d",
+ taskInfo.taskId
)
dragToDesktopTransitionHandler.startDragToDesktopTransition(
- taskInfo.taskId,
- dragToDesktopValueAnimator
+ taskInfo.taskId,
+ dragToDesktopValueAnimator
)
}
@@ -347,48 +408,59 @@ class DesktopTasksController(
*/
private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) {
KtProtoLog.v(
- WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: finalizeDragToDesktop taskId=%d",
- taskInfo.taskId
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: finalizeDragToDesktop taskId=%d",
+ taskInfo.taskId
)
val wct = WindowContainerTransaction()
exitSplitIfApplicable(wct, taskInfo)
moveHomeTaskToFront(wct)
- bringDesktopAppsToFront(taskInfo.displayId, wct)
+ val taskToMinimize =
+ bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
addMoveToDesktopChanges(wct, taskInfo)
wct.setBounds(taskInfo.token, freeformBounds)
- dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
+ val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
+ transition?.let { addPendingMinimizeTransition(it, taskToMinimize) }
}
/**
- * Perform needed cleanup transaction once animation is complete. Bounds need to be set
- * here instead of initial wct to both avoid flicker and to have task bounds to use for
- * the staging animation.
+ * Perform needed cleanup transaction once animation is complete. Bounds need to be set here
+ * instead of initial wct to both avoid flicker and to have task bounds to use for the staging
+ * animation.
*
* @param taskInfo task entering split that requires a bounds update
*/
fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) {
val wct = WindowContainerTransaction()
wct.setBounds(taskInfo.token, Rect())
+ wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED)
shellTaskOrganizer.applyTransaction(wct)
}
+ /**
+ * Perform clean up of the desktop wallpaper activity if the closed window task is the last
+ * active task.
+ *
+ * @param wct transaction to modify if the last active task is closed
+ * @param taskId task id of the window that's being closed
+ */
+ fun onDesktopWindowClose(wct: WindowContainerTransaction, taskId: Int) {
+ if (desktopModeTaskRepository.isOnlyActiveTask(taskId)) {
+ removeWallpaperActivity(wct)
+ }
+ }
+
/** 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) {
- if (DesktopModeStatus.isEnabled()) {
- shellTaskOrganizer
- .getRunningTasks(displayId)
- .find { taskInfo ->
- taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
- }
- ?.let { moveToFullscreenWithAnimation(it, it.positionInParent) }
+ fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) {
+ getFocusedFreeformTask(displayId)?.let {
+ moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource)
}
}
@@ -401,7 +473,9 @@ class DesktopTasksController(
)
val wct = WindowContainerTransaction()
wct.setBounds(task.token, Rect())
- addMoveToSplitChanges(wct, task)
+ // Rather than set windowing mode to multi-window at task level, set it to
+ // undefined and inherit from split stage.
+ wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
} else {
@@ -412,10 +486,11 @@ class DesktopTasksController(
private fun exitSplitIfApplicable(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) {
if (splitScreenController.isTaskInSplitScreen(taskInfo.taskId)) {
splitScreenController.prepareExitSplitScreen(
- wct,
- splitScreenController.getStageOfTask(taskInfo.taskId),
- EXIT_REASON_DESKTOP_MODE
+ wct,
+ splitScreenController.getStageOfTask(taskInfo.taskId),
+ EXIT_REASON_DESKTOP_MODE
)
+ splitScreenController.transitionHandler?.onSplitToDesktop()
}
}
@@ -429,21 +504,31 @@ 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",
- task.taskId
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: moveToFullscreen with animation taskId=%d",
+ task.taskId
)
val wct = WindowContainerTransaction()
addMoveToFullscreenChanges(wct, task)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
exitDesktopTaskTransitionHandler.startTransition(
- Transitions.TRANSIT_EXIT_DESKTOP_MODE, wct, position, mOnAnimationFinishedCallback)
+ transitionSource,
+ wct,
+ position,
+ mOnAnimationFinishedCallback
+ )
} else {
shellTaskOrganizer.applyTransaction(wct)
releaseVisualIndicator()
@@ -465,8 +550,10 @@ class DesktopTasksController(
val wct = WindowContainerTransaction()
wct.reorder(taskInfo.token, true)
+ val taskToMinimize = addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo)
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
+ val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */)
+ addPendingMinimizeTransition(transition, taskToMinimize)
} else {
shellTaskOrganizer.applyTransaction(wct)
}
@@ -476,12 +563,12 @@ class DesktopTasksController(
* Move task to the next display.
*
* Queries all current known display ids and sorts them in ascending order. Then iterates
- * through the list and looks for the display id that is larger than the display id for
- * the passed in task. If a display with a higher id is not found, iterates through the list and
+ * through the list and looks for the display id that is larger than the display id for the
+ * passed in task. If a display with a higher id is not found, iterates through the list and
* finds the first display id that is not the display id for the passed in task.
*
- * If a display matching the above criteria is found, re-parents the task to that display.
- * No-op if no such display is found.
+ * If a display matching the above criteria is found, re-parents the task to that display. No-op
+ * if no such display is found.
*/
fun moveToNextDisplay(taskId: Int) {
val task = shellTaskOrganizer.getRunningTaskInfo(taskId)
@@ -489,8 +576,12 @@ class DesktopTasksController(
KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d not found", taskId)
return
}
- KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d taskDisplayId=%d",
- taskId, task.displayId)
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "moveToNextDisplay: taskId=%d taskDisplayId=%d",
+ taskId,
+ task.displayId
+ )
val displayIds = rootTaskDisplayAreaOrganizer.displayIds.sorted()
// Get the first display id that is higher than current task display id
@@ -512,8 +603,12 @@ class DesktopTasksController(
* No-op if task is already on that display per [RunningTaskInfo.displayId].
*/
private fun moveToDisplay(task: RunningTaskInfo, displayId: Int) {
- KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToDisplay: taskId=%d displayId=%d",
- task.taskId, displayId)
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "moveToDisplay: taskId=%d displayId=%d",
+ task.taskId,
+ displayId
+ )
if (task.displayId == displayId) {
KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "moveToDisplay: task already on display")
@@ -535,7 +630,11 @@ class DesktopTasksController(
}
}
- /** Quick-resizes a desktop task, toggling between the stable bounds and the default bounds. */
+ /**
+ * Quick-resizes a desktop task, toggling between a fullscreen state (represented by the stable
+ * bounds) and a free floating state (either the last saved bounds if available or the default
+ * bounds otherwise).
+ */
fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) {
val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
@@ -543,11 +642,25 @@ class DesktopTasksController(
displayLayout.getStableBounds(stableBounds)
val destinationBounds = Rect()
if (taskInfo.configuration.windowConfiguration.bounds == stableBounds) {
- // The desktop task is currently occupying the whole stable bounds, toggle to the
- // default bounds.
- getDefaultDesktopTaskBounds(displayLayout, destinationBounds)
+ // The desktop task is currently occupying the whole stable bounds. If the bounds
+ // before the task was toggled to stable bounds were saved, toggle the task to those
+ // bounds. Otherwise, toggle to the default bounds.
+ val taskBoundsBeforeMaximize =
+ desktopModeTaskRepository.removeBoundsBeforeMaximize(taskInfo.taskId)
+ if (taskBoundsBeforeMaximize != null) {
+ destinationBounds.set(taskBoundsBeforeMaximize)
+ } else {
+ if (Flags.enableWindowingDynamicInitialBounds()) {
+ destinationBounds.set(calculateInitialBounds(displayLayout, taskInfo))
+ } else {
+ destinationBounds.set(getDefaultDesktopTaskBounds(displayLayout))
+ }
+ }
} else {
- // Toggle to the stable bounds.
+ // Save current bounds so that task can be restored back to original bounds if necessary
+ // and toggle to the stable bounds.
+ val taskBounds = taskInfo.configuration.windowConfiguration.bounds
+ desktopModeTaskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, taskBounds)
destinationBounds.set(stableBounds)
}
@@ -565,52 +678,57 @@ class DesktopTasksController(
* @param position the portion of the screen (RIGHT or LEFT) we want to snap the task to.
*/
fun snapToHalfScreen(taskInfo: RunningTaskInfo, position: SnapPosition) {
- val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+ val destinationBounds = getSnapBounds(taskInfo, position)
+
+ if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) return
+
+ val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ toggleResizeDesktopTaskTransitionHandler.startTransition(wct)
+ } else {
+ shellTaskOrganizer.applyTransaction(wct)
+ }
+ }
+
+ private fun getDefaultDesktopTaskBounds(displayLayout: DisplayLayout): Rect {
+ // TODO(b/319819547): Account for app constraints so apps do not become letterboxed
+ val desiredWidth = (displayLayout.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt()
+ val desiredHeight = (displayLayout.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt()
+ val heightOffset = (displayLayout.height() - desiredHeight) / 2
+ val widthOffset = (displayLayout.width() - desiredWidth) / 2
+ return Rect(
+ widthOffset,
+ heightOffset,
+ desiredWidth + widthOffset,
+ desiredHeight + heightOffset
+ )
+ }
+
+ private fun getSnapBounds(taskInfo: RunningTaskInfo, position: SnapPosition): Rect {
+ val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return Rect()
val stableBounds = Rect()
displayLayout.getStableBounds(stableBounds)
val destinationWidth = stableBounds.width() / 2
- val destinationBounds = when (position) {
+ return when (position) {
SnapPosition.LEFT -> {
Rect(
- stableBounds.left,
- stableBounds.top,
- stableBounds.left + destinationWidth,
- stableBounds.bottom
+ stableBounds.left,
+ stableBounds.top,
+ stableBounds.left + destinationWidth,
+ stableBounds.bottom
)
}
SnapPosition.RIGHT -> {
Rect(
- stableBounds.right - destinationWidth,
- stableBounds.top,
- stableBounds.right,
- stableBounds.bottom
+ stableBounds.right - destinationWidth,
+ stableBounds.top,
+ stableBounds.right,
+ stableBounds.bottom
)
}
}
-
- if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) return
-
- val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- toggleResizeDesktopTaskTransitionHandler.startTransition(wct)
- } else {
- shellTaskOrganizer.applyTransaction(wct)
- }
- }
-
- private fun getDefaultDesktopTaskBounds(displayLayout: DisplayLayout, outBounds: Rect) {
- // TODO(b/319819547): Account for app constraints so apps do not become letterboxed
- val screenBounds = Rect(0, 0, displayLayout.width(), displayLayout.height())
- // Update width and height with default desktop mode values
- val desiredWidth = screenBounds.width().times(DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt()
- val desiredHeight = screenBounds.height().times(DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt()
- outBounds.set(0, 0, desiredWidth, desiredHeight)
- // Center the task in screen bounds
- outBounds.offset(
- screenBounds.centerX() - outBounds.centerX(),
- screenBounds.centerY() - outBounds.centerY())
}
/**
@@ -624,19 +742,52 @@ class DesktopTasksController(
?: WINDOWING_MODE_UNDEFINED
}
- private fun bringDesktopAppsToFront(displayId: Int, wct: WindowContainerTransaction) {
- KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: bringDesktopAppsToFront")
- val activeTasks = desktopModeTaskRepository.getActiveTasks(displayId)
+ private fun bringDesktopAppsToFrontBeforeShowingNewTask(
+ displayId: Int,
+ wct: WindowContainerTransaction,
+ newTaskIdInFront: Int
+ ): RunningTaskInfo? = bringDesktopAppsToFront(displayId, wct, newTaskIdInFront)
- // First move home to front and then other tasks on top of it
- moveHomeTaskToFront(wct)
+ private fun bringDesktopAppsToFront(
+ displayId: Int,
+ wct: WindowContainerTransaction,
+ newTaskIdInFront: Int? = null
+ ): RunningTaskInfo? {
+ KtProtoLog.v(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: bringDesktopAppsToFront, newTaskIdInFront=%s",
+ newTaskIdInFront ?: "null"
+ )
- val allTasksInZOrder = desktopModeTaskRepository.getFreeformTasksInZOrder()
- activeTasks
- // Sort descending as the top task is at index 0. It should be ordered to top last
- .sortedByDescending { taskId -> allTasksInZOrder.indexOf(taskId) }
+ if (Flags.enableDesktopWindowingWallpaperActivity()) {
+ // Add translucent wallpaper activity to show the wallpaper underneath
+ addWallpaperActivity(wct)
+ } else {
+ // Move home to front
+ moveHomeTaskToFront(wct)
+ }
+
+ val nonMinimizedTasksOrderedFrontToBack =
+ desktopModeTaskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId)
+ // If we're adding a new Task we might need to minimize an old one
+ val taskToMinimize: RunningTaskInfo? =
+ if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) {
+ desktopTasksLimiter
+ .get()
+ .getTaskToMinimizeIfNeeded(
+ nonMinimizedTasksOrderedFrontToBack,
+ newTaskIdInFront
+ )
+ } else {
+ null
+ }
+ nonMinimizedTasksOrderedFrontToBack
+ // If there is a Task to minimize, let it stay behind the Home Task
+ .filter { taskId -> taskId != taskToMinimize?.taskId }
.mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) }
+ .reversed() // Start from the back so the front task is brought forward last
.forEach { task -> wct.reorder(task.token, true /* onTop */) }
+ return taskToMinimize
}
private fun moveHomeTaskToFront(wct: WindowContainerTransaction) {
@@ -646,7 +797,33 @@ class DesktopTasksController(
?.let { homeTask -> wct.reorder(homeTask.getToken(), true /* onTop */) }
}
- private fun releaseVisualIndicator() {
+ private fun addWallpaperActivity(wct: WindowContainerTransaction) {
+ KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: addWallpaper")
+ val intent = Intent(context, DesktopWallpaperActivity::class.java)
+ val options =
+ ActivityOptions.makeBasic().apply {
+ isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
+ pendingIntentBackgroundActivityStartMode =
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+ }
+ val pendingIntent =
+ PendingIntent.getActivity(
+ context,
+ /* requestCode = */ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ wct.sendPendingIntent(pendingIntent, intent, options.toBundle())
+ }
+
+ private fun removeWallpaperActivity(wct: WindowContainerTransaction) {
+ desktopModeTaskRepository.wallpaperActivityToken?.let { token ->
+ KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: removeWallpaper")
+ wct.removeTask(token)
+ }
+ }
+
+ fun releaseVisualIndicator() {
val t = SurfaceControl.Transaction()
visualIndicator?.releaseVisualIndicator(t)
visualIndicator = null
@@ -693,6 +870,8 @@ class DesktopTasksController(
reason = "recents animation is running"
false
}
+ // Handle back navigation for the last window if wallpaper available
+ shouldRemoveWallpaper(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})"
@@ -710,7 +889,7 @@ class DesktopTasksController(
}
// Only handle fullscreen or freeform tasks
triggerTask.windowingMode != WINDOWING_MODE_FULLSCREEN &&
- triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> {
+ triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> {
reason = "windowingMode not handled (${triggerTask.windowingMode})"
false
}
@@ -720,30 +899,34 @@ class DesktopTasksController(
if (!shouldHandleRequest) {
KtProtoLog.v(
- WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: skipping handleRequest reason=%s",
- reason
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: skipping handleRequest reason=%s",
+ reason
)
return null
}
- val result = triggerTask?.let { task ->
- when {
- // If display has tasks stashed, handle as stashed launch
- desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task)
- // Check if fullscreen task should be updated
- task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task)
- // Check if freeform task should be updated
- task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task)
- else -> {
- null
+ val result =
+ triggerTask?.let { task ->
+ when {
+ request.type == TRANSIT_TO_BACK -> handleBackNavigation(task)
+ // Check if the task has a top transparent activity
+ 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
+ task.isFreeform -> handleFreeformTaskLaunch(task, transition)
+ else -> {
+ null
+ }
}
}
- }
KtProtoLog.v(
- WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: handleRequest result=%s",
- result ?: "null"
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: handleRequest result=%s",
+ result ?: "null"
)
return result
}
@@ -753,82 +936,128 @@ class DesktopTasksController(
* This is intended to be used when desktop mode is part of another animation but isn't, itself,
* animating.
*/
- fun syncSurfaceState(
- info: TransitionInfo,
- finishTransaction: SurfaceControl.Transaction
- ) {
+ fun syncSurfaceState(info: TransitionInfo, finishTransaction: SurfaceControl.Transaction) {
// Add rounded corners to freeform windows
if (!DesktopModeStatus.useRoundedCorners()) {
return
}
val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
info.changes
- .filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM }
- .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) }
+ .filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM }
+ .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) }
}
- private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
+ // TODO(b/347289970): Consider replacing with API
+ private fun shouldLaunchAsModal(task: TaskInfo) =
+ Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task)
+
+ private fun shouldRemoveWallpaper(request: TransitionRequestInfo): Boolean {
+ return Flags.enableDesktopWindowingWallpaperActivity() &&
+ request.type == TRANSIT_TO_BACK &&
+ request.triggerTask?.let { task ->
+ desktopModeTaskRepository.isOnlyActiveTask(task.taskId)
+ }
+ ?: false
+ }
+
+ private fun handleFreeformTaskLaunch(
+ task: RunningTaskInfo,
+ transition: IBinder
+ ): WindowContainerTransaction? {
KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch")
- val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId)
- if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) {
+ if (!desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) {
KtProtoLog.d(
- WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: switch freeform task to fullscreen oon transition" +
- " taskId=%d",
- task.taskId
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: switch freeform task to fullscreen oon transition" +
+ " taskId=%d",
+ task.taskId
)
return WindowContainerTransaction().also { wct ->
- addMoveToFullscreenChanges(wct, task)
+ bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId)
+ wct.reorder(task.token, true)
}
}
- return null
+ val wct = WindowContainerTransaction()
+ 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
+ // a Task.
+ val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task)
+ if (taskToMinimize != null) {
+ addPendingMinimizeTransition(transition, taskToMinimize)
+ return wct
+ }
+ return if (wct.isEmpty) null else wct
}
- private fun handleFullscreenTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
+ private fun handleFullscreenTaskLaunch(
+ task: RunningTaskInfo,
+ transition: IBinder
+ ): WindowContainerTransaction? {
KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFullscreenTaskLaunch")
- val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId)
- if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) {
+ if (desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) {
KtProtoLog.d(
- WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: switch fullscreen task to freeform on transition" +
- " taskId=%d",
- task.taskId
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: switch fullscreen task to freeform on transition" +
+ " taskId=%d",
+ task.taskId
)
return WindowContainerTransaction().also { wct ->
addMoveToDesktopChanges(wct, task)
+ // Desktop Mode is already showing and we're launching a new Task - we might need to
+ // minimize another Task.
+ val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task)
+ addPendingMinimizeTransition(transition, taskToMinimize)
}
}
return null
}
- private fun handleStashedTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction {
- KtProtoLog.d(
- WM_SHELL_DESKTOP_MODE,
- "DesktopTasksController: launch apps with stashed on transition taskId=%d",
- task.taskId
- )
- val wct = WindowContainerTransaction()
- bringDesktopAppsToFront(task.displayId, wct)
- addMoveToDesktopChanges(wct, task)
- desktopModeTaskRepository.setStashed(task.displayId, false)
- return wct
+ /**
+ * 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) &&
+ desktopModeTaskRepository.wallpaperActivityToken != null
+ ) {
+ // Remove wallpaper activity when the last active task is removed
+ return WindowContainerTransaction().also { wct -> removeWallpaperActivity(wct) }
+ } else {
+ return null
+ }
}
private fun addMoveToDesktopChanges(
wct: WindowContainerTransaction,
taskInfo: RunningTaskInfo
) {
- val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode
- val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FREEFORM) {
- // Display windowing is freeform, set to undefined and inherit it
- WINDOWING_MODE_UNDEFINED
- } else {
- WINDOWING_MODE_FREEFORM
+ val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+ val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
+ val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
+ val targetWindowingMode =
+ if (tdaWindowingMode == WINDOWING_MODE_FREEFORM) {
+ // Display windowing is freeform, set to undefined and inherit it
+ WINDOWING_MODE_UNDEFINED
+ } else {
+ WINDOWING_MODE_FREEFORM
+ }
+ if (Flags.enableWindowingDynamicInitialBounds()) {
+ wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo))
}
wct.setWindowingMode(taskInfo.token, targetWindowingMode)
wct.reorder(taskInfo.token, true /* onTop */)
- if (isDesktopDensityOverrideSet()) {
- wct.setDensityDpi(taskInfo.token, getDesktopDensityDpi())
+ if (useDesktopOverrideDensity()) {
+ wct.setDensityDpi(taskInfo.token, DESKTOP_DENSITY_OVERRIDE)
}
}
@@ -836,16 +1065,18 @@ class DesktopTasksController(
wct: WindowContainerTransaction,
taskInfo: RunningTaskInfo
) {
- val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode
- val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FULLSCREEN) {
- // Display windowing is fullscreen, set to undefined and inherit it
- WINDOWING_MODE_UNDEFINED
- } else {
- WINDOWING_MODE_FULLSCREEN
- }
+ val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
+ val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
+ val targetWindowingMode =
+ if (tdaWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+ // Display windowing is fullscreen, set to undefined and inherit it
+ WINDOWING_MODE_UNDEFINED
+ } else {
+ WINDOWING_MODE_FULLSCREEN
+ }
wct.setWindowingMode(taskInfo.token, targetWindowingMode)
wct.setBounds(taskInfo.token, Rect())
- if (isDesktopDensityOverrideSet()) {
+ if (useDesktopOverrideDensity()) {
wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
}
}
@@ -854,32 +1085,82 @@ class DesktopTasksController(
* Adds split screen changes to a transaction. Note that bounds are not reset here due to
* animation; see {@link onDesktopSplitSelectAnimComplete}
*/
- private fun addMoveToSplitChanges(
- wct: WindowContainerTransaction,
- taskInfo: RunningTaskInfo
- ) {
- // Explicitly setting multi-window at task level interferes with animations.
- // Let task inherit windowing mode once transition is complete instead.
- wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED)
+ private fun addMoveToSplitChanges(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) {
+ // This windowing mode is to get the transition animation started; once we complete
+ // split select, we will change windowing mode to undefined and inherit from split stage.
+ // Going to undefined here causes task to flicker to the top left.
+ // Cancelling the split select flow will revert it to fullscreen.
+ wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW)
// The task's density may have been overridden in freeform; revert it here as we don't
// want it overridden in multi-window.
wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
}
+ /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */
+ private fun addAndGetMinimizeChangesIfNeeded(
+ displayId: Int,
+ wct: WindowContainerTransaction,
+ newTaskInfo: RunningTaskInfo
+ ): RunningTaskInfo? {
+ if (!desktopTasksLimiter.isPresent) return null
+ return desktopTasksLimiter
+ .get()
+ .addAndGetMinimizeTaskChangesIfNeeded(displayId, wct, newTaskInfo)
+ }
+
+ private fun addPendingMinimizeTransition(
+ transition: IBinder,
+ taskToMinimize: RunningTaskInfo?
+ ) {
+ if (taskToMinimize == null) return
+ desktopTasksLimiter.ifPresent {
+ it.addPendingMinimizeChange(transition, taskToMinimize.displayId, taskToMinimize.taskId)
+ }
+ }
+
+ /** Enter split by using the focused desktop task in given `displayId`. */
+ fun enterSplit(displayId: Int, leftOrTop: Boolean) {
+ getFocusedFreeformTask(displayId)?.let { requestSplit(it, leftOrTop) }
+ }
+
+ private fun getFocusedFreeformTask(displayId: Int): RunningTaskInfo? {
+ return shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo ->
+ taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
+ }
+ }
+
/**
* Requests a task be transitioned from desktop to split select. Applies needed windowing
* changes if this transition is enabled.
*/
+ @JvmOverloads
fun requestSplit(
- taskInfo: RunningTaskInfo
+ taskInfo: RunningTaskInfo,
+ 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,
- 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
+ )
+ }
}
}
@@ -887,10 +1168,6 @@ class DesktopTasksController(
return context.resources.displayMetrics.densityDpi
}
- private fun getDesktopDensityDpi(): Int {
- return DESKTOP_DENSITY_OVERRIDE
- }
-
/** Creates a new instance of the external interface to pass to another process. */
private fun createExternalInterface(): ExternalInterfaceBinder {
return IDesktopModeImpl(this)
@@ -903,9 +1180,9 @@ class DesktopTasksController(
/**
* Perform checks required on drag move. Create/release fullscreen indicator as needed.
- * Different sources for x and y coordinates are used due to different needs for each:
- * We want split transitions to be based on input coordinates but fullscreen transition
- * to be based on task edge coordinate.
+ * Different sources for x and y coordinates are used due to different needs for each: We want
+ * split transitions to be based on input coordinates but fullscreen transition to be based on
+ * task edge coordinate.
*
* @param taskInfo the task being dragged.
* @param taskSurface SurfaceControl of dragged task.
@@ -927,20 +1204,25 @@ class DesktopTasksController(
taskSurface: SurfaceControl,
inputX: Float,
taskTop: Float
- ) {
+ ): DesktopModeVisualIndicator.IndicatorType {
// If the visual indicator does not exist, create it.
- if (visualIndicator == null) {
- visualIndicator = DesktopModeVisualIndicator(
- syncQueue, taskInfo, displayController, context, taskSurface,
- rootTaskDisplayAreaOrganizer)
- }
- // Then, update the indicator type.
- val indicator = visualIndicator ?: return
- indicator.updateIndicatorType(PointF(inputX, taskTop), taskInfo.windowingMode)
+ val indicator =
+ visualIndicator
+ ?: DesktopModeVisualIndicator(
+ syncQueue,
+ taskInfo,
+ displayController,
+ context,
+ taskSurface,
+ rootTaskDisplayAreaOrganizer
+ )
+ if (visualIndicator == null) visualIndicator = indicator
+ return indicator.updateIndicatorType(PointF(inputX, taskTop), taskInfo.windowingMode)
}
/**
- * Perform checks required on drag end. Move to fullscreen if drag ends in status bar area.
+ * Perform checks required on drag end. If indicator indicates a windowing mode change, perform
+ * that change. Otherwise, ensure bounds are up to date.
*
* @param taskInfo the task being dragged.
* @param position position of surface when drag ends.
@@ -951,25 +1233,55 @@ class DesktopTasksController(
taskInfo: RunningTaskInfo,
position: Point,
inputCoordinate: PointF,
- taskBounds: Rect
+ taskBounds: Rect,
+ validDragArea: Rect
) {
if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) {
return
}
- if (taskBounds.top <= transitionAreaHeight) {
- moveToFullscreenWithAnimation(taskInfo, position)
- return
- }
- if (inputCoordinate.x <= transitionAreaWidth) {
- releaseVisualIndicator()
- snapToHalfScreen(taskInfo, SnapPosition.LEFT)
- return
- }
- if (inputCoordinate.x >= (displayController.getDisplayLayout(taskInfo.displayId)?.width()
- ?.minus(transitionAreaWidth) ?: return)) {
- releaseVisualIndicator()
- snapToHalfScreen(taskInfo, SnapPosition.RIGHT)
- return
+
+ val indicator = visualIndicator ?: return
+ val indicatorType =
+ indicator.updateIndicatorType(
+ PointF(inputCoordinate.x, taskBounds.top.toFloat()),
+ taskInfo.windowingMode
+ )
+ when (indicatorType) {
+ DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> {
+ moveToFullscreenWithAnimation(
+ taskInfo,
+ position,
+ DesktopModeTransitionSource.TASK_DRAG
+ )
+ }
+ DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
+ releaseVisualIndicator()
+ snapToHalfScreen(taskInfo, SnapPosition.LEFT)
+ }
+ DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
+ releaseVisualIndicator()
+ snapToHalfScreen(taskInfo, SnapPosition.RIGHT)
+ }
+ DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR -> {
+ // If task bounds are outside valid drag area, snap them inward and perform a
+ // transaction to set bounds.
+ if (
+ DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(
+ taskBounds,
+ validDragArea
+ )
+ ) {
+ val wct = WindowContainerTransaction()
+ wct.setBounds(taskInfo.token, taskBounds)
+ transitions.startTransition(TRANSIT_CHANGE, wct, null)
+ }
+ releaseVisualIndicator()
+ }
+ DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> {
+ throw IllegalArgumentException(
+ "Should not be receiving TO_DESKTOP_INDICATOR for " + "a freeform task."
+ )
+ }
}
// A freeform drag-move ended, remove the indicator immediately.
releaseVisualIndicator()
@@ -982,26 +1294,39 @@ class DesktopTasksController(
* @param y height of drag, to be checked against status bar height.
*/
fun onDragPositioningEndThroughStatusBar(
- taskInfo: RunningTaskInfo,
- freeformBounds: Rect
+ inputCoordinates: PointF,
+ taskInfo: RunningTaskInfo,
) {
- finalizeDragToDesktop(taskInfo, freeformBounds)
- }
-
- private fun getStatusBarHeight(taskInfo: RunningTaskInfo): Int {
- return displayController.getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0
+ val indicator = getVisualIndicator() ?: return
+ val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
+ when (indicatorType) {
+ DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> {
+ val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+ if (Flags.enableWindowingDynamicInitialBounds()) {
+ finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo))
+ } else {
+ finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout))
+ }
+ }
+ DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR,
+ DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> {
+ cancelDragToDesktop(taskInfo)
+ }
+ DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
+ requestSplit(taskInfo, leftOrTop = true)
+ }
+ DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
+ requestSplit(taskInfo, leftOrTop = false)
+ }
+ }
}
- /**
- * Update the exclusion region for a specified task
- */
+ /** Update the exclusion region for a specified task */
fun onExclusionRegionChanged(taskId: Int, exclusionRegion: Region) {
desktopModeTaskRepository.updateTaskExclusionRegions(taskId, exclusionRegion)
}
- /**
- * Remove a previously tracked exclusion region for a specified task.
- */
+ /** Remove a previously tracked exclusion region for a specified task. */
fun removeExclusionRegionForTask(taskId: Int) {
desktopModeTaskRepository.removeExclusionRegion(taskId)
}
@@ -1022,10 +1347,7 @@ class DesktopTasksController(
* @param listener the listener to add.
* @param callbackExecutor the executor to call the listener on.
*/
- fun setTaskRegionListener(
- listener: Consumer<Region>,
- callbackExecutor: Executor
- ) {
+ fun setTaskRegionListener(listener: Consumer<Region>, callbackExecutor: Executor) {
desktopModeTaskRepository.setExclusionRegionListener(listener, callbackExecutor)
}
@@ -1050,14 +1372,16 @@ class DesktopTasksController(
}
// Start a new transition to launch the app
- val opts = ActivityOptions.makeBasic().apply {
- launchWindowingMode = WINDOWING_MODE_FREEFORM
- pendingIntentLaunchFlags =
- Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
- setPendingIntentBackgroundActivityStartMode(
- ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
- isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
- }
+ val opts =
+ ActivityOptions.makeBasic().apply {
+ launchWindowingMode = WINDOWING_MODE_FREEFORM
+ pendingIntentLaunchFlags =
+ Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+ setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED
+ )
+ isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
+ }
val wct = WindowContainerTransaction()
wct.sendPendingIntent(launchIntent, null, opts.toBundle())
transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */)
@@ -1083,8 +1407,8 @@ class DesktopTasksController(
@ExternalThread
private inner class DesktopModeImpl : DesktopMode {
override fun addVisibleTasksListener(
- listener: VisibleTasksListener,
- callbackExecutor: Executor
+ listener: VisibleTasksListener,
+ callbackExecutor: Executor
) {
mainExecutor.execute {
this@DesktopTasksController.addVisibleTasksListener(listener, callbackExecutor)
@@ -1092,25 +1416,35 @@ class DesktopTasksController(
}
override fun addDesktopGestureExclusionRegionListener(
- listener: Consumer<Region>,
- callbackExecutor: Executor
+ listener: Consumer<Region>,
+ callbackExecutor: Executor
) {
mainExecutor.execute {
this@DesktopTasksController.setTaskRegionListener(listener, callbackExecutor)
}
}
- override fun enterDesktop(displayId: Int) {
+ override fun moveFocusedTaskToDesktop(
+ displayId: Int,
+ transitionSource: DesktopModeTransitionSource
+ ) {
mainExecutor.execute {
- this@DesktopTasksController.enterDesktop(displayId)
+ this@DesktopTasksController.moveFocusedTaskToDesktop(displayId, transitionSource)
}
}
- override fun moveFocusedTaskToFullscreen(displayId: Int) {
+ override fun moveFocusedTaskToFullscreen(
+ displayId: Int,
+ transitionSource: DesktopModeTransitionSource
+ ) {
mainExecutor.execute {
- this@DesktopTasksController.enterFullscreen(displayId)
+ this@DesktopTasksController.enterFullscreen(displayId, transitionSource)
}
}
+
+ override fun moveFocusedTaskToStageSplit(displayId: Int, leftOrTop: Boolean) {
+ mainExecutor.execute { this@DesktopTasksController.enterSplit(displayId, leftOrTop) }
+ }
}
/** The interface for calls from outside the host process. */
@@ -1119,46 +1453,35 @@ class DesktopTasksController(
IDesktopMode.Stub(), ExternalInterfaceBinder {
private lateinit var remoteListener:
- SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>
+ SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>
- private val listener: VisibleTasksListener = object : VisibleTasksListener {
- override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
- KtProtoLog.v(
+ private val listener: VisibleTasksListener =
+ object : VisibleTasksListener {
+ override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
+ KtProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"IDesktopModeImpl: onVisibilityChanged display=%d visible=%d",
displayId,
visibleTasksCount
- )
- remoteListener.call {
- l -> l.onTasksVisibilityChanged(displayId, visibleTasksCount)
+ )
+ remoteListener.call { l ->
+ l.onTasksVisibilityChanged(displayId, visibleTasksCount)
+ }
}
}
- override fun onStashedChanged(displayId: Int, stashed: Boolean) {
- KtProtoLog.v(
- WM_SHELL_DESKTOP_MODE,
- "IDesktopModeImpl: onStashedChanged display=%d stashed=%b",
- displayId,
- stashed
- )
- remoteListener.call { l -> l.onStashedChanged(displayId, stashed) }
- }
- }
-
init {
remoteListener =
- SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>(
- controller,
- { c ->
- c.desktopModeTaskRepository.addVisibleTasksListener(
- listener,
- c.mainExecutor
- )
- },
- { c ->
- c.desktopModeTaskRepository.removeVisibleTasksListener(listener)
- }
- )
+ SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>(
+ controller,
+ { c ->
+ c.desktopModeTaskRepository.addVisibleTasksListener(
+ listener,
+ c.mainExecutor
+ )
+ },
+ { c -> c.desktopModeTaskRepository.removeVisibleTasksListener(listener) }
+ )
}
/** Invalidates this instance, preventing future calls from updating the controller. */
@@ -1168,36 +1491,31 @@ class DesktopTasksController(
}
override fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition?) {
- ExecutorUtils.executeRemoteCallWithTaskPermission(
- controller,
- "showDesktopApps"
- ) { c -> c.showDesktopApps(displayId, remoteTransition) }
+ executeRemoteCallWithTaskPermission(controller, "showDesktopApps") { c ->
+ c.showDesktopApps(displayId, remoteTransition)
+ }
}
- override fun stashDesktopApps(displayId: Int) {
- ExecutorUtils.executeRemoteCallWithTaskPermission(
- controller,
- "stashDesktopApps"
- ) { c -> c.stashDesktopApps(displayId) }
+ override fun showDesktopApp(taskId: Int) {
+ executeRemoteCallWithTaskPermission(controller, "showDesktopApp") { c ->
+ c.moveTaskToFront(taskId)
+ }
}
- override fun hideStashedDesktopApps(displayId: Int) {
- ExecutorUtils.executeRemoteCallWithTaskPermission(
- controller,
- "hideStashedDesktopApps"
- ) { c -> c.hideStashedDesktopApps(displayId) }
+ override fun stashDesktopApps(displayId: Int) {
+ KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: stashDesktopApps is deprecated")
}
- override fun showDesktopApp(taskId: Int) {
- ExecutorUtils.executeRemoteCallWithTaskPermission(
- controller,
- "showDesktopApp"
- ) { c -> c.moveTaskToFront(taskId) }
+ override fun hideStashedDesktopApps(displayId: Int) {
+ KtProtoLog.w(
+ WM_SHELL_DESKTOP_MODE,
+ "IDesktopModeImpl: hideStashedDesktopApps is deprecated"
+ )
}
override fun getVisibleTaskCount(displayId: Int): Int {
val result = IntArray(1)
- ExecutorUtils.executeRemoteCallWithTaskPermission(
+ executeRemoteCallWithTaskPermission(
controller,
"getVisibleTaskCount",
{ controller -> result[0] = controller.getVisibleTaskCount(displayId) },
@@ -1207,43 +1525,41 @@ class DesktopTasksController(
}
override fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) {
- ExecutorUtils.executeRemoteCallWithTaskPermission(
+ executeRemoteCallWithTaskPermission(
controller,
"onDesktopSplitSelectAnimComplete"
- ) { c -> c.onDesktopSplitSelectAnimComplete(taskInfo) }
+ ) { c ->
+ c.onDesktopSplitSelectAnimComplete(taskInfo)
+ }
}
override fun setTaskListener(listener: IDesktopTaskListener?) {
KtProtoLog.v(
- WM_SHELL_DESKTOP_MODE,
- "IDesktopModeImpl: set task listener=%s",
- listener ?: "null"
+ WM_SHELL_DESKTOP_MODE,
+ "IDesktopModeImpl: set task listener=%s",
+ listener ?: "null"
)
- ExecutorUtils.executeRemoteCallWithTaskPermission(
- controller,
- "setTaskListener"
- ) { _ -> listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() }
+ executeRemoteCallWithTaskPermission(controller, "setTaskListener") { _ ->
+ listener?.let { remoteListener.register(it) } ?: remoteListener.unregister()
+ }
+ }
+
+ override fun moveToDesktop(taskId: Int, transitionSource: DesktopModeTransitionSource) {
+ executeRemoteCallWithTaskPermission(controller, "moveToDesktop") { c ->
+ c.moveToDesktop(taskId, transitionSource = transitionSource)
+ }
}
}
companion object {
- private val DESKTOP_DENSITY_OVERRIDE =
- SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 284)
- private val DESKTOP_DENSITY_ALLOWED_RANGE = (100..1000)
-
@JvmField
- val DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties
- .getInt("persist.wm.debug.freeform_initial_bounds_scale", 75) / 100f
-
- /**
- * Check if desktop density override is enabled
- */
- @JvmStatic
- fun isDesktopDensityOverrideSet(): Boolean {
- return DESKTOP_DENSITY_OVERRIDE in DESKTOP_DENSITY_ALLOWED_RANGE
- }
+ val DESKTOP_MODE_INITIAL_BOUNDS_SCALE =
+ SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
}
/** The positions on a screen that a task can snap to. */
- enum class SnapPosition { RIGHT, LEFT }
+ enum class SnapPosition {
+ RIGHT,
+ LEFT
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
new file mode 100644
index 000000000000..0f88384ec2ac
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -0,0 +1,218 @@
+/*
+ * 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.app.ActivityManager.RunningTaskInfo
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.window.TransitionInfo
+import android.window.WindowContainerTransaction
+import androidx.annotation.VisibleForTesting
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TransitionObserver
+import com.android.wm.shell.util.KtProtoLog
+
+/**
+ * Limits the number of tasks shown in Desktop Mode.
+ *
+ * This class should only be used if
+ * [com.android.window.flags.Flags.enableDesktopWindowingTaskLimit()] is true.
+ */
+class DesktopTasksLimiter (
+ transitions: Transitions,
+ private val taskRepository: DesktopModeTaskRepository,
+ private val shellTaskOrganizer: ShellTaskOrganizer,
+) {
+ private val minimizeTransitionObserver = MinimizeTransitionObserver()
+
+ init {
+ transitions.registerObserver(minimizeTransitionObserver)
+ }
+
+ private data class TaskDetails (val displayId: Int, val taskId: Int)
+
+ // TODO(b/333018485): replace this observer when implementing the minimize-animation
+ private inner class MinimizeTransitionObserver : TransitionObserver {
+ private val mPendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>()
+
+ fun addPendingTransitionToken(transition: IBinder, taskDetails: TaskDetails) {
+ mPendingTransitionTokensAndTasks[transition] = taskDetails
+ }
+
+ override fun onTransitionReady(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction
+ ) {
+ val taskToMinimize = mPendingTransitionTokensAndTasks.remove(transition) ?: return
+
+ if (!taskRepository.isActiveTask(taskToMinimize.taskId)) return
+
+ if (!isTaskReorderedToBackOrInvisible(info, taskToMinimize)) {
+ KtProtoLog.v(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksLimiter: task %d is not reordered to back nor invis",
+ taskToMinimize.taskId)
+ return
+ }
+ this@DesktopTasksLimiter.markTaskMinimized(
+ taskToMinimize.displayId, taskToMinimize.taskId)
+ }
+
+ /**
+ * Returns whether the given Task is being reordered to the back in the given transition, or
+ * is already invisible.
+ *
+ * <p> This check can be used to double-check that a task was indeed minimized before
+ * marking it as such.
+ */
+ private fun isTaskReorderedToBackOrInvisible(
+ info: TransitionInfo,
+ taskDetails: TaskDetails
+ ): Boolean {
+ val taskChange = info.changes.find { change ->
+ change.taskInfo?.taskId == taskDetails.taskId }
+ if (taskChange == null) {
+ return !taskRepository.isVisibleTask(taskDetails.taskId)
+ }
+ return taskChange.mode == TRANSIT_TO_BACK
+ }
+
+ override fun onTransitionStarting(transition: IBinder) {}
+
+ override fun onTransitionMerged(merged: IBinder, playing: IBinder) {
+ mPendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer ->
+ mPendingTransitionTokensAndTasks[playing] = taskToTransfer
+ }
+ }
+
+ override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
+ KtProtoLog.v(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksLimiter: transition %s finished", transition)
+ mPendingTransitionTokensAndTasks.remove(transition)
+ }
+ }
+
+ /**
+ * Mark a task as minimized, this should only be done after the corresponding transition has
+ * finished so we don't minimize the task if the transition fails.
+ */
+ private fun markTaskMinimized(displayId: Int, taskId: Int) {
+ KtProtoLog.v(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksLimiter: marking %d as minimized", taskId)
+ taskRepository.minimizeTask(displayId, taskId)
+ }
+
+ /**
+ * Add a minimize-transition to [wct] if adding [newFrontTaskInfo] brings us over the task
+ * limit.
+ *
+ * @param transition the transition that the minimize-transition will be appended to, or null if
+ * the transition will be started later.
+ * @return the ID of the minimized task, or null if no task is being minimized.
+ */
+ fun addAndGetMinimizeTaskChangesIfNeeded(
+ displayId: Int,
+ wct: WindowContainerTransaction,
+ newFrontTaskInfo: RunningTaskInfo,
+ ): RunningTaskInfo? {
+ KtProtoLog.v(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksLimiter: addMinimizeBackTaskChangesIfNeeded, newFrontTask=%d",
+ newFrontTaskInfo.taskId)
+ val newTaskListOrderedFrontToBack = createOrderedTaskListWithGivenTaskInFront(
+ taskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId),
+ newFrontTaskInfo.taskId)
+ val taskToMinimize = getTaskToMinimizeIfNeeded(newTaskListOrderedFrontToBack)
+ if (taskToMinimize != null) {
+ wct.reorder(taskToMinimize.token, false /* onTop */)
+ return taskToMinimize
+ }
+ return null
+ }
+
+ /**
+ * Add a pending minimize transition change, to update the list of minimized apps once the
+ * transition goes through.
+ */
+ fun addPendingMinimizeChange(transition: IBinder, displayId: Int, taskId: Int) {
+ minimizeTransitionObserver.addPendingTransitionToken(
+ transition, TaskDetails(displayId, taskId))
+ }
+
+ /**
+ * Returns the maximum number of tasks that should ever be displayed at the same time in Desktop
+ * Mode.
+ */
+ fun getMaxTaskLimit(): Int = DesktopModeStatus.getMaxTaskLimit()
+
+ /**
+ * Returns the Task to minimize given 1. a list of visible tasks ordered from front to back and
+ * 2. a new task placed in front of all the others.
+ */
+ fun getTaskToMinimizeIfNeeded(
+ visibleFreeformTaskIdsOrderedFrontToBack: List<Int>,
+ newTaskIdInFront: Int
+ ): RunningTaskInfo? {
+ return getTaskToMinimizeIfNeeded(
+ createOrderedTaskListWithGivenTaskInFront(
+ visibleFreeformTaskIdsOrderedFrontToBack, newTaskIdInFront))
+ }
+
+ /** Returns the Task to minimize given a list of visible tasks ordered from front to back. */
+ fun getTaskToMinimizeIfNeeded(
+ visibleFreeformTaskIdsOrderedFrontToBack: List<Int>
+ ): RunningTaskInfo? {
+ if (visibleFreeformTaskIdsOrderedFrontToBack.size <= getMaxTaskLimit()) {
+ KtProtoLog.v(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksLimiter: no need to minimize; tasks below limit")
+ // No need to minimize anything
+ return null
+ }
+ val taskToMinimize =
+ shellTaskOrganizer.getRunningTaskInfo(
+ visibleFreeformTaskIdsOrderedFrontToBack.last())
+ if (taskToMinimize == null) {
+ KtProtoLog.e(
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksLimiter: taskToMinimize == null")
+ return null
+ }
+ return taskToMinimize
+ }
+
+ private fun createOrderedTaskListWithGivenTaskInFront(
+ existingTaskIdsOrderedFrontToBack: List<Int>,
+ newTaskId: Int
+ ): List<Int> {
+ return listOf(newTaskId) +
+ existingTaskIdsOrderedFrontToBack.filter { taskId -> taskId != newTaskId }
+ }
+
+ @VisibleForTesting
+ fun getTransitionObserver(): TransitionObserver {
+ return minimizeTransitionObserver
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
new file mode 100644
index 000000000000..dae75f90e3ae
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.content.Context
+import android.os.IBinder
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.util.KtProtoLog
+
+/**
+ * A [Transitions.TransitionObserver] that observes shell transitions and updates
+ * the [DesktopModeTaskRepository] state TODO: b/332682201
+ * This observes transitions related to desktop mode
+ * and other transitions that originate both within and outside shell.
+ */
+class DesktopTasksTransitionObserver(
+ context: Context,
+ private val desktopModeTaskRepository: DesktopModeTaskRepository,
+ private val transitions: Transitions,
+ shellInit: ShellInit
+) : Transitions.TransitionObserver {
+
+ init {
+ if (Transitions.ENABLE_SHELL_TRANSITIONS &&
+ DesktopModeStatus.canEnterDesktopMode(context)) {
+ shellInit.addInitCallback(::onInit, this)
+ }
+ }
+
+ fun onInit() {
+ KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopTasksTransitionObserver: onInit")
+ transitions.registerObserver(this)
+ }
+
+ override fun onTransitionReady(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction
+ ) {
+ // TODO: b/332682201 Update repository state
+ updateWallpaperToken(info)
+ }
+
+ override fun onTransitionStarting(transition: IBinder) {
+ // TODO: b/332682201 Update repository state
+ }
+
+ override fun onTransitionMerged(merged: IBinder, playing: IBinder) {
+ // TODO: b/332682201 Update repository state
+ }
+
+ override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
+ // TODO: b/332682201 Update repository state
+ }
+
+ private fun updateWallpaperToken(info: TransitionInfo) {
+ if (!enableDesktopWindowingWallpaperActivity()) {
+ return
+ }
+ info.changes.forEach { change ->
+ change.taskInfo?.let { taskInfo ->
+ if (DesktopWallpaperActivity.isWallpaperTask(taskInfo)) {
+ when (change.mode) {
+ WindowManager.TRANSIT_OPEN ->
+ desktopModeTaskRepository.wallpaperActivityToken = taskInfo.token
+ WindowManager.TRANSIT_CLOSE ->
+ desktopModeTaskRepository.wallpaperActivityToken = null
+ else -> {}
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt
new file mode 100644
index 000000000000..c4a4474689fa
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.app.Activity
+import android.app.ActivityManager
+import android.content.ComponentName
+import android.os.Bundle
+import android.view.WindowManager
+import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
+import com.android.wm.shell.util.KtProtoLog
+
+/**
+ * A transparent activity used in the desktop mode to show the wallpaper under the freeform windows.
+ * This activity will be running in `FULLSCREEN` windowing mode, which ensures it hides Launcher.
+ * When entering desktop, we would ensure that it's added behind desktop apps and removed when
+ * leaving the desktop mode.
+ *
+ * Note! This activity should NOT interact directly with any other code in the Shell without calling
+ * onto the shell main thread. Activities are always started on the main thread.
+ */
+class DesktopWallpaperActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopWallpaperActivity: onCreate")
+ super.onCreate(savedInstanceState)
+ window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
+ }
+
+ companion object {
+ private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"
+ private val wallpaperActivityComponent =
+ ComponentName(SYSTEM_UI_PACKAGE_NAME, DesktopWallpaperActivity::class.java.name)
+
+ @JvmStatic
+ fun isWallpaperTask(taskInfo: ActivityManager.RunningTaskInfo) =
+ taskInfo.baseIntent.component?.let(::isWallpaperComponent) ?: false
+
+ @JvmStatic
+ fun isWallpaperComponent(component: ComponentName) =
+ component == wallpaperActivityComponent
+ }
+}
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 af26e2980afe..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,17 +4,22 @@ 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
import android.app.PendingIntent
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
import android.os.SystemClock
import android.view.SurfaceControl
@@ -25,6 +30,10 @@ import android.window.TransitionRequestInfo
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
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
@@ -45,29 +54,28 @@ import java.util.function.Supplier
* gesture.
*/
class DragToDesktopTransitionHandler(
- private val context: Context,
- private val transitions: Transitions,
- private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
- private val transactionSupplier: Supplier<SurfaceControl.Transaction>
+ private val context: Context,
+ private val transitions: Transitions,
+ private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
+ private val transactionSupplier: Supplier<SurfaceControl.Transaction>
) : TransitionHandler {
constructor(
- context: Context,
- transitions: Transitions,
- rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+ context: Context,
+ transitions: Transitions,
+ rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
) : this(
- context,
- transitions,
- rootTaskDisplayAreaOrganizer,
- Supplier { SurfaceControl.Transaction() }
+ context,
+ transitions,
+ rootTaskDisplayAreaOrganizer,
+ Supplier { SurfaceControl.Transaction() }
)
private val rectEvaluator = RectEvaluator(Rect())
- private val launchHomeIntent = Intent(Intent.ACTION_MAIN)
- .addCategory(Intent.CATEGORY_HOME)
+ private val launchHomeIntent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
private var dragToDesktopStateListener: DragToDesktopStateListener? = null
- private var splitScreenController: SplitScreenController? = null
+ private lateinit var splitScreenController: SplitScreenController
private var transitionState: TransitionState? = null
private lateinit var onTaskResizeAnimationListener: OnTaskResizeAnimationListener
@@ -75,6 +83,9 @@ class DragToDesktopTransitionHandler(
val inProgress: Boolean
get() = transitionState != null
+ /** The task id of the task currently being dragged from fullscreen/split. */
+ val draggingTaskId: Int
+ get() = transitionState?.draggedTaskId ?: INVALID_TASK_ID
/** Sets a listener to receive callback about events during the transition animation. */
fun setDragToDesktopStateListener(listener: DragToDesktopStateListener) {
dragToDesktopStateListener = listener
@@ -99,48 +110,55 @@ class DragToDesktopTransitionHandler(
* after one of the "end" or "cancel" transitions is merged into this transition.
*/
fun startDragToDesktopTransition(
- taskId: Int,
- dragToDesktopAnimator: MoveToDesktopAnimator,
+ taskId: Int,
+ dragToDesktopAnimator: MoveToDesktopAnimator,
) {
if (inProgress) {
KtProtoLog.v(
- ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
- "DragToDesktop: Drag to desktop transition already in progress."
+ ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
+ "DragToDesktop: Drag to desktop transition already in progress."
)
return
}
- val options = ActivityOptions.makeBasic().apply {
- setTransientLaunch()
- setSourceInfo(SourceInfo.TYPE_DESKTOP_ANIMATION, SystemClock.uptimeMillis())
- pendingIntentCreatorBackgroundActivityStartMode =
- ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
- }
- val pendingIntent = PendingIntent.getActivity(
+ val options =
+ ActivityOptions.makeBasic().apply {
+ setTransientLaunch()
+ setSourceInfo(SourceInfo.TYPE_DESKTOP_ANIMATION, SystemClock.uptimeMillis())
+ pendingIntentCreatorBackgroundActivityStartMode =
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+ }
+ val pendingIntent =
+ PendingIntent.getActivity(
context,
0 /* requestCode */,
launchHomeIntent,
FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT,
options.toBundle()
- )
+ )
val wct = WindowContainerTransaction()
- wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle())
- val startTransitionToken = transitions
- .startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this)
-
- transitionState = if (isSplitTask(taskId)) {
- TransitionState.FromSplit(
+ wct.sendPendingIntent(pendingIntent, launchHomeIntent, Bundle())
+ val startTransitionToken =
+ transitions.startTransition(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, wct, this)
+
+ transitionState =
+ if (isSplitTask(taskId)) {
+ val otherTask =
+ getOtherSplitTask(taskId)
+ ?: throw IllegalStateException("Expected split task to have a counterpart.")
+ TransitionState.FromSplit(
draggedTaskId = taskId,
dragAnimator = dragToDesktopAnimator,
- startTransitionToken = startTransitionToken
- )
- } else {
- TransitionState.FromFullscreen(
+ startTransitionToken = startTransitionToken,
+ otherSplitTask = otherTask
+ )
+ } else {
+ TransitionState.FromFullscreen(
draggedTaskId = taskId,
dragAnimator = dragToDesktopAnimator,
startTransitionToken = startTransitionToken
- )
- }
+ )
+ }
}
/**
@@ -149,20 +167,20 @@ class DragToDesktopTransitionHandler(
* windowing mode changes to the dragged task. This is called when the dragged task is released
* inside the desktop drop zone.
*/
- fun finishDragToDesktopTransition(wct: WindowContainerTransaction) {
+ fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? {
if (!inProgress) {
// Don't attempt to finish a drag to desktop transition since there is no transition in
// progress which means that the drag to desktop transition was never successfully
// started.
- return
+ return null
}
if (requireTransitionState().startAborted) {
// Don't attempt to complete the drag-to-desktop since the start transition didn't
// succeed as expected. Just reset the state as if nothing happened.
clearState()
- return
+ return null
}
- transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this)
+ return transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this)
}
/**
@@ -172,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
@@ -186,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
@@ -203,16 +240,65 @@ 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,
- startTransaction: SurfaceControl.Transaction,
- finishTransaction: SurfaceControl.Transaction,
- finishCallback: Transitions.TransitionFinishCallback
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction,
+ finishCallback: Transitions.TransitionFinishCallback
): Boolean {
val state = requireTransitionState()
- val isStartDragToDesktop = info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP &&
+ val isStartDragToDesktop =
+ info.type == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP &&
transition == state.startTransitionToken
if (!isStartDragToDesktop) {
return false
@@ -245,13 +331,16 @@ class DragToDesktopTransitionHandler(
when (state) {
is TransitionState.FromSplit -> {
state.splitRootChange = change
- val layer = if (!state.cancelled) {
- // Normal case, split root goes to the bottom behind everything else.
- appLayers - i
- } else {
- // Cancel-early case, pretend nothing happened so split root stays top.
- dragLayer
- }
+ val layer =
+ if (state.cancelState == CancelState.NO_CANCEL) {
+ // Normal case, split root goes to the bottom behind everything
+ // else.
+ appLayers - i
+ } else {
+ // Cancel-early case, pretend nothing happened so split root stays
+ // top.
+ dragLayer
+ }
startTransaction.apply {
setLayer(change.leash, layer)
show(change.leash)
@@ -293,10 +382,23 @@ 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, startTransaction)
+ change.endDisplayId,
+ change.leash,
+ startTransaction
+ )
val bounds = change.endAbsBounds
startTransaction.apply {
setLayer(change.leash, dragLayer)
@@ -310,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
@@ -322,30 +424,68 @@ 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
}
override fun mergeAnimation(
- transition: IBinder,
- info: TransitionInfo,
- t: SurfaceControl.Transaction,
- mergeTarget: IBinder,
- finishCallback: Transitions.TransitionFinishCallback
+ transition: IBinder,
+ info: TransitionInfo,
+ t: SurfaceControl.Transaction,
+ mergeTarget: IBinder,
+ finishCallback: Transitions.TransitionFinishCallback
) {
val state = requireTransitionState()
- val isCancelTransition = info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP &&
+ // 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 &&
mergeTarget == state.startTransitionToken
- val isEndTransition = info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP &&
+ val isEndTransition =
+ info.type == TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP &&
mergeTarget == state.startTransitionToken
- val startTransactionFinishT = state.startTransitionFinishTransaction
+ val startTransactionFinishT =
+ state.startTransitionFinishTransaction
?: error("Start transition expected to be waiting for merge but wasn't")
- val startTransitionFinishCb = state.startTransitionFinishCb
+ val startTransitionFinishCb =
+ state.startTransitionFinishCb
?: error("Start transition expected to be waiting for merge but wasn't")
if (isEndTransition) {
info.changes.withIndex().forEach { (i, change) ->
+ // If we're exiting split, hide the remaining split task.
+ if (
+ state is TransitionState.FromSplit &&
+ change.taskInfo?.taskId == state.otherSplitTask
+ ) {
+ t.hide(change.leash)
+ startTransactionFinishT.hide(change.leash)
+ }
if (change.mode == TRANSIT_CLOSE) {
t.hide(change.leash)
startTransactionFinishT.hide(change.leash)
@@ -355,97 +495,83 @@ class DragToDesktopTransitionHandler(
state.draggedTaskChange = change
} else if (change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM) {
// Other freeform tasks that are being restored go behind the dragged task.
- val draggedTaskLeash = state.draggedTaskChange?.leash
+ val draggedTaskLeash =
+ state.draggedTaskChange?.leash
?: error("Expected dragged leash to be non-null")
t.setRelativeLayer(change.leash, draggedTaskLeash, -i)
startTransactionFinishT.setRelativeLayer(change.leash, draggedTaskLeash, -i)
}
}
- val draggedTaskChange = state.draggedTaskChange
+ val draggedTaskChange =
+ state.draggedTaskChange
?: throw IllegalStateException("Expected non-null change of dragged task")
val draggedTaskLeash = draggedTaskChange.leash
val startBounds = draggedTaskChange.startAbsBounds
val endBounds = draggedTaskChange.endAbsBounds
- // TODO(b/301106941): Instead of forcing-finishing the animation that scales the
- // surface down and then starting another that scales it back up to the final size,
- // blend the two animations.
- state.dragAnimator.endAnimator()
- // Using [DRAG_FREEFORM_SCALE] to calculate animated width/height is possible because
- // it is known that the animation scale is finished because the animation was
- // force-ended above. This won't be true when the two animations are blended.
- val animStartWidth = (startBounds.width() * DRAG_FREEFORM_SCALE).toInt()
- val animStartHeight = (startBounds.height() * DRAG_FREEFORM_SCALE).toInt()
- // Using end bounds here to find the left/top also assumes the center animation has
- // finished and the surface is placed exactly in the center of the screen which matches
- // the end/default bounds of the now freeform task.
- val animStartLeft = endBounds.centerX() - (animStartWidth / 2)
- val animStartTop = endBounds.centerY() - (animStartHeight / 2)
- val animStartBounds = Rect(
- animStartLeft,
- animStartTop,
- animStartLeft + animStartWidth,
- animStartTop + animStartHeight
- )
-
+ // Pause any animation that may be currently playing; we will use the relevant
+ // details of that animation here.
+ state.dragAnimator.cancelAnimator()
+ // We still apply scale to task bounds; as we animate the bounds to their
+ // end value, animate scale to 1.
+ val startScale = state.dragAnimator.scale
+ val startPosition = state.dragAnimator.position
+ val unscaledStartWidth = startBounds.width()
+ val unscaledStartHeight = startBounds.height()
+ val unscaledStartBounds =
+ Rect(
+ startPosition.x.toInt(),
+ startPosition.y.toInt(),
+ startPosition.x.toInt() + unscaledStartWidth,
+ startPosition.y.toInt() + unscaledStartHeight
+ )
dragToDesktopStateListener?.onCommitToDesktopAnimationStart(t)
- t.apply {
- setScale(draggedTaskLeash, 1f, 1f)
- setPosition(
- draggedTaskLeash,
- animStartBounds.left.toFloat(),
- animStartBounds.top.toFloat()
- )
- setWindowCrop(
- draggedTaskLeash,
- animStartBounds.width(),
- animStartBounds.height()
- )
- }
// Accept the merge by applying the merging transaction (applied by #showResizeVeil)
// and finish callback. Show the veil and position the task at the first frame before
// starting the final animation.
- onTaskResizeAnimationListener.onAnimationStart(state.draggedTaskId, t, animStartBounds)
+ onTaskResizeAnimationListener.onAnimationStart(
+ state.draggedTaskId,
+ t,
+ unscaledStartBounds
+ )
finishCallback.onTransitionFinished(null /* wct */)
-
- // Because the task surface was scaled down during the drag, we must use the animated
- // bounds instead of the [startAbsBounds].
val tx: SurfaceControl.Transaction = transactionSupplier.get()
- ValueAnimator.ofObject(rectEvaluator, animStartBounds, endBounds)
- .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
- .apply {
- addUpdateListener { animator ->
- val animBounds = animator.animatedValue as Rect
- tx.apply {
- setScale(draggedTaskLeash, 1f, 1f)
- setPosition(
- draggedTaskLeash,
- animBounds.left.toFloat(),
- animBounds.top.toFloat()
- )
- setWindowCrop(
- draggedTaskLeash,
- animBounds.width(),
- animBounds.height()
- )
- }
- onTaskResizeAnimationListener.onBoundsChange(
- state.draggedTaskId,
- tx,
- animBounds
+ ValueAnimator.ofObject(rectEvaluator, unscaledStartBounds, endBounds)
+ .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
+ .apply {
+ addUpdateListener { animator ->
+ val animBounds = animator.animatedValue as Rect
+ val animFraction = animator.animatedFraction
+ // Progress scale from starting value to 1 as animation plays.
+ val animScale = startScale + animFraction * (1 - startScale)
+ tx.apply {
+ setScale(draggedTaskLeash, animScale, animScale)
+ setPosition(
+ draggedTaskLeash,
+ animBounds.left.toFloat(),
+ animBounds.top.toFloat()
)
+ setWindowCrop(draggedTaskLeash, animBounds.width(), animBounds.height())
}
- addListener(object : AnimatorListenerAdapter() {
+ onTaskResizeAnimationListener.onBoundsChange(
+ state.draggedTaskId,
+ tx,
+ animBounds
+ )
+ }
+ addListener(
+ object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
onTaskResizeAnimationListener.onAnimationEnd(state.draggedTaskId)
startTransitionFinishCb.onTransitionFinished(null /* null */)
clearState()
}
- })
- start()
- }
+ }
+ )
+ start()
+ }
} else if (isCancelTransition) {
info.changes.forEach { change ->
t.show(change.leash)
@@ -459,8 +585,8 @@ class DragToDesktopTransitionHandler(
}
override fun handleRequest(
- transition: IBinder,
- request: TransitionRequestInfo
+ transition: IBinder,
+ request: TransitionRequestInfo
): WindowContainerTransaction? {
// Only handle transitions started from shell.
return null
@@ -489,13 +615,11 @@ class DragToDesktopTransitionHandler(
val state = requireTransitionState()
val dragToDesktopAnimator = state.dragAnimator
- val draggedTaskChange = state.draggedTaskChange
- ?: throw IllegalStateException("Expected non-null task change")
+ val draggedTaskChange =
+ state.draggedTaskChange ?: throw IllegalStateException("Expected non-null task change")
val sc = draggedTaskChange.leash
- // TODO(b/301106941): Don't end the animation and start one to scale it back, merge them
- // instead.
- // End the animation that shrinks the window when task is first dragged from fullscreen
- dragToDesktopAnimator.endAnimator()
+ // Pause the animation that shrinks the window when task is first dragged from fullscreen
+ dragToDesktopAnimator.cancelAnimator()
// Then animate the scaled window back to its original bounds.
val x: Float = dragToDesktopAnimator.position.x
val y: Float = dragToDesktopAnimator.position.y
@@ -505,61 +629,75 @@ class DragToDesktopTransitionHandler(
val dy = targetY - y
val tx: SurfaceControl.Transaction = transactionSupplier.get()
ValueAnimator.ofFloat(DRAG_FREEFORM_SCALE, 1f)
- .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
- .apply {
- addUpdateListener { animator ->
- val scale = animator.animatedValue as Float
- val fraction = animator.animatedFraction
- val animX = x + (dx * fraction)
- val animY = y + (dy * fraction)
- tx.apply {
- setPosition(sc, animX, animY)
- setScale(sc, scale, scale)
- show(sc)
- apply()
- }
+ .setDuration(DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS)
+ .apply {
+ addUpdateListener { animator ->
+ val scale = animator.animatedValue as Float
+ val fraction = animator.animatedFraction
+ val animX = x + (dx * fraction)
+ val animY = y + (dy * fraction)
+ tx.apply {
+ setPosition(sc, animX, animY)
+ setScale(sc, scale, scale)
+ show(sc)
+ apply()
}
- addListener(object : AnimatorListenerAdapter() {
+ }
+ addListener(
+ object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
dragToDesktopStateListener?.onCancelToDesktopAnimationEnd(tx)
// Start the cancel transition to restore order.
startCancelDragToDesktopTransition()
}
- })
- start()
- }
+ }
+ )
+ start()
+ }
}
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
// when the dragged task is translucent and that makes the task behind it visible).
// Restore the order of those first.
- state.otherRootChanges.mapNotNull { it.container }.forEach { wc ->
- // TODO(b/322852244): investigate why even though these "other" tasks are
- // reordered in front of home and behind the translucent dragged task, its
- // surface is not visible on screen.
- wct.reorder(wc, true /* toTop */)
- }
- val wc = state.draggedTaskChange?.container
+ state.otherRootChanges
+ .mapNotNull { it.container }
+ .forEach { wc ->
+ // TODO(b/322852244): investigate why even though these "other" tasks are
+ // reordered in front of home and behind the translucent dragged task, its
+ // surface is not visible on screen.
+ wct.reorder(wc, true /* toTop */)
+ }
+ val wc =
+ state.draggedTaskChange?.container
?: error("Dragged task should be non-null before cancelling")
// Then the dragged task a the very top.
wct.reorder(wc, true /* toTop */)
}
is TransitionState.FromSplit -> {
- val wc = state.splitRootChange?.container
+ val wc =
+ state.splitRootChange?.container
?: error("Split root should be non-null before cancelling")
wct.reorder(wc, true /* toTop */)
}
}
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() {
@@ -567,7 +705,19 @@ class DragToDesktopTransitionHandler(
}
private fun isSplitTask(taskId: Int): Boolean {
- return splitScreenController?.isTaskInSplitScreen(taskId) ?: false
+ return splitScreenController.isTaskInSplitScreen(taskId)
+ }
+
+ private fun getOtherSplitTask(taskId: Int): Int? {
+ val splitPos = splitScreenController.getSplitPosition(taskId)
+ if (splitPos == SPLIT_POSITION_UNDEFINED) return null
+ val otherTaskPos =
+ if (splitPos == SPLIT_POSITION_BOTTOM_OR_RIGHT) {
+ SPLIT_POSITION_TOP_OR_LEFT
+ } else {
+ SPLIT_POSITION_BOTTOM_OR_RIGHT
+ }
+ return splitScreenController.getTaskInfo(otherTaskPos)?.taskId
}
private fun requireTransitionState(): TransitionState {
@@ -576,6 +726,7 @@ class DragToDesktopTransitionHandler(
interface DragToDesktopStateListener {
fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction)
+
fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction)
}
@@ -588,37 +739,51 @@ 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(
- override val draggedTaskId: Int,
- override val dragAnimator: MoveToDesktopAnimator,
- override val startTransitionToken: IBinder,
- override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
- override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
- override var cancelTransitionToken: IBinder? = null,
- override var homeToken: WindowContainerToken? = null,
- override var draggedTaskChange: Change? = null,
- override var cancelled: Boolean = false,
- override var startAborted: Boolean = false,
- var otherRootChanges: MutableList<Change> = mutableListOf()
+ override val draggedTaskId: Int,
+ override val dragAnimator: MoveToDesktopAnimator,
+ override val startTransitionToken: IBinder,
+ override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
+ override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
+ override var cancelTransitionToken: IBinder? = null,
+ override var homeToken: WindowContainerToken? = null,
+ override var draggedTaskChange: Change? = null,
+ override var cancelState: CancelState = CancelState.NO_CANCEL,
+ override var startAborted: Boolean = false,
+ var otherRootChanges: MutableList<Change> = mutableListOf()
) : TransitionState()
+
data class FromSplit(
- override val draggedTaskId: Int,
- override val dragAnimator: MoveToDesktopAnimator,
- override val startTransitionToken: IBinder,
- override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
- override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
- override var cancelTransitionToken: IBinder? = null,
- override var homeToken: WindowContainerToken? = null,
- override var draggedTaskChange: Change? = null,
- override var cancelled: Boolean = false,
- override var startAborted: Boolean = false,
- var splitRootChange: Change? = null,
+ override val draggedTaskId: Int,
+ override val dragAnimator: MoveToDesktopAnimator,
+ override val startTransitionToken: IBinder,
+ override var startTransitionFinishCb: Transitions.TransitionFinishCallback? = null,
+ override var startTransitionFinishTransaction: SurfaceControl.Transaction? = null,
+ override var cancelTransitionToken: IBinder? = null,
+ override var homeToken: WindowContainerToken? = null,
+ override var draggedTaskChange: Change? = null,
+ 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 79bb5408df82..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;
@@ -59,6 +62,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition
private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
private OnTaskResizeAnimationListener mOnTaskResizeAnimationListener;
+
public EnterDesktopTaskTransitionHandler(
Transitions transitions) {
this(transitions, SurfaceControl.Transaction::new);
@@ -72,16 +76,23 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition
}
void setOnTaskResizeAnimationListener(OnTaskResizeAnimationListener listener) {
- mOnTaskResizeAnimationListener = listener;
+ mOnTaskResizeAnimationListener = listener;
}
/**
* Starts Transition of type TRANSIT_MOVE_TO_DESKTOP
+ *
* @param wct WindowContainerTransaction for transition
+ * @return the token representing the started transition
*/
- public void 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;
}
@Override
@@ -113,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,
@@ -123,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 7342bd1ae5de..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,11 +57,12 @@ 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<>();
private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback;
- private Supplier<SurfaceControl.Transaction> mTransactionSupplier;
+ private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
private Point mPosition;
public ExitDesktopTaskTransitionHandler(
@@ -76,17 +82,19 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH
/**
* Starts Transition of a given type
- * @param type Transition type
- * @param wct WindowContainerTransaction for transition
- * @param position Position of the task when transition is started
+ *
+ * @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);
}
@@ -120,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,
@@ -129,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 6bdaf1eadb8a..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;
/**
@@ -28,10 +29,10 @@ interface IDesktopMode {
/** Show apps on the desktop on the given display */
void showDesktopApps(int displayId, in RemoteTransition remoteTransition);
- /** Stash apps on the desktop to allow launching another app from home screen */
+ /** @deprecated use {@link #showDesktopApps} instead. */
void stashDesktopApps(int displayId);
- /** Hide apps that may be stashed */
+ /** @deprecated this is no longer supported. */
void hideStashedDesktopApps(int displayId);
/** Bring task with the given id to front */
@@ -45,4 +46,7 @@ interface IDesktopMode {
/** Set listener that will receive callbacks about updates to desktop tasks */
oneway void setTaskListener(IDesktopTaskListener listener);
+
+ /** Move a task with given `taskId` to desktop */
+ 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/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
index 8ed87f23bf40..8ebdfdcf4731 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl
@@ -25,6 +25,6 @@ interface IDesktopTaskListener {
/** Desktop tasks visibility has changed. Visible if at least 1 task is visible. */
oneway void onTasksVisibilityChanged(int displayId, int visibleTasksCount);
- /** Desktop task stashed status has changed. */
+ /** @deprecated this is no longer supported. */
oneway void onStashedChanged(int displayId, boolean stashed);
} \ No newline at end of file
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 c469e652b117..88d0554669b7 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
@@ -86,9 +86,9 @@ class ToggleResizeDesktopTaskTransitionHandler(
.setWindowCrop(leash, startBounds.width(), startBounds.height())
.show(leash)
onTaskResizeAnimationListener.onAnimationStart(
- taskId,
- startTransaction,
- startBounds
+ taskId,
+ startTransaction,
+ startBounds
)
},
onEnd = {
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/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index 7da1b23dd5b1..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;
@@ -67,8 +66,8 @@ import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ExternalMainThread;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.annotations.ExternalMainThread;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
@@ -316,7 +315,8 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll
}
// TODO(b/290391688): Also update the session data with task stack changes
pd.dragSession = new DragSession(ActivityTaskManager.getInstance(),
- mDisplayController.getDisplayLayout(displayId), event.getClipData());
+ mDisplayController.getDisplayLayout(displayId), event.getClipData(),
+ event.getDragFlags());
pd.dragSession.update();
pd.activeDragCount++;
pd.dragLayout.prepare(pd.dragSession, mLogger.logStart(pd.dragSession));
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
index eb82da8a8e9f..a42ca1905ee7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
@@ -16,9 +16,9 @@
package com.android.wm.shell.draganddrop;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED;
-import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.content.ClipDescription.EXTRA_ACTIVITY_OPTIONS;
@@ -46,7 +46,6 @@ import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
-import android.content.ClipData;
import android.content.ClipDescription;
import android.content.Context;
import android.content.Intent;
@@ -264,13 +263,14 @@ public class DragAndDropPolicy {
final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT);
final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic();
baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true);
+ // Put BAL flags to avoid activity start aborted.
+ baseActivityOpts.setPendingIntentBackgroundActivityStartMode(
+ MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+ baseActivityOpts.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
final Bundle opts = baseActivityOpts.toBundle();
if (session.appData.hasExtra(EXTRA_ACTIVITY_OPTIONS)) {
opts.putAll(session.appData.getBundleExtra(EXTRA_ACTIVITY_OPTIONS));
}
- // Put BAL flags to avoid activity start aborted.
- opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true);
- opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true);
final UserHandle user = session.appData.getParcelableExtra(EXTRA_USER);
if (isTask) {
@@ -301,16 +301,14 @@ public class DragAndDropPolicy {
position);
final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic();
baseActivityOpts.setDisallowEnterPictureInPictureWhileLaunching(true);
+ baseActivityOpts.setPendingIntentBackgroundActivityStartMode(
+ MODE_BACKGROUND_ACTIVITY_START_DENIED);
// TODO(b/255649902): Rework this so that SplitScreenController can always use the options
// instead of a fillInIntent since it's assuming that the PendingIntent is mutable
baseActivityOpts.setPendingIntentLaunchFlags(FLAG_ACTIVITY_NEW_TASK
| FLAG_ACTIVITY_MULTIPLE_TASK);
final Bundle opts = baseActivityOpts.toBundle();
- // Put BAL flags to avoid activity start aborted.
- opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true);
- opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true);
-
mStarter.startIntent(session.launchableIntent,
session.launchableIntent.getCreatorUserHandle().getIdentifier(),
null /* fillIntent */, position, opts);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
index 59d696918448..4bb10dfdf8c6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java
@@ -22,11 +22,11 @@ import static android.content.pm.ActivityInfo.CONFIG_ASSETS_PATHS;
import static android.content.pm.ActivityInfo.CONFIG_UI_MODE;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
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 static com.android.wm.shell.common.split.SplitScreenUtils.getResizingBackgroundColor;
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_BOTTOM;
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_LEFT;
import static com.android.wm.shell.draganddrop.DragAndDropPolicy.Target.TYPE_SPLIT_RIGHT;
@@ -41,7 +41,6 @@ import android.app.StatusBarManager;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
-import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.Region;
@@ -278,7 +277,7 @@ public class DragLayout extends LinearLayout
final int activityType = taskInfo1.getActivityType();
if (activityType == ACTIVITY_TYPE_STANDARD) {
Drawable icon1 = mIconProvider.getIcon(taskInfo1.topActivityInfo);
- int bgColor1 = getResizingBackgroundColor(taskInfo1);
+ int bgColor1 = getResizingBackgroundColor(taskInfo1).toArgb();
mDropZoneView1.setAppInfo(bgColor1, icon1);
mDropZoneView2.setAppInfo(bgColor1, icon1);
updateDropZoneSizes(null, null); // passing null splits the views evenly
@@ -298,10 +297,10 @@ public class DragLayout extends LinearLayout
mSplitScreenController.getTaskInfo(SPLIT_POSITION_BOTTOM_OR_RIGHT);
if (topOrLeftTask != null && bottomOrRightTask != null) {
Drawable topOrLeftIcon = mIconProvider.getIcon(topOrLeftTask.topActivityInfo);
- int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask);
+ int topOrLeftColor = getResizingBackgroundColor(topOrLeftTask).toArgb();
Drawable bottomOrRightIcon = mIconProvider.getIcon(
bottomOrRightTask.topActivityInfo);
- int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask);
+ int bottomOrRightColor = getResizingBackgroundColor(bottomOrRightTask).toArgb();
mDropZoneView1.setAppInfo(topOrLeftColor, topOrLeftIcon);
mDropZoneView2.setAppInfo(bottomOrRightColor, bottomOrRightIcon);
}
@@ -556,11 +555,6 @@ public class DragLayout extends LinearLayout
}
}
- private static int getResizingBackgroundColor(ActivityManager.RunningTaskInfo taskInfo) {
- final int taskBgColor = taskInfo.taskDescription.getBackgroundColor();
- return Color.valueOf(taskBgColor == -1 ? Color.WHITE : taskBgColor).toArgb();
- }
-
/**
* Dumps information about this drag layout.
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java
index 8f1bc59af1ef..0addd432aff0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java
@@ -40,6 +40,7 @@ import java.util.List;
public class DragSession {
private final ActivityTaskManager mActivityTaskManager;
private final ClipData mInitialDragData;
+ private final int mInitialDragFlags;
final DisplayLayout displayLayout;
// The activity info associated with the activity in the appData or the launchableIntent
@@ -62,9 +63,10 @@ public class DragSession {
boolean dragItemSupportsSplitscreen;
DragSession(ActivityTaskManager activityTaskManager,
- DisplayLayout dispLayout, ClipData data) {
+ DisplayLayout dispLayout, ClipData data, int dragFlags) {
mActivityTaskManager = activityTaskManager;
mInitialDragData = data;
+ mInitialDragFlags = dragFlags;
displayLayout = dispLayout;
}
@@ -94,6 +96,6 @@ public class DragSession {
dragItemSupportsSplitscreen = activityInfo == null
|| ActivityInfo.isResizeableMode(activityInfo.resizeMode);
appData = mInitialDragData.getItemAt(0).getIntent();
- launchableIntent = DragUtils.getLaunchIntent(mInitialDragData);
+ launchableIntent = DragUtils.getLaunchIntent(mInitialDragData, mInitialDragFlags);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java
index 24f8e186bf76..e215870f1894 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragUtils.java
@@ -24,6 +24,7 @@ import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipDescription;
import android.view.DragEvent;
+import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -67,14 +68,18 @@ public class DragUtils {
*/
@Nullable
public static PendingIntent getLaunchIntent(@NonNull DragEvent dragEvent) {
- return getLaunchIntent(dragEvent.getClipData());
+ return getLaunchIntent(dragEvent.getClipData(), dragEvent.getDragFlags());
}
/**
* Returns a launchable intent in the given `ClipData` or `null` if there is none.
*/
@Nullable
- public static PendingIntent getLaunchIntent(@NonNull ClipData data) {
+ public static PendingIntent getLaunchIntent(@NonNull ClipData data, int dragFlags) {
+ if ((dragFlags & View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG) == 0) {
+ // Disallow launching the intent if the app does not want to delegate it to the system
+ return null;
+ }
for (int i = 0; i < data.getItemCount(); i++) {
final ClipData.Item item = data.getItemAt(i);
if (item.getIntentSender() != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt
index 8826141fb406..31214eba8dd0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/GlobalDragListener.kt
@@ -17,6 +17,8 @@ package com.android.wm.shell.draganddrop
import android.app.ActivityManager
import android.os.RemoteException
+import android.os.Trace
+import android.os.Trace.TRACE_TAG_WINDOW_MANAGER
import android.util.Log
import android.view.DragEvent
import android.view.IWindowManager
@@ -27,6 +29,7 @@ import com.android.internal.protolog.common.ProtoLog
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.protolog.ShellProtoLogGroup
import java.util.function.Consumer
+import kotlin.random.Random
/**
* Manages the listener and callbacks for unhandled global drags.
@@ -101,10 +104,15 @@ class GlobalDragListener(
@VisibleForTesting
fun onUnhandledDrop(dragEvent: DragEvent, wmCallback: IUnhandledDragCallback) {
+ val traceCookie = Random.nextInt()
+ Trace.asyncTraceBegin(TRACE_TAG_WINDOW_MANAGER, "GlobalDragListener.onUnhandledDrop",
+ traceCookie);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
"onUnhandledDrop: %s", dragEvent)
if (callback == null) {
wmCallback.notifyUnhandledDropComplete(false)
+ Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, "GlobalDragListener.onUnhandledDrop",
+ traceCookie);
return
}
@@ -112,6 +120,8 @@ class GlobalDragListener(
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
"Notifying onUnhandledDrop complete: %b", it)
wmCallback.notifyUnhandledDropComplete(it)
+ Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, "GlobalDragListener.onUnhandledDrop",
+ traceCookie);
}
}
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 f2bdcae31956..7d2aa275a684 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
@@ -21,14 +21,15 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM;
import android.app.ActivityManager.RunningTaskInfo;
+import android.content.Context;
import android.util.SparseArray;
import android.view.SurfaceControl;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.DesktopModeStatus;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
@@ -44,6 +45,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
ShellTaskOrganizer.FocusListener {
private static final String TAG = "FreeformTaskListener";
+ private final Context mContext;
private final ShellTaskOrganizer mShellTaskOrganizer;
private final Optional<DesktopModeTaskRepository> mDesktopModeTaskRepository;
private final WindowDecorViewModel mWindowDecorationViewModel;
@@ -56,10 +58,12 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
}
public FreeformTaskListener(
+ Context context,
ShellInit shellInit,
ShellTaskOrganizer shellTaskOrganizer,
Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
WindowDecorViewModel windowDecorationViewModel) {
+ mContext = context;
mShellTaskOrganizer = shellTaskOrganizer;
mWindowDecorationViewModel = windowDecorationViewModel;
mDesktopModeTaskRepository = desktopModeTaskRepository;
@@ -70,7 +74,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
private void onInit() {
mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FREEFORM);
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
mShellTaskOrganizer.addFocusListener(this);
}
}
@@ -92,9 +96,10 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
t.apply();
}
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
mDesktopModeTaskRepository.ifPresent(repository -> {
- repository.addOrMoveFreeformTaskToTop(taskInfo.taskId);
+ repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId);
+ repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
if (taskInfo.isVisible) {
if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
@@ -113,9 +118,10 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
taskInfo.taskId);
mTasks.remove(taskInfo.taskId);
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
mDesktopModeTaskRepository.ifPresent(repository -> {
- repository.removeFreeformTask(taskInfo.taskId);
+ repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId);
+ repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
if (repository.removeActiveTask(taskInfo.taskId)) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"Removing active freeform task: #%d", taskInfo.taskId);
@@ -123,7 +129,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
repository.updateVisibleFreeformTasks(taskInfo.displayId, taskInfo.taskId, false);
});
}
-
+ mWindowDecorationViewModel.onTaskVanished(taskInfo);
if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
mWindowDecorationViewModel.destroyWindowDecoration(taskInfo);
}
@@ -137,7 +143,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
taskInfo.taskId);
mWindowDecorationViewModel.onTaskInfoChanged(taskInfo);
state.mTaskInfo = taskInfo;
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
mDesktopModeTaskRepository.ifPresent(repository -> {
if (taskInfo.isVisible) {
if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) {
@@ -159,9 +165,10 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener,
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG,
"Freeform Task Focus Changed: #%d focused=%b",
taskInfo.taskId, taskInfo.isFocused);
- if (DesktopModeStatus.isEnabled() && taskInfo.isFocused) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext) && taskInfo.isFocused) {
mDesktopModeTaskRepository.ifPresent(repository -> {
- repository.addOrMoveFreeformTaskToTop(taskInfo.taskId);
+ repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId);
+ repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId);
});
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
index 998728d65e6a..2626e7380163 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
@@ -161,7 +161,7 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TASK_ORG, "Fullscreen Task Vanished: #%d",
taskInfo.taskId);
mTasks.remove(taskInfo.taskId);
-
+ mWindowDecorViewModelOptional.ifPresent(v -> v.onTaskVanished(taskInfo));
if (Transitions.ENABLE_SHELL_TRANSITIONS) return;
if (mWindowDecorViewModelOptional.isPresent()) {
mWindowDecorViewModelOptional.get().destroyWindowDecoration(taskInfo);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
index 73de231fb63a..cd478e5bd567 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
@@ -20,7 +20,9 @@ import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.service.dreams.Flags.dismissDreamOnKeyguardDismiss;
import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_OCCLUDING;
@@ -44,12 +46,15 @@ import android.window.IRemoteTransition;
import android.window.IRemoteTransitionFinishedCallback;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.common.TaskStackListenerCallback;
+import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.annotations.ExternalThread;
import com.android.wm.shell.sysui.KeyguardChangeListener;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -62,7 +67,8 @@ import com.android.wm.shell.transition.Transitions.TransitionFinishCallback;
* <p>This takes the highest priority.
*/
public class KeyguardTransitionHandler
- implements Transitions.TransitionHandler, KeyguardChangeListener {
+ implements Transitions.TransitionHandler, KeyguardChangeListener,
+ TaskStackListenerCallback {
private static final String TAG = "KeyguardTransition";
private final Transitions mTransitions;
@@ -71,12 +77,14 @@ public class KeyguardTransitionHandler
private final ShellExecutor mMainExecutor;
private final ArrayMap<IBinder, StartedTransition> mStartedTransitions = new ArrayMap<>();
+ private final TaskStackListenerImpl mTaskStackListener;
/**
* Local IRemoteTransition implementations registered by the keyguard service.
* @see KeyguardTransitions
*/
private IRemoteTransition mExitTransition = null;
+ private IRemoteTransition mAppearTransition = null;
private IRemoteTransition mOccludeTransition = null;
private IRemoteTransition mOccludeByDreamTransition = null;
private IRemoteTransition mUnoccludeTransition = null;
@@ -87,6 +95,8 @@ public class KeyguardTransitionHandler
// Last value reported by {@link KeyguardChangeListener}.
private boolean mKeyguardShowing = true;
+ @Nullable
+ private WindowContainerToken mDreamToken;
private final class StartedTransition {
final TransitionInfo mInfo;
@@ -105,18 +115,23 @@ public class KeyguardTransitionHandler
@NonNull ShellInit shellInit,
@NonNull ShellController shellController,
@NonNull Transitions transitions,
+ @NonNull TaskStackListenerImpl taskStackListener,
@NonNull Handler mainHandler,
@NonNull ShellExecutor mainExecutor) {
mTransitions = transitions;
mShellController = shellController;
mMainHandler = mainHandler;
mMainExecutor = mainExecutor;
+ mTaskStackListener = taskStackListener;
shellInit.addInitCallback(this::onInit, this);
}
private void onInit() {
mTransitions.addHandler(this);
mShellController.addKeyguardChangeListener(this);
+ if (dismissDreamOnKeyguardDismiss()) {
+ mTaskStackListener.addListener(this);
+ }
}
/**
@@ -128,6 +143,10 @@ public class KeyguardTransitionHandler
}
public static boolean handles(TransitionInfo info) {
+ // There is no animation for screen-wake unless we are immediately unlocking.
+ if (info.getType() == WindowManager.TRANSIT_WAKE && !info.isKeyguardGoingAway()) {
+ return false;
+ }
return (info.getFlags() & KEYGUARD_VISIBILITY_TRANSIT_FLAGS) != 0;
}
@@ -142,6 +161,11 @@ public class KeyguardTransitionHandler
}
@Override
+ public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
+ mDreamToken = taskInfo.getActivityType() == ACTIVITY_TYPE_DREAM ? taskInfo.token : null;
+ }
+
+ @Override
public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@@ -152,26 +176,28 @@ public class KeyguardTransitionHandler
// Choose a transition applicable for the changes and keyguard state.
if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) {
- return startAnimation(mExitTransition,
- "going-away",
+ return startAnimation(mExitTransition, "going-away",
+ transition, info, startTransaction, finishTransaction, finishCallback);
+ }
+
+ if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0) {
+ return startAnimation(mAppearTransition, "appearing",
transition, info, startTransaction, finishTransaction, finishCallback);
}
+
// Occlude/unocclude animations are only played if the keyguard is locked.
if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0) {
if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_OCCLUDING) != 0) {
if (hasOpeningDream(info)) {
- return startAnimation(mOccludeByDreamTransition,
- "occlude-by-dream",
+ return startAnimation(mOccludeByDreamTransition, "occlude-by-dream",
transition, info, startTransaction, finishTransaction, finishCallback);
} else {
- return startAnimation(mOccludeTransition,
- "occlude",
+ return startAnimation(mOccludeTransition, "occlude",
transition, info, startTransaction, finishTransaction, finishCallback);
}
} else if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) {
- return startAnimation(mUnoccludeTransition,
- "unocclude",
+ return startAnimation(mUnoccludeTransition, "unocclude",
transition, info, startTransaction, finishTransaction, finishCallback);
}
}
@@ -271,6 +297,13 @@ public class KeyguardTransitionHandler
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request) {
+ if (dismissDreamOnKeyguardDismiss()
+ && (request.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0
+ && mDreamToken != null) {
+ // Dismiss the dream in the same transaction, so that it isn't visible once the device
+ // is unlocked.
+ return new WindowContainerTransaction().removeTask(mDreamToken);
+ }
return null;
}
@@ -334,11 +367,13 @@ public class KeyguardTransitionHandler
@Override
public void register(
IRemoteTransition exitTransition,
+ IRemoteTransition appearTransition,
IRemoteTransition occludeTransition,
IRemoteTransition occludeByDreamTransition,
IRemoteTransition unoccludeTransition) {
mMainExecutor.execute(() -> {
mExitTransition = exitTransition;
+ mAppearTransition = appearTransition;
mOccludeTransition = occludeTransition;
mOccludeByDreamTransition = occludeByDreamTransition;
mUnoccludeTransition = unoccludeTransition;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
index 33c299f0b161..b7245b91f36c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
@@ -19,7 +19,7 @@ package com.android.wm.shell.keyguard;
import android.annotation.NonNull;
import android.window.IRemoteTransition;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
/**
* Interface exposed to SystemUI Keyguard to register handlers for running
@@ -35,6 +35,7 @@ public interface KeyguardTransitions {
*/
default void register(
@NonNull IRemoteTransition unlockTransition,
+ @NonNull IRemoteTransition appearTransition,
@NonNull IRemoteTransition occludeTransition,
@NonNull IRemoteTransition occludeByDreamTransition,
@NonNull IRemoteTransition unoccludeTransition) {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
index 2ee334873780..b000e3228b9a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
@@ -18,7 +18,7 @@ package com.android.wm.shell.onehanded;
import android.os.SystemProperties;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
/**
* Interface to engage one handed feature.
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 679d4ca2ac48..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;
@@ -55,7 +54,7 @@ import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TaskStackListenerCallback;
import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.KeyguardChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index a9aa6badcfe2..a749019046f8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -18,7 +18,7 @@ package com.android.wm.shell.pip;
import android.graphics.Rect;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
import java.util.function.Consumer;
@@ -39,7 +39,7 @@ public interface Pip {
* @param isSysUiStateValid Is SysUI state valid or not.
* @param flag Current SysUI state.
*/
- default void onSystemUiStateChanged(boolean isSysUiStateValid, int flag) {
+ default void onSystemUiStateChanged(boolean isSysUiStateValid, long flag) {
}
/**
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 4c477373c32c..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);
@@ -743,11 +752,6 @@ public class PipAnimationController {
.alpha(tx, leash, 1f)
.round(tx, leash, shouldApplyCornerRadius())
.shadow(tx, leash, shouldApplyShadowRadius());
- // TODO(b/178632364): this is a work around for the black background when
- // entering PiP in button navigation mode.
- if (isInPipDirection(direction)) {
- tx.setWindowCrop(leash, getStartValue());
- }
tx.show(leash);
tx.apply();
}
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..202f60dad842 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
@@ -137,7 +137,7 @@ public class PipSurfaceTransactionHelper {
mTmpDestinationRect.inset(insets);
// Scale to the bounds no smaller than the destination and offset such that the top/left
// of the scaled inset source rect aligns with the top/left of the destination bounds
- final float scale;
+ final float scale, left, top;
if (isInPipDirection
&& sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) {
// scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only.
@@ -148,12 +148,15 @@ public class PipSurfaceTransactionHelper {
? (float) destinationBounds.width() / sourceBounds.width()
: (float) destinationBounds.height() / sourceBounds.height();
scale = (1 - fraction) * startScale + fraction * endScale;
+ left = destinationBounds.left - insets.left * scale;
+ top = destinationBounds.top - insets.top * scale;
} else {
scale = Math.max((float) destinationBounds.width() / sourceBounds.width(),
(float) destinationBounds.height() / sourceBounds.height());
+ // Work around the rounding error by fix the position at very beginning.
+ left = scale == 1 ? 0 : destinationBounds.left - insets.left * scale;
+ top = scale == 1 ? 0 : destinationBounds.top - insets.top * scale;
}
- final float left = destinationBounds.left - insets.left * scale;
- final float top = destinationBounds.top - insets.top * scale;
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 bd186ba22588..e4420d73886e 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
@@ -82,7 +82,6 @@ import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ScreenshotUtils;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
import com.android.wm.shell.common.pip.PipDisplayLayoutState;
@@ -92,10 +91,12 @@ import com.android.wm.shell.common.pip.PipUiEventLogger;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.pip.phone.PipMotionHelper;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
@@ -372,6 +373,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,
@@ -503,7 +508,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
@@ -512,6 +517,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
@@ -522,7 +528,39 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, t);
t.setLayer(overlay, Integer.MAX_VALUE);
t.apply();
+ // This serves as a last resort in case the Shell Transition is not handled properly.
+ // We want to make sure the overlay passed from Launcher gets removed eventually.
+ mayRemoveContentOverlay(overlay);
+ }
+ }
+
+ /**
+ * 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
+ + CONTENT_OVERLAY_FADE_OUT_DELAY_MS
+ + EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS) * 2L;
+ mMainExecutor.executeDelayed(() -> {
+ final SurfaceControl overlayLeash = overlayRef.get();
+ if (overlayLeash != null && overlayLeash.isValid() && overlayLeash == mPipOverlay) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "Cleanup the overlay(%s) as a last resort.", overlayLeash);
+ removeContentOverlay(overlayLeash, null /* callback */);
+ }
+ }, timeoutDuration);
}
/**
@@ -578,6 +616,19 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
return;
}
+ if (mPipTransitionState.isEnteringPip()
+ && !mPipTransitionState.getInSwipePipToHomeTransition()) {
+ // If we are still entering PiP with Shell playing enter animation, jump-cut to
+ // 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;
+ }
+
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"exitPip: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState);
final WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -824,7 +875,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
mPipUiEventLoggerLogger.log(uiEventEnum);
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "onTaskAppeared: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState);
+ "onTaskAppeared: %s, state=%s, taskId=%s", mTaskInfo.topActivity,
+ mPipTransitionState, mTaskInfo.taskId);
if (mPipTransitionState.getInSwipePipToHomeTransition()) {
if (!mWaitForFixedRotation) {
onEndOfSwipePipToHomeTransition();
@@ -947,7 +999,6 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
private void onEndOfSwipePipToHomeTransition() {
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- mPipTransitionController.setEnterAnimationType(ANIM_TYPE_BOUNDS);
return;
}
@@ -1957,6 +2008,8 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
}
if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) {
// Avoid double removal, which is fatal.
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: trying to remove overlay (%s) while in UNDEFINED state", TAG, surface);
return;
}
if (surface == null || !surface.isValid()) {
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 6a1a62ea30a1..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
@@ -27,6 +27,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_PIP;
import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.view.WindowManager.transitTypeToString;
import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
@@ -42,7 +43,6 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT;
import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
-import android.animation.Animator;
import android.annotation.IntDef;
import android.app.ActivityManager;
import android.app.TaskInfo;
@@ -75,6 +75,7 @@ import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.CounterRotatorHelper;
+import com.android.wm.shell.transition.HomeTransitionObserver;
import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
@@ -107,6 +108,7 @@ public class PipTransition extends PipTransitionController {
private final PipDisplayLayoutState mPipDisplayLayoutState;
private final int mEnterExitAnimationDuration;
private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
+ private final HomeTransitionObserver mHomeTransitionObserver;
private final Optional<SplitScreenController> mSplitScreenOptional;
private final PipAnimationController mPipAnimationController;
private @PipAnimationController.AnimationType int mEnterAnimationType = ANIM_TYPE_BOUNDS;
@@ -164,6 +166,7 @@ public class PipTransition extends PipTransitionController {
PipBoundsAlgorithm pipBoundsAlgorithm,
PipAnimationController pipAnimationController,
PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
+ HomeTransitionObserver homeTransitionObserver,
Optional<SplitScreenController> splitScreenOptional) {
super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
pipBoundsAlgorithm);
@@ -174,6 +177,7 @@ public class PipTransition extends PipTransitionController {
mEnterExitAnimationDuration = context.getResources()
.getInteger(R.integer.config_pipResizeAnimationDuration);
mSurfaceTransactionHelper = pipSurfaceTransactionHelper;
+ mHomeTransitionObserver = homeTransitionObserver;
mSplitScreenOptional = splitScreenOptional;
}
@@ -279,6 +283,12 @@ public class PipTransition extends PipTransitionController {
// Entering PIP.
if (isEnteringPip(info)) {
+ if (handleEnteringPipWithDisplayChange(transition, info, startTransaction,
+ finishTransaction, finishCallback)) {
+ // The destination position is applied directly and let default transition handler
+ // run the display change animation.
+ return true;
+ }
startEnterAnimation(info, startTransaction, finishTransaction, finishCallback);
return true;
}
@@ -293,6 +303,25 @@ public class PipTransition extends PipTransitionController {
return false;
}
+ private boolean handleEnteringPipWithDisplayChange(@NonNull IBinder transition,
+ @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT,
+ @NonNull SurfaceControl.Transaction finishT,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ if (mFixedRotationState != FIXED_ROTATION_UNDEFINED
+ || !TransitionUtil.hasDisplayChange(info)) {
+ return false;
+ }
+ final TransitionInfo.Change pipChange = getPipChange(info);
+ if (pipChange == null) {
+ return false;
+ }
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: handle entering PiP with display change", TAG);
+ mMixedHandler.animateEnteringPipWithDisplayChange(transition, info, pipChange,
+ startT, finishT, finishCallback);
+ return true;
+ }
+
@Override
public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
@@ -347,9 +376,16 @@ public class PipTransition extends PipTransitionController {
@Override
public void end() {
- Animator animator = mPipAnimationController.getCurrentAnimator();
- if (animator != null && animator.isRunning()) {
- animator.end();
+ end(null);
+ }
+
+ @Override
+ public void end(@Nullable Runnable onTransitionEnd) {
+ if (mPipAnimationController.isAnimating()) {
+ mPipAnimationController.getCurrentAnimator().end();
+ }
+ if (onTransitionEnd != null) {
+ onTransitionEnd.run();
}
}
@@ -450,6 +486,14 @@ public class PipTransition extends PipTransitionController {
// activity windowing mode, and set the task bounds to the final bounds
wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
wct.setBounds(taskInfo.token, destinationBounds);
+ // If the animation is only used to apply destination bounds immediately and
+ // invisibly, then reshow it until the pip is drawn with the bounds.
+ final PipAnimationController.PipTransitionAnimator<?> animator =
+ mPipAnimationController.getCurrentAnimator();
+ if (animator != null && animator.getEndValue().equals(0f)) {
+ tx.addTransactionCommittedListener(mTransitions.getMainExecutor(),
+ () -> fadeExistingPip(true /* show */));
+ }
} else {
wct.setBounds(taskInfo.token, null /* bounds */);
}
@@ -612,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);
@@ -636,6 +683,7 @@ public class PipTransition extends PipTransitionController {
.setContainerLayer()
.setHidden(false)
.setParent(root.getLeash())
+ .setCallsite("PipTransition.startExitAnimation")
.build();
startTransaction.reparent(activitySurface, pipLeash);
// Put the activity at local position with offset in case it is letterboxed.
@@ -817,8 +865,11 @@ public class PipTransition extends PipTransitionController {
@NonNull Transitions.TransitionFinishCallback finishCallback,
@NonNull TaskInfo taskInfo) {
startTransaction.apply();
- finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(),
- mPipDisplayLayoutState.getDisplayBounds());
+ final TransitionInfo.Change pipChange = findCurrentPipTaskChange(info);
+ if (pipChange == null) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "removePipImmediately is called without pip change");
+ }
mPipOrganizer.onExitPipFinished(taskInfo);
finishCallback.onTransitionFinished(null);
}
@@ -840,8 +891,11 @@ public class PipTransition extends PipTransitionController {
&& change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED
&& !change.getContainer().equals(mCurrentPipTaskToken)) {
// We support TRANSIT_PIP type (from RootWindowContainer) or TRANSIT_OPEN (from apps
- // that enter PiP instantly on opening, mostly from CTS/Flicker tests)
- if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN) {
+ // that enter PiP instantly on opening, mostly from CTS/Flicker tests).
+ // TRANSIT_TO_FRONT, though uncommon with triggering PiP, should semantically also
+ // be allowed to animate if the task in question is pinned already - see b/308054074.
+ if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN
+ || transitType == TRANSIT_TO_FRONT) {
return true;
}
// This can happen if the request to enter PIP happens when we are collecting for
@@ -862,19 +916,24 @@ public class PipTransition extends PipTransitionController {
mEnterAnimationType = type;
}
- private void startEnterAnimation(@NonNull TransitionInfo info,
- @NonNull SurfaceControl.Transaction startTransaction,
- @NonNull SurfaceControl.Transaction finishTransaction,
- @NonNull Transitions.TransitionFinishCallback finishCallback) {
- // Search for an Enter PiP transition
- TransitionInfo.Change enterPip = null;
+ @Nullable
+ private static TransitionInfo.Change getPipChange(@NonNull TransitionInfo info) {
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
if (change.getTaskInfo() != null
&& change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) {
- enterPip = change;
+ return change;
}
}
+ return null;
+ }
+
+ private void startEnterAnimation(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ // Search for an Enter PiP transition
+ final TransitionInfo.Change enterPip = getPipChange(info);
if (enterPip == null) {
throw new IllegalStateException("Trying to start PiP animation without a pip"
+ "participant");
@@ -945,11 +1004,14 @@ 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
- && mFixedRotationState == FIXED_ROTATION_TRANSITION) {
- // Need to get the bounds of new rotation in old rotation for fixed rotation,
+ && endRotation != mPipDisplayLayoutState.getRotation()) {
+ // Computes the destination bounds in new rotation.
computeEnterPipRotatedBounds(rotationDelta, startRotation, endRotation, taskInfo,
destinationBounds, sourceHintRect);
}
@@ -975,6 +1037,7 @@ public class PipTransition extends PipTransitionController {
}
startTransaction.apply();
+ int animationDuration = mEnterExitAnimationDuration;
PipAnimationController.PipTransitionAnimator animator;
if (enterAnimationType == ANIM_TYPE_BOUNDS) {
animator = mPipAnimationController.getAnimator(taskInfo, leash, currentBounds,
@@ -1006,18 +1069,29 @@ public class PipTransition extends PipTransitionController {
}
}
} else if (enterAnimationType == ANIM_TYPE_ALPHA) {
+ // In case augmentRequest() is unable to apply the entering bounds (e.g. the request
+ // info only contains display change), keep the animation invisible (alpha 0) and
+ // duration 0 to apply the destination bounds. The actual fade-in animation will be
+ // done in onFinishResize() after the bounds are applied.
+ final boolean fadeInAfterOnFinishResize = rotationDelta != Surface.ROTATION_0
+ && mFixedRotationState == FIXED_ROTATION_CALLBACK;
animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds,
- 0f, 1f);
+ 0f, fadeInAfterOnFinishResize ? 0f : 1f);
+ if (fadeInAfterOnFinishResize) {
+ animationDuration = 0;
+ }
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);
}
mPipOrganizer.setContentOverlay(animator.getContentOverlayLeash(), currentBounds);
animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP)
.setPipAnimationCallback(mPipAnimationCallback)
- .setDuration(mEnterExitAnimationDuration);
+ .setDuration(animationDuration);
if (rotationDelta != Surface.ROTATION_0
&& mFixedRotationState == FIXED_ROTATION_TRANSITION) {
// For fixed rotation, the animation destination bounds is in old rotation coordinates.
@@ -1041,8 +1115,11 @@ public class PipTransition extends PipTransitionController {
final Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds();
outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds());
- // Transform the destination bounds to current display coordinates.
- rotateBounds(outDestinationBounds, displayBounds, endRotation, startRotation);
+ if (mFixedRotationState == FIXED_ROTATION_TRANSITION) {
+ // Transform the destination bounds to current display coordinates.
+ // With fixed rotation, the bounds of new rotation shows in old rotation.
+ rotateBounds(outDestinationBounds, displayBounds, endRotation, startRotation);
+ }
// When entering PiP (from button navigation mode), adjust the source rect hint by
// display cutout if applicable.
if (outSourceHintRect != null && taskInfo.displayCutoutInsets != null) {
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 32442f740a52..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
@@ -49,12 +49,12 @@ import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.common.split.SplitScreenUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.DefaultMixedHandler;
import com.android.wm.shell.transition.Transitions;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
-import java.util.function.Consumer;
/**
* Responsible supplying PiP Transitions.
@@ -68,6 +68,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH
protected final Transitions mTransitions;
private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>();
protected PipTaskOrganizer mPipOrganizer;
+ protected DefaultMixedHandler mMixedHandler;
protected final PipAnimationController.PipAnimationCallback mPipAnimationCallback =
new PipAnimationController.PipAnimationCallback() {
@@ -125,12 +126,8 @@ public abstract class PipTransitionController implements Transitions.TransitionH
/**
* Called when the Shell wants to start resizing Pip transition/animation.
- *
- * @param onFinishResizeCallback callback guaranteed to execute when animation ends and
- * client completes any potential draws upon WM state updates.
*/
- public void startResizeTransition(WindowContainerTransaction wct,
- Consumer<Rect> onFinishResizeCallback) {
+ public void startResizeTransition(WindowContainerTransaction wct) {
// Default implementation does nothing.
}
@@ -173,6 +170,14 @@ public abstract class PipTransitionController implements Transitions.TransitionH
mPipOrganizer = pto;
}
+ public void setMixedHandler(DefaultMixedHandler mixedHandler) {
+ mMixedHandler = mixedHandler;
+ }
+
+ public void applyTransaction(WindowContainerTransaction wct) {
+ mShellTaskOrganizer.applyTransaction(wct);
+ }
+
/**
* Registers {@link PipTransitionCallback} to receive transition callbacks.
*/
@@ -266,9 +271,9 @@ public abstract class PipTransitionController implements Transitions.TransitionH
}
/** Whether a particular package is same as current pip package. */
- public boolean isInPipPackage(String packageName) {
+ public boolean isPackageActiveInPip(String packageName) {
final TaskInfo inPipTask = mPipOrganizer.getTaskInfo();
- return packageName != null && inPipTask != null
+ return packageName != null && inPipTask != null && mPipOrganizer.isInPip()
&& packageName.equals(SplitScreenUtils.getPackageName(inPipTask.baseIntent));
}
@@ -305,6 +310,22 @@ public abstract class PipTransitionController implements Transitions.TransitionH
public void end() {
}
+ /**
+ * 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.
+ */
+ public void end(@Nullable Runnable onTransitionEnd) {
+ }
+
/** Starts the {@link android.window.SystemPerformanceHinter.HighPerfSession}. */
public void startHighPerfSession() {}
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 84afed18b8d4..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;
@@ -122,6 +121,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb
private static final long PIP_KEEP_CLEAR_AREAS_DELAY =
SystemProperties.getLong("persist.wm.debug.pip_keep_clear_areas_delay", 200);
+ private static final long ENABLE_TOUCH_DELAY_MS = 200L;
+
private Context mContext;
protected ShellExecutor mMainExecutor;
private DisplayController mDisplayController;
@@ -152,6 +153,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb
private final Runnable mMovePipInResponseToKeepClearAreasChangeCallback =
this::onKeepClearAreasChangedCallback;
+ private final Runnable mEnableTouchCallback = () -> mTouchHandler.setTouchEnabled(true);
+
private void onKeepClearAreasChangedCallback() {
if (mIsKeyguardShowingOrAnimating) {
// early bail out if the change was caused by keyguard showing up
@@ -843,7 +846,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
}
- private void onSystemUiStateChanged(boolean isValidState, int flag) {
+ private void onSystemUiStateChanged(boolean isValidState, long flag) {
mTouchHandler.onSystemUiStateChanged(isValidState);
}
@@ -998,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) {
@@ -1043,6 +1046,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
saveReentryState(pipBounds);
}
// Disable touches while the animation is running
+ mMainExecutor.removeCallbacks(mEnableTouchCallback);
mTouchHandler.setTouchEnabled(false);
if (mPinnedStackAnimationRecentsCallback != null) {
mPinnedStackAnimationRecentsCallback.onPipAnimationStarted();
@@ -1073,7 +1077,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
InteractionJankMonitor.getInstance().end(CUJ_PIP_TRANSITION);
// Re-enable touches after the animation completes
- mTouchHandler.setTouchEnabled(true);
+ mMainExecutor.executeDelayed(mEnableTouchCallback, ENABLE_TOUCH_DELAY_MS);
mTouchHandler.onPinnedStackAnimationEnded(direction);
}
@@ -1190,7 +1194,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
@Override
- public void onSystemUiStateChanged(boolean isSysUiStateValid, int flag) {
+ public void onSystemUiStateChanged(boolean isSysUiStateValid, long flag) {
mMainExecutor.execute(() -> {
PipController.this.onSystemUiStateChanged(isSysUiStateValid, flag);
});
@@ -1287,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/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index df67707e2014..ef468434db6a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -37,7 +37,6 @@ import android.os.Debug;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.R;
import com.android.wm.shell.animation.FloatProperties;
-import com.android.wm.shell.animation.PhysicsAnimator;
import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
import com.android.wm.shell.common.pip.PipAppOpsListener;
@@ -47,6 +46,7 @@ import com.android.wm.shell.common.pip.PipSnapAlgorithm;
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index c1adfffce074..d8ac8e948a97 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -219,6 +219,7 @@ public class PipTouchHandler {
mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(),
this::onAccessibilityShowMenu, this::updateMovementBounds,
this::animateToUnStashedState, mainExecutor);
+ mPipBoundsState.addOnAspectRatioChangedCallback(this::updateMinMaxSize);
// TODO(b/181599115): This should really be initializes as part of the pip controller, but
// until all PIP implementations derive from the controller, just initialize the touch handler
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index 62156fc7443b..6b5bdd2299e1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -64,6 +64,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
private TvPipBackgroundView mPipBackgroundView;
private boolean mIsReloading;
+ private static final int PIP_MENU_FORCE_CLOSE_DELAY_MS = 10_000;
+ private final Runnable mClosePipMenuRunnable = this::closeMenu;
@TvPipMenuMode
private int mCurrentMenuMode = MODE_NO_MENU;
@@ -280,6 +282,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: closeMenu()", TAG);
requestMenuMode(MODE_NO_MENU);
+ mMainHandler.removeCallbacks(mClosePipMenuRunnable);
}
@Override
@@ -488,13 +491,17 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
private void requestMenuMode(@TvPipMenuMode int menuMode) {
if (isMenuOpen() == isMenuOpen(menuMode)) {
+ if (mMainHandler.hasCallbacks(mClosePipMenuRunnable)) {
+ mMainHandler.removeCallbacks(mClosePipMenuRunnable);
+ mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS);
+ }
// No need to request a focus change. We can directly switch to the new mode.
switchToMenuMode(menuMode);
} else {
if (isMenuOpen(menuMode)) {
+ mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS);
mMenuModeOnFocus = menuMode;
}
-
// Send a request to gain window focus if the menu is open, or lose window focus
// otherwise. Once the focus change happens, we will request the new mode in the
// callback {@link #onPipWindowFocusChanged}.
@@ -584,6 +591,14 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis
}
@Override
+ public void onUserInteracting() {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: onUserInteracting - mCurrentMenuMode=%s", TAG, getMenuModeString());
+ mMainHandler.removeCallbacks(mClosePipMenuRunnable);
+ mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS);
+
+ }
+ @Override
public void onPipMovement(int keycode) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipMovement - mCurrentMenuMode=%s", TAG, getMenuModeString());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
index b259e8d584a6..4a767ef2a113 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java
@@ -491,30 +491,33 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getAction() == ACTION_UP) {
-
if (event.getKeyCode() == KEYCODE_BACK) {
mListener.onExitCurrentMenuMode();
return true;
}
-
- if (mCurrentMenuMode == MODE_MOVE_MENU && !mA11yManager.isEnabled()) {
- switch (event.getKeyCode()) {
- case KEYCODE_DPAD_UP:
- case KEYCODE_DPAD_DOWN:
- case KEYCODE_DPAD_LEFT:
- case KEYCODE_DPAD_RIGHT:
+ switch (event.getKeyCode()) {
+ case KEYCODE_DPAD_UP:
+ case KEYCODE_DPAD_DOWN:
+ case KEYCODE_DPAD_LEFT:
+ case KEYCODE_DPAD_RIGHT:
+ mListener.onUserInteracting();
+ if (mCurrentMenuMode == MODE_MOVE_MENU && !mA11yManager.isEnabled()) {
mListener.onPipMovement(event.getKeyCode());
return true;
- case KEYCODE_ENTER:
- case KEYCODE_DPAD_CENTER:
+ }
+ break;
+ case KEYCODE_ENTER:
+ case KEYCODE_DPAD_CENTER:
+ mListener.onUserInteracting();
+ if (mCurrentMenuMode == MODE_MOVE_MENU && !mA11yManager.isEnabled()) {
mListener.onExitCurrentMenuMode();
return true;
- default:
- // Dispatch key event as normal below
- }
+ }
+ break;
+ default:
+ // Dispatch key event as normal below
}
}
-
return super.dispatchKeyEvent(event);
}
@@ -637,6 +640,11 @@ public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.L
interface Listener {
/**
+ * Called when any button (that affects the menu) on current menu mode was pressed.
+ */
+ void onUserInteracting();
+
+ /**
* Called when a button for exiting the current menu mode was pressed.
*/
void onExitCurrentMenuMode();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
index c2f4d72a1ddf..ca0d61f8fc9b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
@@ -233,6 +233,7 @@ public class TvPipTransition extends PipTransitionController {
.setContainerLayer()
.setHidden(false)
.setParent(root.getLeash())
+ .setCallsite("TvPipTransition.startAnimation")
.build();
startTransaction.reparent(activitySurface, pipLeash);
// Put the activity at local position with offset in case it is letterboxed.
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 e73a85003881..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
@@ -16,29 +16,39 @@
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;
import android.app.PictureInPictureParams;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
+import android.os.Bundle;
import android.view.InsetsState;
import android.view.SurfaceControl;
import androidx.annotation.BinderThread;
+import androidx.annotation.Nullable;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.internal.util.Preconditions;
+import com.android.wm.shell.R;
+import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ExternalInterfaceBinder;
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.TaskStackListenerCallback;
+import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.pip.IPip;
import com.android.wm.shell.common.pip.IPipAnimationListener;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
@@ -47,28 +57,64 @@ import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import java.io.PrintWriter;
+
/**
* Manages the picture-in-picture (PIP) UI and states for Phones.
*/
public class PipController implements ConfigurationChangeListener,
+ PipTransitionState.PipTransitionStateChangedListener,
DisplayController.OnDisplaysChangedListener, RemoteCallable<PipController> {
private static final String TAG = PipController.class.getSimpleName();
+ private static final String SWIPE_TO_PIP_APP_BOUNDS = "pip_app_bounds";
+ private static final String SWIPE_TO_PIP_OVERLAY = "swipe_to_pip_overlay";
+
+ private final Context mContext;
+ private final ShellCommandHandler mShellCommandHandler;
+ private final ShellController mShellController;
+ private final DisplayController mDisplayController;
+ private final DisplayInsetsController mDisplayInsetsController;
+ private final PipBoundsState mPipBoundsState;
+ private final PipBoundsAlgorithm mPipBoundsAlgorithm;
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
+ private final PipScheduler mPipScheduler;
+ private final TaskStackListenerImpl mTaskStackListener;
+ private final ShellTaskOrganizer mShellTaskOrganizer;
+ private final PipTransitionState mPipTransitionState;
+ private final ShellExecutor mMainExecutor;
+
+ // Wrapper for making Binder calls into PiP animation listener hosted in launcher's Recents.
+ private PipAnimationListener mPipRecentsAnimationListener;
+
+ @VisibleForTesting
+ interface PipAnimationListener {
+ /**
+ * Notifies the listener that the Pip animation is started.
+ */
+ void onPipAnimationStarted();
+
+ /**
+ * Notifies the listener about PiP resource dimensions changed.
+ * Listener can expect an immediate callback the first time they attach.
+ *
+ * @param cornerRadius the pixel value of the corner radius, zero means it's disabled.
+ * @param shadowRadius the pixel value of the shadow radius, zero means it's disabled.
+ */
+ void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius);
- private Context mContext;
- private ShellController mShellController;
- private DisplayController mDisplayController;
- private DisplayInsetsController mDisplayInsetsController;
- private PipBoundsState mPipBoundsState;
- private PipBoundsAlgorithm mPipBoundsAlgorithm;
- private PipDisplayLayoutState mPipDisplayLayoutState;
- private PipScheduler mPipScheduler;
- private ShellExecutor mMainExecutor;
+ /**
+ * Notifies the listener that user leaves PiP by tapping on the expand button.
+ */
+ void onExpandPip();
+ }
private PipController(Context context,
ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
ShellController shellController,
DisplayController displayController,
DisplayInsetsController displayInsetsController,
@@ -76,8 +122,12 @@ public class PipController implements ConfigurationChangeListener,
PipBoundsAlgorithm pipBoundsAlgorithm,
PipDisplayLayoutState pipDisplayLayoutState,
PipScheduler pipScheduler,
+ TaskStackListenerImpl taskStackListener,
+ ShellTaskOrganizer shellTaskOrganizer,
+ PipTransitionState pipTransitionState,
ShellExecutor mainExecutor) {
mContext = context;
+ mShellCommandHandler = shellCommandHandler;
mShellController = shellController;
mDisplayController = displayController;
mDisplayInsetsController = displayInsetsController;
@@ -85,6 +135,10 @@ public class PipController implements ConfigurationChangeListener,
mPipBoundsAlgorithm = pipBoundsAlgorithm;
mPipDisplayLayoutState = pipDisplayLayoutState;
mPipScheduler = pipScheduler;
+ mTaskStackListener = taskStackListener;
+ mShellTaskOrganizer = shellTaskOrganizer;
+ mPipTransitionState = pipTransitionState;
+ mPipTransitionState.addPipTransitionStateChangedListener(this);
mMainExecutor = mainExecutor;
if (PipUtils.isPip2ExperimentEnabled()) {
@@ -92,24 +146,42 @@ public class PipController implements ConfigurationChangeListener,
}
}
- @Override
- public Context getContext() {
- return mContext;
- }
-
- @Override
- public ShellExecutor getRemoteCallExecutor() {
- return mMainExecutor;
+ /**
+ * Instantiates {@link PipController}, returns {@code null} if the feature not supported.
+ */
+ public static PipController create(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ ShellController shellController,
+ DisplayController displayController,
+ DisplayInsetsController displayInsetsController,
+ PipBoundsState pipBoundsState,
+ PipBoundsAlgorithm pipBoundsAlgorithm,
+ PipDisplayLayoutState pipDisplayLayoutState,
+ PipScheduler pipScheduler,
+ TaskStackListenerImpl taskStackListener,
+ ShellTaskOrganizer shellTaskOrganizer,
+ PipTransitionState pipTransitionState,
+ ShellExecutor mainExecutor) {
+ if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Device doesn't support Pip feature", TAG);
+ return null;
+ }
+ return new PipController(context, shellInit, shellCommandHandler, shellController,
+ displayController, displayInsetsController, pipBoundsState, pipBoundsAlgorithm,
+ pipDisplayLayoutState, pipScheduler, taskStackListener, shellTaskOrganizer,
+ pipTransitionState, mainExecutor);
}
private void onInit() {
+ mShellCommandHandler.addDumpCallback(this::dump, this);
// Ensure that we have the display info in case we get calls to update the bounds before the
// listener calls back
mPipDisplayLayoutState.setDisplayId(mContext.getDisplayId());
DisplayLayout layout = new DisplayLayout(mContext, mContext.getDisplay());
mPipDisplayLayoutState.setDisplayLayout(layout);
- mShellController.addConfigurationChangeListener(this);
mDisplayController.addDisplayWindowListener(this);
mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(),
new DisplayInsetsController.OnInsetsChangedListener() {
@@ -123,45 +195,61 @@ public class PipController implements ConfigurationChangeListener,
// Allow other outside processes to bind to PiP controller using the key below.
mShellController.addExternalInterface(KEY_EXTRA_SHELL_PIP,
this::createExternalInterface, this);
- }
+ mShellController.addConfigurationChangeListener(this);
- /**
- * Instantiates {@link PipController}, returns {@code null} if the feature not supported.
- */
- public static PipController create(Context context,
- ShellInit shellInit,
- ShellController shellController,
- DisplayController displayController,
- DisplayInsetsController displayInsetsController,
- PipBoundsState pipBoundsState,
- PipBoundsAlgorithm pipBoundsAlgorithm,
- PipDisplayLayoutState pipDisplayLayoutState,
- PipScheduler pipScheduler,
- ShellExecutor mainExecutor) {
- if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
- ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: Device doesn't support Pip feature", TAG);
- return null;
- }
- return new PipController(context, shellInit, shellController, displayController,
- displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState,
- pipScheduler, mainExecutor);
+ mTaskStackListener.addListener(new TaskStackListenerCallback() {
+ @Override
+ public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
+ boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
+ if (task.getWindowingMode() != WINDOWING_MODE_PINNED) {
+ return;
+ }
+ mPipScheduler.scheduleExitPipViaExpand();
+ }
+ });
}
private ExternalInterfaceBinder createExternalInterface() {
return new IPipImpl(this);
}
+ //
+ // RemoteCallable implementations
+ //
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Override
+ public ShellExecutor getRemoteCallExecutor() {
+ return mMainExecutor;
+ }
+
+ //
+ // ConfigurationChangeListener implementations
+ //
+
@Override
public void onConfigurationChanged(Configuration newConfiguration) {
mPipDisplayLayoutState.onConfigurationChanged();
}
@Override
+ public void onDensityOrFontScaleChanged() {
+ onPipResourceDimensionsChanged();
+ }
+
+ @Override
public void onThemeChanged() {
onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay()));
}
+ //
+ // DisplayController.OnDisplaysChangedListener implementations
+ //
+
@Override
public void onDisplayAdded(int displayId) {
if (displayId != mPipDisplayLayoutState.getDisplayId()) {
@@ -182,6 +270,10 @@ public class PipController implements ConfigurationChangeListener,
mPipDisplayLayoutState.setDisplayLayout(layout);
}
+ //
+ // IPip Binder stub helpers
+ //
+
private Rect getSwipePipToHomeBounds(ComponentName componentName, ActivityInfo activityInfo,
PictureInPictureParams pictureInPictureParams,
int launcherRotation, Rect hotseatKeepClearArea) {
@@ -193,11 +285,73 @@ 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);
- mPipScheduler.setInSwipePipToHomeTransition(true);
- // TODO: cache the overlay if provided for reparenting later.
+ Bundle extra = new Bundle();
+ extra.putParcelable(SWIPE_TO_PIP_OVERLAY, overlay);
+ extra.putParcelable(SWIPE_TO_PIP_APP_BOUNDS, appBounds);
+ mPipTransitionState.setState(PipTransitionState.SWIPING_TO_PIP, extra);
+ if (overlay != null) {
+ // Shell transitions might use a root animation leash, which will be removed when
+ // the Recents transition is finished. Launcher attaches the overlay leash to this
+ // animation target leash; thus, we need to reparent it to the actual Task surface now.
+ // PipTransition is responsible to fade it out and cleanup when finishing the enter PIP
+ // transition.
+ SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+ mShellTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, tx);
+ tx.setLayer(overlay, Integer.MAX_VALUE);
+ tx.apply();
+ }
+ mPipRecentsAnimationListener.onPipAnimationStarted();
+ }
+
+ @Override
+ public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState,
+ @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) {
+ if (newState == PipTransitionState.SWIPING_TO_PIP) {
+ Preconditions.checkState(extra != null,
+ "No extra bundle for " + mPipTransitionState);
+
+ SurfaceControl overlay = extra.getParcelable(
+ SWIPE_TO_PIP_OVERLAY, SurfaceControl.class);
+ Rect appBounds = extra.getParcelable(
+ SWIPE_TO_PIP_APP_BOUNDS, Rect.class);
+
+ Preconditions.checkState(appBounds != null,
+ "App bounds can't be null for " + mPipTransitionState);
+ mPipTransitionState.setSwipePipToHomeState(overlay, appBounds);
+ } else if (newState == PipTransitionState.ENTERED_PIP) {
+ if (mPipTransitionState.isInSwipePipToHomeTransition()) {
+ mPipTransitionState.resetSwipePipToHomeState();
+ }
+ }
+ }
+
+ //
+ // IPipAnimationListener Binder proxy helpers
+ //
+
+ private void setPipRecentsAnimationListener(PipAnimationListener pipAnimationListener) {
+ mPipRecentsAnimationListener = pipAnimationListener;
+ onPipResourceDimensionsChanged();
+ }
+
+ private void onPipResourceDimensionsChanged() {
+ if (mPipRecentsAnimationListener != null) {
+ mPipRecentsAnimationListener.onPipResourceDimensionsChanged(
+ mContext.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius),
+ mContext.getResources().getDimensionPixelSize(R.dimen.pip_shadow_radius));
+ }
+ }
+
+ private void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = " ";
+ pw.println(TAG);
+ mPipBoundsAlgorithm.dump(pw, innerPrefix);
+ mPipBoundsState.dump(pw, innerPrefix);
+ mPipDisplayLayoutState.dump(pw, innerPrefix);
}
/**
@@ -206,9 +360,29 @@ public class PipController implements ConfigurationChangeListener,
@BinderThread
private static class IPipImpl extends IPip.Stub implements ExternalInterfaceBinder {
private PipController mController;
+ private final SingleInstanceRemoteListener<PipController, IPipAnimationListener> mListener;
+ private final PipAnimationListener mPipAnimationListener = new PipAnimationListener() {
+ @Override
+ public void onPipAnimationStarted() {
+ mListener.call(l -> l.onPipAnimationStarted());
+ }
+
+ @Override
+ public void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius) {
+ mListener.call(l -> l.onPipResourceDimensionsChanged(cornerRadius, shadowRadius));
+ }
+
+ @Override
+ public void onExpandPip() {
+ mListener.call(l -> l.onExpandPip());
+ }
+ };
IPipImpl(PipController controller) {
mController = controller;
+ mListener = new SingleInstanceRemoteListener<>(mController,
+ (cntrl) -> cntrl.setPipRecentsAnimationListener(mPipAnimationListener),
+ (cntrl) -> cntrl.setPipRecentsAnimationListener(null));
}
/**
@@ -217,6 +391,8 @@ 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();
}
@Override
@@ -234,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
@@ -257,7 +435,14 @@ public class PipController implements ConfigurationChangeListener,
@Override
public void setPipAnimationListener(IPipAnimationListener listener) {
- // TODO: set a proper animation listener to update the Launcher state as needed.
+ executeRemoteCallWithTaskPermission(mController, "setPipAnimationListener",
+ (controller) -> {
+ if (listener != null) {
+ mListener.register(listener);
+ } else {
+ mListener.unregister();
+ }
+ });
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java
new file mode 100644
index 000000000000..e7e797096c0e
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.SurfaceControl;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+
+import com.android.wm.shell.R;
+import com.android.wm.shell.bubbles.DismissViewUtils;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.bubbles.DismissCircleView;
+import com.android.wm.shell.common.bubbles.DismissView;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
+
+import kotlin.Unit;
+
+/**
+ * Handler of all Magnetized Object related code for PiP.
+ */
+public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListener {
+
+ /* The multiplier to apply scale the target size by when applying the magnetic field radius */
+ private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f;
+
+ /**
+ * MagnetizedObject wrapper for PIP. This allows the magnetic target library to locate and move
+ * PIP.
+ */
+ private MagnetizedObject<Rect> mMagnetizedPip;
+
+ /**
+ * Container for the dismiss circle, so that it can be animated within the container via
+ * translation rather than within the WindowManager via slow layout animations.
+ */
+ private DismissView mTargetViewContainer;
+
+ /** Circle view used to render the dismiss target. */
+ private DismissCircleView mTargetView;
+
+ /**
+ * MagneticTarget instance wrapping the target view and allowing us to set its magnetic radius.
+ */
+ private MagnetizedObject.MagneticTarget mMagneticTarget;
+
+ // Allow dragging the PIP to a location to close it
+ private boolean mEnableDismissDragToEdge;
+
+ private int mTargetSize;
+ private int mDismissAreaHeight;
+ private float mMagneticFieldRadiusPercent = 1f;
+ private WindowInsets mWindowInsets;
+
+ private SurfaceControl mTaskLeash;
+ private boolean mHasDismissTargetSurface;
+
+ private final Context mContext;
+ private final PipMotionHelper mMotionHelper;
+ private final PipUiEventLogger mPipUiEventLogger;
+ private final WindowManager mWindowManager;
+ private final ShellExecutor mMainExecutor;
+
+ public PipDismissTargetHandler(Context context, PipUiEventLogger pipUiEventLogger,
+ PipMotionHelper motionHelper, ShellExecutor mainExecutor) {
+ mContext = context;
+ mPipUiEventLogger = pipUiEventLogger;
+ mMotionHelper = motionHelper;
+ mMainExecutor = mainExecutor;
+ mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ }
+
+ void init() {
+ Resources res = mContext.getResources();
+ mEnableDismissDragToEdge = res.getBoolean(R.bool.config_pipEnableDismissDragToEdge);
+ mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height);
+
+ if (mTargetViewContainer != null) {
+ // init can be called multiple times, remove the old one from view hierarchy first.
+ cleanUpDismissTarget();
+ }
+
+ mTargetViewContainer = new DismissView(mContext);
+ DismissViewUtils.setup(mTargetViewContainer);
+ mTargetView = mTargetViewContainer.getCircle();
+ mTargetViewContainer.setOnApplyWindowInsetsListener((view, windowInsets) -> {
+ if (!windowInsets.equals(mWindowInsets)) {
+ mWindowInsets = windowInsets;
+ updateMagneticTargetSize();
+ }
+ return windowInsets;
+ });
+
+ mMagnetizedPip = mMotionHelper.getMagnetizedPip();
+ mMagnetizedPip.clearAllTargets();
+ mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0);
+ updateMagneticTargetSize();
+
+ mMagnetizedPip.setAnimateStuckToTarget(
+ (target, velX, velY, flung, after) -> {
+ if (mEnableDismissDragToEdge) {
+ mMotionHelper.animateIntoDismissTarget(target, velX, velY, flung, after);
+ }
+ return Unit.INSTANCE;
+ });
+ mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() {
+ @Override
+ public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target,
+ @NonNull MagnetizedObject<?> draggedObject) {
+ // Show the dismiss target, in case the initial touch event occurred within
+ // the magnetic field radius.
+ if (mEnableDismissDragToEdge) {
+ showDismissTargetMaybe();
+ }
+ }
+
+ @Override
+ public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+ @NonNull MagnetizedObject<?> draggedObject,
+ float velX, float velY, boolean wasFlungOut) {
+ if (wasFlungOut) {
+ mMotionHelper.flingToSnapTarget(velX, velY, null /* endAction */);
+ hideDismissTargetMaybe();
+ } else {
+ mMotionHelper.setSpringingToTouch(true);
+ }
+ }
+
+ @Override
+ public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target,
+ @NonNull MagnetizedObject<?> draggedObject) {
+ if (mEnableDismissDragToEdge) {
+ mMainExecutor.executeDelayed(() -> {
+ mMotionHelper.notifyDismissalPending();
+ mMotionHelper.animateDismiss();
+ hideDismissTargetMaybe();
+
+ mPipUiEventLogger.log(
+ PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE);
+ }, 0);
+ }
+ }
+ });
+
+ }
+
+ @Override
+ public boolean onPreDraw() {
+ mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this);
+ mHasDismissTargetSurface = true;
+ updateDismissTargetLayer();
+ return true;
+ }
+
+ /**
+ * Potentially start consuming future motion events if PiP is currently near the magnetized
+ * object.
+ */
+ public boolean maybeConsumeMotionEvent(MotionEvent ev) {
+ return mMagnetizedPip.maybeConsumeMotionEvent(ev);
+ }
+
+ /**
+ * Update the magnet size.
+ */
+ public void updateMagneticTargetSize() {
+ if (mTargetView == null) {
+ return;
+ }
+ if (mTargetViewContainer != null) {
+ mTargetViewContainer.updateResources();
+ }
+
+ final Resources res = mContext.getResources();
+ mTargetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
+ mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height);
+
+ // Set the magnetic field radius equal to the target size from the center of the target
+ setMagneticFieldRadiusPercent(mMagneticFieldRadiusPercent);
+ }
+
+ /**
+ * Increase or decrease the field radius of the magnet object, e.g. with larger percent,
+ * PiP will magnetize to the field sooner.
+ */
+ public void setMagneticFieldRadiusPercent(float percent) {
+ mMagneticFieldRadiusPercent = percent;
+ mMagneticTarget.setMagneticFieldRadiusPx((int) (mMagneticFieldRadiusPercent * mTargetSize
+ * MAGNETIC_FIELD_RADIUS_MULTIPLIER));
+ }
+
+ public void setTaskLeash(SurfaceControl taskLeash) {
+ mTaskLeash = taskLeash;
+ }
+
+ private void updateDismissTargetLayer() {
+ if (!mHasDismissTargetSurface || mTaskLeash == null) {
+ // No dismiss target surface, can just return
+ return;
+ }
+
+ final SurfaceControl targetViewLeash =
+ mTargetViewContainer.getViewRootImpl().getSurfaceControl();
+ if (!targetViewLeash.isValid()) {
+ // The surface of mTargetViewContainer is somehow not ready, bail early
+ return;
+ }
+
+ // Put the dismiss target behind the task
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ t.setRelativeLayer(targetViewLeash, mTaskLeash, -1);
+ t.apply();
+ }
+
+ /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */
+ public void createOrUpdateDismissTarget() {
+ if (mTargetViewContainer.getParent() == null) {
+ mTargetViewContainer.cancelAnimators();
+
+ mTargetViewContainer.setVisibility(View.INVISIBLE);
+ mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this);
+ mHasDismissTargetSurface = false;
+
+ mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams());
+ } else {
+ mWindowManager.updateViewLayout(mTargetViewContainer, getDismissTargetLayoutParams());
+ }
+ }
+
+ /** Returns layout params for the dismiss target, using the latest display metrics. */
+ private WindowManager.LayoutParams getDismissTargetLayoutParams() {
+ final Point windowSize = new Point();
+ mWindowManager.getDefaultDisplay().getRealSize(windowSize);
+ int height = Math.min(windowSize.y, mDismissAreaHeight);
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ WindowManager.LayoutParams.MATCH_PARENT,
+ height,
+ 0, windowSize.y - height,
+ WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
+ WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSLUCENT);
+
+ lp.setTitle("pip-dismiss-overlay");
+ lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
+ lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ lp.setFitInsetsTypes(0 /* types */);
+
+ return lp;
+ }
+
+ /** Makes the dismiss target visible and animates it in, if it isn't already visible. */
+ public void showDismissTargetMaybe() {
+ if (!mEnableDismissDragToEdge) {
+ return;
+ }
+
+ createOrUpdateDismissTarget();
+
+ if (mTargetViewContainer.getVisibility() != View.VISIBLE) {
+ mTargetViewContainer.getViewTreeObserver().addOnPreDrawListener(this);
+ }
+ // always invoke show, since the target might still be VISIBLE while playing hide animation,
+ // so we want to ensure it will show back again
+ mTargetViewContainer.show();
+ }
+
+ /** Animates the magnetic dismiss target out and then sets it to GONE. */
+ public void hideDismissTargetMaybe() {
+ if (!mEnableDismissDragToEdge) {
+ return;
+ }
+ mTargetViewContainer.hide();
+ }
+
+ /**
+ * Removes the dismiss target and cancels any pending callbacks to show it.
+ */
+ public void cleanUpDismissTarget() {
+ if (mTargetViewContainer.getParent() != null) {
+ mWindowManager.removeViewImmediate(mTargetViewContainer);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java
new file mode 100644
index 000000000000..b757b00f16dd
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipInputConsumer.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.view.BatchedInputEventReceiver;
+import android.view.Choreographer;
+import android.view.IWindowManager;
+import android.view.InputChannel;
+import android.view.InputEvent;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.io.PrintWriter;
+
+/**
+ * Manages the input consumer that allows the Shell to directly receive input.
+ */
+public class PipInputConsumer {
+
+ private static final String TAG = PipInputConsumer.class.getSimpleName();
+
+ /**
+ * Listener interface for callers to subscribe to input events.
+ */
+ public interface InputListener {
+ /** Handles any input event. */
+ boolean onInputEvent(InputEvent ev);
+ }
+
+ /**
+ * Listener interface for callers to learn when this class is registered or unregistered with
+ * window manager
+ */
+ interface RegistrationListener {
+ void onRegistrationChanged(boolean isRegistered);
+ }
+
+ /**
+ * Input handler used for the input consumer. Input events are batched and consumed with the
+ * SurfaceFlinger vsync.
+ */
+ private final class InputEventReceiver extends BatchedInputEventReceiver {
+
+ InputEventReceiver(InputChannel inputChannel, Looper looper,
+ Choreographer choreographer) {
+ super(inputChannel, looper, choreographer);
+ }
+
+ @Override
+ public void onInputEvent(InputEvent event) {
+ boolean handled = true;
+ try {
+ if (mListener != null) {
+ handled = mListener.onInputEvent(event);
+ }
+ } finally {
+ finishInputEvent(event, handled);
+ }
+ }
+ }
+
+ private final IWindowManager mWindowManager;
+ private final IBinder mToken;
+ private final String mName;
+ private final ShellExecutor mMainExecutor;
+
+ private InputEventReceiver mInputEventReceiver;
+ private InputListener mListener;
+ private RegistrationListener mRegistrationListener;
+
+ /**
+ * @param name the name corresponding to the input consumer that is defined in the system.
+ */
+ public PipInputConsumer(IWindowManager windowManager, String name,
+ ShellExecutor mainExecutor) {
+ mWindowManager = windowManager;
+ mToken = new Binder();
+ mName = name;
+ mMainExecutor = mainExecutor;
+ }
+
+ /**
+ * Sets the input listener.
+ */
+ public void setInputListener(InputListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Sets the registration listener.
+ */
+ public void setRegistrationListener(RegistrationListener listener) {
+ mRegistrationListener = listener;
+ mMainExecutor.execute(() -> {
+ if (mRegistrationListener != null) {
+ mRegistrationListener.onRegistrationChanged(mInputEventReceiver != null);
+ }
+ });
+ }
+
+ /**
+ * Check if the InputConsumer is currently registered with WindowManager
+ *
+ * @return {@code true} if registered, {@code false} if not.
+ */
+ public boolean isRegistered() {
+ return mInputEventReceiver != null;
+ }
+
+ /**
+ * Registers the input consumer.
+ */
+ public void registerInputConsumer() {
+ if (mInputEventReceiver != null) {
+ return;
+ }
+ final InputChannel inputChannel = new InputChannel();
+ try {
+ // TODO(b/113087003): Support Picture-in-picture in multi-display.
+ mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY);
+ mWindowManager.createInputConsumer(mToken, mName, DEFAULT_DISPLAY, inputChannel);
+ } catch (RemoteException e) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Failed to create input consumer, %s", TAG, e);
+ }
+ mMainExecutor.execute(() -> {
+ mInputEventReceiver = new InputEventReceiver(inputChannel,
+ Looper.myLooper(), Choreographer.getInstance());
+ if (mRegistrationListener != null) {
+ mRegistrationListener.onRegistrationChanged(true /* isRegistered */);
+ }
+ });
+ }
+
+ /**
+ * Unregisters the input consumer.
+ */
+ public void unregisterInputConsumer() {
+ if (mInputEventReceiver == null) {
+ return;
+ }
+ try {
+ // TODO(b/113087003): Support Picture-in-picture in multi-display.
+ mWindowManager.destroyInputConsumer(mToken, DEFAULT_DISPLAY);
+ } catch (RemoteException e) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Failed to destroy input consumer, %s", TAG, e);
+ }
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ mMainExecutor.execute(() -> {
+ if (mRegistrationListener != null) {
+ mRegistrationListener.onRegistrationChanged(false /* isRegistered */);
+ }
+ });
+ }
+
+ /**
+ * Dumps the {@link PipInputConsumer} state.
+ */
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "registered=" + (mInputEventReceiver != null));
+ }
+}
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
new file mode 100644
index 000000000000..495cd0075494
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java
@@ -0,0 +1,790 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_NO_BOUNCY;
+import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW;
+import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM;
+
+import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_LEFT;
+import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_NONE;
+import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_RIGHT;
+import static com.android.wm.shell.pip2.phone.PipMenuView.ANIM_TYPE_DISMISS;
+import static com.android.wm.shell.pip2.phone.PipMenuView.ANIM_TYPE_NONE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Debug;
+import android.view.SurfaceControl;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.animation.FloatProperties;
+import com.android.wm.shell.common.FloatingContentCoordinator;
+import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.common.pip.PipAppOpsListener;
+import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
+import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipPerfHintController;
+import com.android.wm.shell.common.pip.PipSnapAlgorithm;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
+
+import kotlin.Unit;
+import kotlin.jvm.functions.Function0;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+/**
+ * A helper to animate and manipulate the PiP.
+ */
+public class PipMotionHelper implements PipAppOpsListener.Callback,
+ FloatingContentCoordinator.FloatingContent,
+ PipTransitionState.PipTransitionStateChangedListener {
+ private static final String TAG = "PipMotionHelper";
+ private static final String FLING_BOUNDS_CHANGE = "fling_bounds_change";
+ private static final boolean DEBUG = false;
+
+ private static final int SHRINK_STACK_FROM_MENU_DURATION = 250;
+ private static final int EXPAND_STACK_TO_MENU_DURATION = 250;
+ private static final int UNSTASH_DURATION = 250;
+ private static final int LEAVE_PIP_DURATION = 300;
+ private static final int SHIFT_DURATION = 300;
+
+ /** Friction to use for PIP when it moves via physics fling animations. */
+ private static final float DEFAULT_FRICTION = 1.9f;
+ /** How much of the dismiss circle size to use when scaling down PIP. **/
+ private static final float DISMISS_CIRCLE_PERCENT = 0.85f;
+
+ private final Context mContext;
+ private @NonNull PipBoundsState mPipBoundsState;
+ private @NonNull PipBoundsAlgorithm mPipBoundsAlgorithm;
+ private @NonNull PipScheduler mPipScheduler;
+ private @NonNull PipTransitionState mPipTransitionState;
+ private PhonePipMenuController mMenuController;
+ private PipSnapAlgorithm mSnapAlgorithm;
+
+ /** The region that all of PIP must stay within. */
+ private final Rect mFloatingAllowedArea = new Rect();
+
+ /** Coordinator instance for resolving conflicts with other floating content. */
+ private FloatingContentCoordinator mFloatingContentCoordinator;
+
+ @Nullable private final PipPerfHintController mPipPerfHintController;
+ @Nullable private PipPerfHintController.PipHighPerfSession mPipHighPerfSession;
+
+ /**
+ * PhysicsAnimator instance for animating {@link PipBoundsState#getMotionBoundsState()}
+ * using physics animations.
+ */
+ private PhysicsAnimator<Rect> mTemporaryBoundsPhysicsAnimator;
+
+ private MagnetizedObject<Rect> mMagnetizedPip;
+
+ /**
+ * Update listener that resizes the PIP to {@link PipBoundsState#getMotionBoundsState()}.
+ */
+ private final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener;
+
+ /** FlingConfig instances provided to PhysicsAnimator for fling gestures. */
+ private PhysicsAnimator.FlingConfig mFlingConfigX;
+ private PhysicsAnimator.FlingConfig mFlingConfigY;
+ /** FlingConfig instances provided to PhysicsAnimator for stashing. */
+ private PhysicsAnimator.FlingConfig mStashConfigX;
+
+ /** SpringConfig to use for fling-then-spring animations. */
+ private final PhysicsAnimator.SpringConfig mSpringConfig =
+ new PhysicsAnimator.SpringConfig(700f, DAMPING_RATIO_NO_BOUNCY);
+
+ /** SpringConfig used for animating into the dismiss region, matches the one in
+ * {@link MagnetizedObject}. */
+ private final PhysicsAnimator.SpringConfig mAnimateToDismissSpringConfig =
+ new PhysicsAnimator.SpringConfig(STIFFNESS_MEDIUM, DAMPING_RATIO_NO_BOUNCY);
+
+ /** SpringConfig used for animating the pip to catch up to the finger once it leaves the dismiss
+ * drag region. */
+ private final PhysicsAnimator.SpringConfig mCatchUpSpringConfig =
+ new PhysicsAnimator.SpringConfig(5000f, DAMPING_RATIO_NO_BOUNCY);
+
+ /** SpringConfig to use for springing PIP away from conflicting floating content. */
+ private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig =
+ new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_NO_BOUNCY);
+
+ private final Consumer<Rect> mUpdateBoundsCallback = (Rect newBounds) -> {
+ if (mPipBoundsState.getBounds().equals(newBounds)) {
+ return;
+ }
+
+ mMenuController.updateMenuLayout(newBounds);
+ mPipBoundsState.setBounds(newBounds);
+ };
+
+ /**
+ * Whether we're springing to the touch event location (vs. moving it to that position
+ * instantly). We spring-to-touch after PIP is dragged out of the magnetic target, since it was
+ * 'stuck' in the target and needs to catch up to the touch location.
+ */
+ private boolean mSpringingToTouch = false;
+
+ /**
+ * Whether PIP was released in the dismiss target, and will be animated out and dismissed
+ * shortly.
+ */
+ private boolean mDismissalPending = false;
+
+ /**
+ * Set to true if bounds change transition has been scheduled from PipMotionHelper.
+ */
+ private boolean mWaitingForBoundsChangeTransition = false;
+
+ /**
+ * Gets set in {@link #animateToExpandedState(Rect, Rect, Rect, Runnable)}, this callback is
+ * used to show menu activity when the expand animation is completed.
+ */
+ private Runnable mPostPipTransitionCallback;
+
+ public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState,
+ PhonePipMenuController menuController, PipSnapAlgorithm snapAlgorithm,
+ FloatingContentCoordinator floatingContentCoordinator, PipScheduler pipScheduler,
+ Optional<PipPerfHintController> pipPerfHintControllerOptional,
+ PipBoundsAlgorithm pipBoundsAlgorithm, PipTransitionState pipTransitionState) {
+ mContext = context;
+ mPipBoundsState = pipBoundsState;
+ mPipBoundsAlgorithm = pipBoundsAlgorithm;
+ mPipScheduler = pipScheduler;
+ mMenuController = menuController;
+ mSnapAlgorithm = snapAlgorithm;
+ mFloatingContentCoordinator = floatingContentCoordinator;
+ mPipPerfHintController = pipPerfHintControllerOptional.orElse(null);
+ mResizePipUpdateListener = (target, values) -> {
+ if (mPipBoundsState.getMotionBoundsState().isInMotion()) {
+ mPipScheduler.scheduleUserResizePip(
+ mPipBoundsState.getMotionBoundsState().getBoundsInMotion());
+ }
+ };
+ mPipTransitionState = pipTransitionState;
+ mPipTransitionState.addPipTransitionStateChangedListener(this);
+ }
+
+ void init() {
+ mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance(
+ mPipBoundsState.getMotionBoundsState().getBoundsInMotion());
+ }
+
+ @NonNull
+ @Override
+ public Rect getFloatingBoundsOnScreen() {
+ return !mPipBoundsState.getMotionBoundsState().getAnimatingToBounds().isEmpty()
+ ? mPipBoundsState.getMotionBoundsState().getAnimatingToBounds() : getBounds();
+ }
+
+ @NonNull
+ @Override
+ public Rect getAllowedFloatingBoundsRegion() {
+ return mFloatingAllowedArea;
+ }
+
+ @Override
+ public void moveToBounds(@NonNull Rect bounds) {
+ animateToBounds(bounds, mConflictResolutionSpringConfig);
+ }
+
+ /**
+ * Synchronizes the current bounds with the pinned stack, cancelling any ongoing animations.
+ */
+ void synchronizePinnedStackBounds() {
+ cancelPhysicsAnimation();
+ mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded();
+
+ /*
+ if (mPipTaskOrganizer.isInPip()) {
+ mFloatingContentCoordinator.onContentMoved(this);
+ }
+ */
+ }
+
+ /**
+ * Tries to move the pinned stack to the given {@param bounds}.
+ */
+ void movePip(Rect toBounds) {
+ movePip(toBounds, false /* isDragging */);
+ }
+
+ /**
+ * Tries to move the pinned stack to the given {@param bounds}.
+ *
+ * @param isDragging Whether this movement is the result of a drag touch gesture. If so, we
+ * won't notify the floating content coordinator of this move, since that will
+ * happen when the gesture ends.
+ */
+ void movePip(Rect toBounds, boolean isDragging) {
+ if (!isDragging) {
+ mFloatingContentCoordinator.onContentMoved(this);
+ }
+
+ if (!mSpringingToTouch) {
+ // If we are moving PIP directly to the touch event locations, cancel any animations and
+ // move PIP to the given bounds.
+ cancelPhysicsAnimation();
+
+ if (!isDragging) {
+ resizePipUnchecked(toBounds);
+ mPipBoundsState.setBounds(toBounds);
+ } else {
+ mPipBoundsState.getMotionBoundsState().setBoundsInMotion(toBounds);
+ mPipScheduler.scheduleUserResizePip(toBounds);
+ }
+ } else {
+ // If PIP is 'catching up' after being stuck in the dismiss target, update the animation
+ // to spring towards the new touch location.
+ mTemporaryBoundsPhysicsAnimator
+ .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mCatchUpSpringConfig)
+ .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mCatchUpSpringConfig)
+ .spring(FloatProperties.RECT_X, toBounds.left, mCatchUpSpringConfig)
+ .spring(FloatProperties.RECT_Y, toBounds.top, mCatchUpSpringConfig);
+
+ startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */);
+ }
+ }
+
+ /** Animates the PIP into the dismiss target, scaling it down. */
+ void animateIntoDismissTarget(
+ MagnetizedObject.MagneticTarget target,
+ float velX, float velY,
+ boolean flung, Function0<Unit> after) {
+ final PointF targetCenter = target.getCenterOnScreen();
+
+ // PIP should fit in the circle
+ final float dismissCircleSize = mContext.getResources().getDimensionPixelSize(
+ R.dimen.dismiss_circle_size);
+
+ final float width = getBounds().width();
+ final float height = getBounds().height();
+ final float ratio = width / height;
+
+ // Width should be a little smaller than the circle size.
+ final float desiredWidth = dismissCircleSize * DISMISS_CIRCLE_PERCENT;
+ final float desiredHeight = desiredWidth / ratio;
+ final float destinationX = targetCenter.x - (desiredWidth / 2f);
+ final float destinationY = targetCenter.y - (desiredHeight / 2f);
+
+ // If we're already in the dismiss target area, then there won't be a move to set the
+ // temporary bounds, so just initialize it to the current bounds.
+ if (!mPipBoundsState.getMotionBoundsState().isInMotion()) {
+ mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds());
+ }
+ mTemporaryBoundsPhysicsAnimator
+ .spring(FloatProperties.RECT_X, destinationX, velX, mAnimateToDismissSpringConfig)
+ .spring(FloatProperties.RECT_Y, destinationY, velY, mAnimateToDismissSpringConfig)
+ .spring(FloatProperties.RECT_WIDTH, desiredWidth, mAnimateToDismissSpringConfig)
+ .spring(FloatProperties.RECT_HEIGHT, desiredHeight, mAnimateToDismissSpringConfig)
+ .withEndActions(after);
+
+ startBoundsAnimator(destinationX, destinationY);
+ }
+
+ /** Set whether we're springing-to-touch to catch up after being stuck in the dismiss target. */
+ void setSpringingToTouch(boolean springingToTouch) {
+ mSpringingToTouch = springingToTouch;
+ }
+
+ /**
+ * Resizes the pinned stack back to unknown windowing mode, which could be freeform or
+ * * fullscreen depending on the display area's windowing mode.
+ */
+ void expandLeavePip(boolean skipAnimation) {
+ expandLeavePip(skipAnimation, false /* enterSplit */);
+ }
+
+ /**
+ * Resizes the pinned task to split-screen mode.
+ */
+ void expandIntoSplit() {
+ expandLeavePip(false, true /* enterSplit */);
+ }
+
+ /**
+ * Resizes the pinned stack back to unknown windowing mode, which could be freeform or
+ * fullscreen depending on the display area's windowing mode.
+ */
+ private void expandLeavePip(boolean skipAnimation, boolean enterSplit) {
+ if (DEBUG) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: exitPip: skipAnimation=%s"
+ + " callers=\n%s", TAG, skipAnimation, Debug.getCallers(5, " "));
+ }
+ cancelPhysicsAnimation();
+ mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */);
+ // mPipTaskOrganizer.exitPip(skipAnimation ? 0 : LEAVE_PIP_DURATION, enterSplit);
+ }
+
+ /**
+ * Dismisses the pinned stack.
+ */
+ @Override
+ public void dismissPip() {
+ if (DEBUG) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: removePip: callers=\n%s", TAG, Debug.getCallers(5, " "));
+ }
+ cancelPhysicsAnimation();
+ mMenuController.hideMenu(ANIM_TYPE_DISMISS, false /* resize */);
+ // mPipTaskOrganizer.removePip();
+ }
+
+ /** Sets the movement bounds to use to constrain PIP position animations. */
+ void onMovementBoundsChanged() {
+ rebuildFlingConfigs();
+
+ // The movement bounds represent the area within which we can move PIP's top-left position.
+ // The allowed area for all of PIP is those bounds plus PIP's width and height.
+ mFloatingAllowedArea.set(mPipBoundsState.getMovementBounds());
+ mFloatingAllowedArea.right += getBounds().width();
+ mFloatingAllowedArea.bottom += getBounds().height();
+ }
+
+ /**
+ * @return the PiP bounds.
+ */
+ private Rect getBounds() {
+ return mPipBoundsState.getBounds();
+ }
+
+ /**
+ * Flings the PiP to the closest snap target.
+ */
+ void flingToSnapTarget(
+ float velocityX, float velocityY, @Nullable Runnable postBoundsUpdateCallback) {
+ movetoTarget(velocityX, velocityY, postBoundsUpdateCallback, false /* isStash */);
+ }
+
+ /**
+ * Stash PiP to the closest edge. We set velocityY to 0 to limit pure horizontal motion.
+ */
+ void stashToEdge(float velX, float velY, @Nullable Runnable postBoundsUpdateCallback) {
+ velY = mPipBoundsState.getStashedState() == STASH_TYPE_NONE ? 0 : velY;
+ movetoTarget(velX, velY, postBoundsUpdateCallback, true /* isStash */);
+ }
+
+ private void onHighPerfSessionTimeout(PipPerfHintController.PipHighPerfSession session) {}
+
+ private void cleanUpHighPerfSessionMaybe() {
+ if (mPipHighPerfSession != null) {
+ // Close the high perf session once pointer interactions are over;
+ mPipHighPerfSession.close();
+ mPipHighPerfSession = null;
+ }
+ }
+
+ private void movetoTarget(
+ float velocityX,
+ float velocityY,
+ @Nullable Runnable postBoundsUpdateCallback,
+ boolean isStash) {
+ // If we're flinging to a snap target now, we're not springing to catch up to the touch
+ // location now.
+ mSpringingToTouch = false;
+
+ mTemporaryBoundsPhysicsAnimator
+ .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mSpringConfig)
+ .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mSpringConfig)
+ .flingThenSpring(
+ FloatProperties.RECT_X, velocityX,
+ isStash ? mStashConfigX : mFlingConfigX,
+ mSpringConfig, true /* flingMustReachMinOrMax */)
+ .flingThenSpring(
+ FloatProperties.RECT_Y, velocityY, mFlingConfigY, mSpringConfig);
+
+ final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets();
+ final float leftEdge = isStash
+ ? mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width()
+ + insetBounds.left
+ : mPipBoundsState.getMovementBounds().left;
+ final float rightEdge = isStash
+ ? mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset()
+ - insetBounds.right
+ : mPipBoundsState.getMovementBounds().right;
+
+ final float xEndValue = velocityX < 0 ? leftEdge : rightEdge;
+
+ final int startValueY = mPipBoundsState.getMotionBoundsState().getBoundsInMotion().top;
+ final float estimatedFlingYEndValue =
+ PhysicsAnimator.estimateFlingEndValue(startValueY, velocityY, mFlingConfigY);
+
+ startBoundsAnimator(xEndValue /* toX */, estimatedFlingYEndValue /* toY */,
+ postBoundsUpdateCallback);
+ }
+
+ /**
+ * Animates PIP to the provided bounds, using physics animations and the given spring
+ * configuration
+ */
+ void animateToBounds(Rect bounds, PhysicsAnimator.SpringConfig springConfig) {
+ if (!mTemporaryBoundsPhysicsAnimator.isRunning()) {
+ // Animate from the current bounds if we're not already animating.
+ mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds());
+ }
+
+ mTemporaryBoundsPhysicsAnimator
+ .spring(FloatProperties.RECT_X, bounds.left, springConfig)
+ .spring(FloatProperties.RECT_Y, bounds.top, springConfig);
+ startBoundsAnimator(bounds.left /* toX */, bounds.top /* toY */);
+ }
+
+ /**
+ * Animates the dismissal of the PiP off the edge of the screen.
+ */
+ void animateDismiss() {
+ // Animate off the bottom of the screen, then dismiss PIP.
+ mTemporaryBoundsPhysicsAnimator
+ .spring(FloatProperties.RECT_Y,
+ mPipBoundsState.getMovementBounds().bottom + getBounds().height() * 2,
+ 0,
+ mSpringConfig)
+ .withEndActions(this::dismissPip);
+
+ startBoundsAnimator(
+ getBounds().left /* toX */, getBounds().bottom + getBounds().height() /* toY */);
+
+ mDismissalPending = false;
+ }
+
+ /**
+ * Animates the PiP to the expanded state to show the menu.
+ */
+ float animateToExpandedState(Rect expandedBounds, Rect movementBounds,
+ Rect expandedMovementBounds, Runnable callback) {
+ float savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(getBounds()),
+ movementBounds);
+ mSnapAlgorithm.applySnapFraction(expandedBounds, expandedMovementBounds, savedSnapFraction);
+ mPostPipTransitionCallback = callback;
+ resizeAndAnimatePipUnchecked(expandedBounds, EXPAND_STACK_TO_MENU_DURATION);
+ return savedSnapFraction;
+ }
+
+ /**
+ * Animates the PiP from the expanded state to the normal state after the menu is hidden.
+ */
+ void animateToUnexpandedState(Rect normalBounds, float savedSnapFraction,
+ Rect normalMovementBounds, Rect currentMovementBounds, boolean immediate) {
+ if (savedSnapFraction < 0f) {
+ // If there are no saved snap fractions, then just use the current bounds
+ savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(getBounds()),
+ currentMovementBounds, mPipBoundsState.getStashedState());
+ }
+
+ mSnapAlgorithm.applySnapFraction(normalBounds, normalMovementBounds, savedSnapFraction,
+ mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(),
+ mPipBoundsState.getDisplayBounds(),
+ mPipBoundsState.getDisplayLayout().stableInsets());
+
+ if (immediate) {
+ movePip(normalBounds);
+ } else {
+ resizeAndAnimatePipUnchecked(normalBounds, SHRINK_STACK_FROM_MENU_DURATION);
+ }
+ }
+
+ /**
+ * Animates the PiP to the stashed state, choosing the closest edge.
+ */
+ void animateToStashedClosestEdge() {
+ Rect tmpBounds = new Rect();
+ final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets();
+ final int stashType =
+ mPipBoundsState.getBounds().left == mPipBoundsState.getMovementBounds().left
+ ? STASH_TYPE_LEFT : STASH_TYPE_RIGHT;
+ final float leftEdge = stashType == STASH_TYPE_LEFT
+ ? mPipBoundsState.getStashOffset()
+ - mPipBoundsState.getBounds().width() + insetBounds.left
+ : mPipBoundsState.getDisplayBounds().right
+ - mPipBoundsState.getStashOffset() - insetBounds.right;
+ tmpBounds.set((int) leftEdge,
+ mPipBoundsState.getBounds().top,
+ (int) (leftEdge + mPipBoundsState.getBounds().width()),
+ mPipBoundsState.getBounds().bottom);
+ resizeAndAnimatePipUnchecked(tmpBounds, UNSTASH_DURATION);
+ mPipBoundsState.setStashed(stashType);
+ }
+
+ /**
+ * Animates the PiP from stashed state into un-stashed, popping it out from the edge.
+ */
+ void animateToUnStashedBounds(Rect unstashedBounds) {
+ resizeAndAnimatePipUnchecked(unstashedBounds, UNSTASH_DURATION);
+ }
+
+ /**
+ * Animates the PiP to offset it from the IME or shelf.
+ */
+ void animateToOffset(Rect originalBounds, int offset) {
+ if (DEBUG) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: animateToOffset: originalBounds=%s offset=%s"
+ + " callers=\n%s", TAG, originalBounds, offset,
+ Debug.getCallers(5, " "));
+ }
+ cancelPhysicsAnimation();
+ /*
+ mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION,
+ mUpdateBoundsCallback);
+ */
+ }
+
+ /**
+ * Cancels all existing animations.
+ */
+ private void cancelPhysicsAnimation() {
+ mTemporaryBoundsPhysicsAnimator.cancel();
+ mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded();
+ mSpringingToTouch = false;
+ }
+
+ /** Set new fling configs whose min/max values respect the given movement bounds. */
+ private void rebuildFlingConfigs() {
+ mFlingConfigX = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION,
+ mPipBoundsAlgorithm.getMovementBounds(getBounds()).left,
+ mPipBoundsAlgorithm.getMovementBounds(getBounds()).right);
+ mFlingConfigY = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION,
+ mPipBoundsAlgorithm.getMovementBounds(getBounds()).top,
+ mPipBoundsAlgorithm.getMovementBounds(getBounds()).bottom);
+ final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets();
+ mStashConfigX = new PhysicsAnimator.FlingConfig(
+ DEFAULT_FRICTION,
+ mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width()
+ + insetBounds.left,
+ mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset()
+ - insetBounds.right);
+ }
+
+ private void startBoundsAnimator(float toX, float toY) {
+ startBoundsAnimator(toX, toY, null /* postBoundsUpdateCallback */);
+ }
+
+ /**
+ * Starts the physics animator which will update the animated PIP bounds using physics
+ * animations, as well as the TimeAnimator which will apply those bounds to PIP.
+ *
+ * This will also add end actions to the bounds animator that cancel the TimeAnimator and update
+ * the 'real' bounds to equal the final animated bounds.
+ *
+ * If one wishes to supply a callback after all the 'real' bounds update has happened,
+ * pass @param postBoundsUpdateCallback.
+ */
+ private void startBoundsAnimator(float toX, float toY, Runnable postBoundsUpdateCallback) {
+ if (!mSpringingToTouch) {
+ cancelPhysicsAnimation();
+ }
+
+ setAnimatingToBounds(new Rect(
+ (int) toX,
+ (int) toY,
+ (int) toX + getBounds().width(),
+ (int) toY + getBounds().height()));
+
+ if (!mTemporaryBoundsPhysicsAnimator.isRunning()) {
+ if (mPipPerfHintController != null) {
+ // Start a high perf session with a timeout callback.
+ mPipHighPerfSession = mPipPerfHintController.startSession(
+ this::onHighPerfSessionTimeout, "startBoundsAnimator");
+ }
+ if (postBoundsUpdateCallback != null) {
+ mTemporaryBoundsPhysicsAnimator
+ .addUpdateListener(mResizePipUpdateListener)
+ .withEndActions(this::onBoundsPhysicsAnimationEnd,
+ postBoundsUpdateCallback);
+ } else {
+ mTemporaryBoundsPhysicsAnimator
+ .addUpdateListener(mResizePipUpdateListener)
+ .withEndActions(this::onBoundsPhysicsAnimationEnd);
+ }
+ }
+
+ mTemporaryBoundsPhysicsAnimator.start();
+ }
+
+ /**
+ * Notify that PIP was released in the dismiss target and will be animated out and dismissed
+ * shortly.
+ */
+ void notifyDismissalPending() {
+ mDismissalPending = true;
+ }
+
+ private void onBoundsPhysicsAnimationEnd() {
+ // The physics animation ended, though we may not necessarily be done animating, such as
+ // when we're still dragging after moving out of the magnetic target.
+ if (!mDismissalPending && !mSpringingToTouch && !mMagnetizedPip.getObjectStuckToTarget()) {
+ // do not schedule resize if PiP is dismissing, which may cause app re-open to
+ // mBounds instead of its normal bounds.
+ Bundle extra = new Bundle();
+ extra.putBoolean(FLING_BOUNDS_CHANGE, true);
+ mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra);
+ return;
+ }
+ settlePipBoundsAfterPhysicsAnimation(true /* animatingAfter */);
+ cleanUpHighPerfSessionMaybe();
+ }
+
+ /**
+ * Notifies the floating coordinator that we're moving, and sets the animating to bounds so
+ * we return these bounds from
+ * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}.
+ */
+ private void setAnimatingToBounds(Rect bounds) {
+ mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(bounds);
+ mFloatingContentCoordinator.onContentMoved(this);
+ }
+
+ /**
+ * Directly resizes the PiP to the given {@param bounds}.
+ */
+ private void resizePipUnchecked(Rect toBounds) {
+ if (DEBUG) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: resizePipUnchecked: toBounds=%s"
+ + " callers=\n%s", TAG, toBounds, Debug.getCallers(5, " "));
+ }
+ if (!toBounds.equals(getBounds())) {
+ mPipScheduler.scheduleAnimateResizePip(toBounds);
+ }
+ }
+
+ /**
+ * Directly resizes the PiP to the given {@param bounds}.
+ */
+ private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) {
+ if (DEBUG) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: resizeAndAnimatePipUnchecked: toBounds=%s"
+ + " duration=%s callers=\n%s", TAG, toBounds, duration,
+ Debug.getCallers(5, " "));
+ }
+
+ // Intentionally resize here even if the current bounds match the destination bounds.
+ // This is so all the proper callbacks are performed.
+
+ // mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration,
+ // TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND, null /* updateBoundsCallback */);
+ // setAnimatingToBounds(toBounds);
+ }
+
+ @Override
+ public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState,
+ @PipTransitionState.TransitionState int newState,
+ @Nullable Bundle extra) {
+ switch (newState) {
+ case PipTransitionState.SCHEDULED_BOUNDS_CHANGE:
+ if (!extra.getBoolean(FLING_BOUNDS_CHANGE)) break;
+
+ if (mPipBoundsState.getBounds().equals(
+ mPipBoundsState.getMotionBoundsState().getBoundsInMotion())) {
+ // Avoid scheduling transitions for bounds that don't change, such transition is
+ // a no-op and would be aborted.
+ settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */);
+ cleanUpHighPerfSessionMaybe();
+ // SCHEDULED_BOUNDS_CHANGE can have multiple active listeners making
+ // actual changes (e.g. PipTouchHandler). So post state update onto handler,
+ // to run after synchronous dispatch is complete.
+ mPipTransitionState.postState(PipTransitionState.CHANGED_PIP_BOUNDS);
+ break;
+ }
+
+ // If touch is turned off and we are in a fling animation, schedule a transition.
+ mWaitingForBoundsChangeTransition = true;
+ mPipScheduler.scheduleAnimateResizePip(
+ mPipBoundsState.getMotionBoundsState().getBoundsInMotion());
+ break;
+ case PipTransitionState.CHANGING_PIP_BOUNDS:
+ if (!mWaitingForBoundsChangeTransition) break;
+
+ // If bounds change transition was scheduled from this class, handle leash updates.
+ mWaitingForBoundsChangeTransition = false;
+ SurfaceControl.Transaction startTx = extra.getParcelable(
+ PipTransition.PIP_START_TX, SurfaceControl.Transaction.class);
+ Rect destinationBounds = extra.getParcelable(
+ PipTransition.PIP_DESTINATION_BOUNDS, Rect.class);
+ startTx.setPosition(mPipTransitionState.mPinnedTaskLeash,
+ destinationBounds.left, destinationBounds.top);
+ startTx.apply();
+
+ // All motion operations have actually finished, so make bounds cache updates.
+ settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */);
+ cleanUpHighPerfSessionMaybe();
+
+ // 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
+ // breaking the state (e.g. leashes are cleaned up upon exit).
+ if (!mPipBoundsState.getMotionBoundsState().isInMotion()) break;
+ cancelPhysicsAnimation();
+ settlePipBoundsAfterPhysicsAnimation(false /* animatingAfter */);
+ }
+ }
+
+ private void settlePipBoundsAfterPhysicsAnimation(boolean animatingAfter) {
+ if (!animatingAfter) {
+ // The physics animation ended, though we may not necessarily be done animating, such as
+ // when we're still dragging after moving out of the magnetic target. Only set the final
+ // bounds state and clear motion bounds completely if the whole animation is over.
+ mPipBoundsState.setBounds(mPipBoundsState.getMotionBoundsState().getBoundsInMotion());
+ mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded();
+ }
+ mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded();
+ mSpringingToTouch = false;
+ mDismissalPending = false;
+ }
+
+ /**
+ * Returns a MagnetizedObject wrapper for PIP's animated bounds. This is provided to the
+ * magnetic dismiss target so it can calculate PIP's size and position.
+ */
+ MagnetizedObject<Rect> getMagnetizedPip() {
+ if (mMagnetizedPip == null) {
+ mMagnetizedPip = new MagnetizedObject<Rect>(
+ mContext, mPipBoundsState.getMotionBoundsState().getBoundsInMotion(),
+ FloatProperties.RECT_X, FloatProperties.RECT_Y) {
+ @Override
+ public float getWidth(@NonNull Rect animatedPipBounds) {
+ return animatedPipBounds.width();
+ }
+
+ @Override
+ public float getHeight(@NonNull Rect animatedPipBounds) {
+ return animatedPipBounds.height();
+ }
+
+ @Override
+ public void getLocationOnScreen(
+ @NonNull Rect animatedPipBounds, @NonNull int[] loc) {
+ loc[0] = animatedPipBounds.left;
+ loc[1] = animatedPipBounds.top;
+ }
+ };
+ mMagnetizedPip.setFlingToTargetEnabled(false);
+ }
+
+ return mMagnetizedPip;
+ }
+}
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
new file mode 100644
index 000000000000..33e80bd80988
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java
@@ -0,0 +1,598 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wm.shell.pip2.phone;
+
+import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.hardware.input.InputManager;
+import android.os.Bundle;
+import android.os.Looper;
+import android.view.BatchedInputEventReceiver;
+import android.view.Choreographer;
+import android.view.InputChannel;
+import android.view.InputEvent;
+import android.view.InputEventReceiver;
+import android.view.InputMonitor;
+import android.view.MotionEvent;
+import android.view.SurfaceControl;
+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;
+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;
+
+/**
+ * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to
+ * trigger dynamic resize.
+ */
+public class PipResizeGestureHandler implements
+ PipTransitionState.PipTransitionStateChangedListener {
+
+ private static final String TAG = "PipResizeGestureHandler";
+ private static final int PINCH_RESIZE_SNAP_DURATION = 250;
+ private static final float PINCH_RESIZE_AUTO_MAX_RATIO = 0.9f;
+ private static final String RESIZE_BOUNDS_CHANGE = "resize_bounds_change";
+
+ private final Context mContext;
+ private final PipBoundsAlgorithm mPipBoundsAlgorithm;
+ private final PipBoundsState mPipBoundsState;
+ private final PipTouchState mPipTouchState;
+ private final PipScheduler mPipScheduler;
+ private final PipTransitionState mPipTransitionState;
+ private final PhonePipMenuController mPhonePipMenuController;
+ private final PipUiEventLogger mPipUiEventLogger;
+ private final PipPinchResizingAlgorithm mPinchResizingAlgorithm;
+ private final int mDisplayId;
+ private final ShellExecutor mMainExecutor;
+
+ private final PointF mDownPoint = new PointF();
+ private final PointF mDownSecondPoint = new PointF();
+ private final PointF mLastPoint = new PointF();
+ private final PointF mLastSecondPoint = new PointF();
+ private final Point mMaxSize = new Point();
+ private final Point mMinSize = new Point();
+ 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;
+
+ private float mTouchSlop;
+
+ private boolean mAllowGesture;
+ private boolean mIsAttached;
+ private boolean mIsEnabled;
+ private boolean mEnablePinchResize;
+ private boolean mIsSysUiStateValid;
+ private boolean mThresholdCrossed;
+ private boolean mOngoingPinchToResize = false;
+ private boolean mWaitingForBoundsChangeTransition = false;
+ private float mAngle = 0;
+ int mFirstIndex = -1;
+ int mSecondIndex = -1;
+
+ private InputMonitor mInputMonitor;
+ private InputEventReceiver mInputEventReceiver;
+
+ @Nullable
+ private final PipPerfHintController mPipPerfHintController;
+
+ @Nullable
+ private PipPerfHintController.PipHighPerfSession mPipHighPerfSession;
+
+ private int mCtrlType;
+ private int mOhmOffset;
+
+ public PipResizeGestureHandler(Context context,
+ PipBoundsAlgorithm pipBoundsAlgorithm,
+ PipBoundsState pipBoundsState,
+ PipTouchState pipTouchState,
+ PipScheduler pipScheduler,
+ PipTransitionState pipTransitionState,
+ Runnable updateMovementBoundsRunnable,
+ PipUiEventLogger pipUiEventLogger,
+ PhonePipMenuController menuActivityController,
+ ShellExecutor mainExecutor,
+ @Nullable PipPerfHintController pipPerfHintController) {
+ mContext = context;
+ mDisplayId = context.getDisplayId();
+ mMainExecutor = mainExecutor;
+ mPipPerfHintController = pipPerfHintController;
+ mPipBoundsAlgorithm = pipBoundsAlgorithm;
+ mPipBoundsState = pipBoundsState;
+ mPipTouchState = pipTouchState;
+ mPipScheduler = pipScheduler;
+
+ mPipTransitionState = pipTransitionState;
+ mPipTransitionState.addPipTransitionStateChangedListener(this);
+
+ mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable;
+ mPhonePipMenuController = menuActivityController;
+ mPipUiEventLogger = pipUiEventLogger;
+ mPinchResizingAlgorithm = new PipPinchResizingAlgorithm();
+
+ mUpdateResizeBoundsCallback = (rect) -> {
+ mUserResizeBounds.set(rect);
+ // mMotionHelper.synchronizePinnedStackBounds();
+ mUpdateMovementBoundsRunnable.run();
+ mPipBoundsState.setBounds(rect);
+ resetState();
+ };
+ }
+
+ void init() {
+ mContext.getDisplay().getRealSize(mMaxSize);
+ reloadResources();
+
+ final Resources res = mContext.getResources();
+ mEnablePinchResize = res.getBoolean(R.bool.config_pipEnablePinchResize);
+ }
+
+ void onConfigurationChanged() {
+ reloadResources();
+ }
+
+ /**
+ * Called when SysUI state changed.
+ *
+ * @param isSysUiStateValid Is SysUI valid or not.
+ */
+ public void onSystemUiStateChanged(boolean isSysUiStateValid) {
+ mIsSysUiStateValid = isSysUiStateValid;
+ }
+
+ private void reloadResources() {
+ mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
+ }
+
+ private void disposeInputChannel() {
+ if (mInputEventReceiver != null) {
+ mInputEventReceiver.dispose();
+ mInputEventReceiver = null;
+ }
+ if (mInputMonitor != null) {
+ mInputMonitor.dispose();
+ mInputMonitor = null;
+ }
+ }
+
+ void onActivityPinned() {
+ mIsAttached = true;
+ updateIsEnabled();
+ }
+
+ void onActivityUnpinned() {
+ mIsAttached = false;
+ mUserResizeBounds.setEmpty();
+ updateIsEnabled();
+ }
+
+ private void updateIsEnabled() {
+ boolean isEnabled = mIsAttached;
+ if (isEnabled == mIsEnabled) {
+ return;
+ }
+ mIsEnabled = isEnabled;
+ disposeInputChannel();
+
+ if (mIsEnabled) {
+ // Register input event receiver
+ mInputMonitor = mContext.getSystemService(InputManager.class).monitorGestureInput(
+ "pip-resize", mDisplayId);
+ try {
+ mMainExecutor.executeBlocking(() -> {
+ mInputEventReceiver = new PipResizeInputEventReceiver(
+ mInputMonitor.getInputChannel(), Looper.myLooper());
+ });
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Failed to create input event receiver", e);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ void onInputEvent(InputEvent ev) {
+ if (!mEnablePinchResize) {
+ // No need to handle anything if resizing isn't enabled.
+ return;
+ }
+
+ if (!mPipTouchState.getAllowInputEvents()) {
+ // No need to handle anything if touches are not enabled
+ return;
+ }
+
+ // Don't allow resize when PiP is stashed.
+ if (mPipBoundsState.isStashed()) {
+ return;
+ }
+
+ if (ev instanceof MotionEvent) {
+ MotionEvent mv = (MotionEvent) ev;
+ int action = mv.getActionMasked();
+ final Rect pipBounds = mPipBoundsState.getBounds();
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ if (!pipBounds.contains((int) mv.getRawX(), (int) mv.getRawY())
+ && mPhonePipMenuController.isMenuVisible()) {
+ mPhonePipMenuController.hideMenu();
+ }
+ }
+
+ if (mOngoingPinchToResize) {
+ onPinchResize(mv);
+ }
+ }
+ }
+
+ /**
+ * Checks if there is currently an on-going gesture, either drag-resize or pinch-resize.
+ */
+ public boolean hasOngoingGesture() {
+ return mCtrlType != CTRL_NONE || mOngoingPinchToResize;
+ }
+
+ public boolean isUsingPinchToZoom() {
+ return mEnablePinchResize;
+ }
+
+ public boolean isResizing() {
+ return mAllowGesture;
+ }
+
+ boolean willStartResizeGesture(MotionEvent ev) {
+ if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
+ if (mEnablePinchResize && ev.getPointerCount() == 2) {
+ onPinchResize(ev);
+ mOngoingPinchToResize = mAllowGesture;
+ return mAllowGesture;
+ }
+ }
+ return false;
+ }
+
+ private boolean isInValidSysUiState() {
+ return mIsSysUiStateValid;
+ }
+
+ private void onHighPerfSessionTimeout(PipPerfHintController.PipHighPerfSession session) {}
+
+ private void cleanUpHighPerfSessionMaybe() {
+ if (mPipHighPerfSession != null) {
+ // Close the high perf session once pointer interactions are over;
+ mPipHighPerfSession.close();
+ mPipHighPerfSession = null;
+ }
+ }
+
+ @VisibleForTesting
+ void onPinchResize(MotionEvent ev) {
+ int action = ev.getActionMasked();
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ mFirstIndex = -1;
+ mSecondIndex = -1;
+ mAllowGesture = false;
+ finishResize();
+ }
+
+ if (ev.getPointerCount() != 2) {
+ return;
+ }
+
+ final Rect pipBounds = mPipBoundsState.getBounds();
+ if (action == MotionEvent.ACTION_POINTER_DOWN) {
+ if (mFirstIndex == -1 && mSecondIndex == -1
+ && pipBounds.contains((int) ev.getRawX(0), (int) ev.getRawY(0))
+ && pipBounds.contains((int) ev.getRawX(1), (int) ev.getRawY(1))) {
+ mAllowGesture = true;
+ mFirstIndex = 0;
+ mSecondIndex = 1;
+ mDownPoint.set(ev.getRawX(mFirstIndex), ev.getRawY(mFirstIndex));
+ mDownSecondPoint.set(ev.getRawX(mSecondIndex), ev.getRawY(mSecondIndex));
+ mDownBounds.set(pipBounds);
+
+ mLastPoint.set(mDownPoint);
+ mLastSecondPoint.set(mLastSecondPoint);
+ mLastResizeBounds.set(mDownBounds);
+
+ // start the high perf session as the second pointer gets detected
+ if (mPipPerfHintController != null) {
+ mPipHighPerfSession = mPipPerfHintController.startSession(
+ this::onHighPerfSessionTimeout, "onPinchResize");
+ }
+ }
+ }
+
+ if (action == MotionEvent.ACTION_MOVE) {
+ if (mFirstIndex == -1 || mSecondIndex == -1) {
+ return;
+ }
+
+ float x0 = ev.getRawX(mFirstIndex);
+ float y0 = ev.getRawY(mFirstIndex);
+ float x1 = ev.getRawX(mSecondIndex);
+ float y1 = ev.getRawY(mSecondIndex);
+ mLastPoint.set(x0, y0);
+ mLastSecondPoint.set(x1, y1);
+
+ // Capture inputs
+ if (!mThresholdCrossed
+ && (distanceBetween(mDownSecondPoint, mLastSecondPoint) > mTouchSlop
+ || distanceBetween(mDownPoint, mLastPoint) > mTouchSlop)) {
+ pilferPointers();
+ mThresholdCrossed = true;
+ // Reset the down to begin resizing from this point
+ mDownPoint.set(mLastPoint);
+ mDownSecondPoint.set(mLastSecondPoint);
+
+ if (mPhonePipMenuController.isMenuVisible()) {
+ mPhonePipMenuController.hideMenu();
+ }
+ }
+
+ if (mThresholdCrossed) {
+ mAngle = mPinchResizingAlgorithm.calculateBoundsAndAngle(mDownPoint,
+ mDownSecondPoint, mLastPoint, mLastSecondPoint, mMinSize, mMaxSize,
+ mDownBounds, mLastResizeBounds);
+
+ mPipScheduler.scheduleUserResizePip(mLastResizeBounds, mAngle);
+ mPipBoundsState.setHasUserResizedPip(true);
+ }
+ }
+ }
+
+ private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) {
+ final int leftEdge = bounds.left;
+
+
+ final int fromLeft = Math.abs(leftEdge - movementBounds.left);
+ final int fromRight = Math.abs(movementBounds.right - leftEdge);
+
+ // The PIP will be snapped to either the right or left edge, so calculate which one
+ // is closest to the current position.
+ final int newLeft = fromLeft < fromRight
+ ? movementBounds.left : movementBounds.right;
+
+ bounds.offsetTo(newLeft, mLastResizeBounds.top);
+ }
+
+ /**
+ * Resizes the pip window and updates user-resized bounds.
+ *
+ * @param bounds target bounds to resize to
+ * @param snapFraction snap fraction to apply after resizing
+ */
+ void userResizeTo(Rect bounds, float snapFraction) {
+ Rect finalBounds = new Rect(bounds);
+
+ // get the current movement bounds
+ final Rect movementBounds = mPipBoundsAlgorithm.getMovementBounds(finalBounds);
+
+ // snap the target bounds to the either left or right edge, by choosing the closer one
+ snapToMovementBoundsEdge(finalBounds, movementBounds);
+
+ // apply the requested snap fraction onto the target bounds
+ mPipBoundsAlgorithm.applySnapFraction(finalBounds, snapFraction);
+
+ // resize from current bounds to target bounds without animation
+ // mPipTaskOrganizer.scheduleUserResizePip(mPipBoundsState.getBounds(), finalBounds, null);
+ // set the flag that pip has been resized
+ mPipBoundsState.setHasUserResizedPip(true);
+
+ // finish the resize operation and update the state of the bounds
+ // mPipTaskOrganizer.scheduleFinishResizePip(finalBounds, mUpdateResizeBoundsCallback);
+ }
+
+ private void finishResize() {
+ if (mLastResizeBounds.isEmpty()) {
+ resetState();
+ }
+ if (!mOngoingPinchToResize) {
+ return;
+ }
+
+ // 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
+ || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) {
+ resizeRectAboutCenter(mLastResizeBounds, mMaxSize.x, mMaxSize.y);
+ }
+
+ // If user resize is smaller than min size, auto resize to min
+ if (mLastResizeBounds.width() < mMinSize.x
+ || mLastResizeBounds.height() < mMinSize.y) {
+ resizeRectAboutCenter(mLastResizeBounds, mMinSize.x, mMinSize.y);
+ }
+
+ // get the current movement bounds
+ final Rect movementBounds = mPipBoundsAlgorithm
+ .getMovementBounds(mLastResizeBounds);
+
+ // snap mLastResizeBounds to the correct edge based on movement bounds
+ snapToMovementBoundsEdge(mLastResizeBounds, movementBounds);
+
+ final float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
+ mLastResizeBounds, movementBounds);
+ mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction);
+
+ // Update the transition state to schedule a resize transition.
+ Bundle extra = new Bundle();
+ extra.putBoolean(RESIZE_BOUNDS_CHANGE, true);
+ mPipTransitionState.setState(PipTransitionState.SCHEDULED_BOUNDS_CHANGE, extra);
+
+ mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE);
+ }
+
+ private void resetState() {
+ mCtrlType = CTRL_NONE;
+ mAngle = 0;
+ mOngoingPinchToResize = false;
+ mAllowGesture = false;
+ mThresholdCrossed = false;
+ }
+
+ void setUserResizeBounds(Rect bounds) {
+ mUserResizeBounds.set(bounds);
+ }
+
+ void invalidateUserResizeBounds() {
+ mUserResizeBounds.setEmpty();
+ }
+
+ Rect getUserResizeBounds() {
+ return mUserResizeBounds;
+ }
+
+ @VisibleForTesting
+ Rect getLastResizeBounds() {
+ return mLastResizeBounds;
+ }
+
+ @VisibleForTesting
+ void pilferPointers() {
+ mInputMonitor.pilferPointers();
+ }
+
+
+ void updateMaxSize(int maxX, int maxY) {
+ mMaxSize.set(maxX, maxY);
+ }
+
+ void updateMinSize(int minX, int minY) {
+ mMinSize.set(minX, minY);
+ }
+
+ void setOhmOffset(int offset) {
+ mOhmOffset = offset;
+ }
+
+ private float distanceBetween(PointF p1, PointF p2) {
+ return (float) Math.hypot(p2.x - p1.x, p2.y - p1.y);
+ }
+
+ private void resizeRectAboutCenter(Rect rect, int w, int h) {
+ int cx = rect.centerX();
+ int cy = rect.centerY();
+ int l = cx - w / 2;
+ int r = l + w;
+ int t = cy - h / 2;
+ int b = t + h;
+ rect.set(l, t, r, b);
+ }
+
+ @Override
+ public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState,
+ @PipTransitionState.TransitionState int newState, @Nullable Bundle extra) {
+ switch (newState) {
+ case PipTransitionState.SCHEDULED_BOUNDS_CHANGE:
+ if (!extra.getBoolean(RESIZE_BOUNDS_CHANGE)) break;
+
+ if (mPipBoundsState.getBounds().equals(mLastResizeBounds)) {
+ // If the bounds are invariant move the destination bounds by a single pixel
+ // to top/bottom to avoid a no-op transition. This trick helps keep the
+ // animation a part of the transition.
+ float snapFraction = mPipBoundsAlgorithm.getSnapFraction(
+ mPipBoundsState.getBounds());
+
+ // Move to the top if closer to the bottom edge and vice versa.
+ boolean inTopHalf = snapFraction < 1.5 || snapFraction > 3.5;
+ int offsetY = inTopHalf ? 1 : -1;
+ mLastResizeBounds.offset(0 /* dx */, offsetY);
+ }
+ mWaitingForBoundsChangeTransition = true;
+
+ // 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 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);
+ 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;
+ }
+ }
+
+ /**
+ * Dumps the {@link PipResizeGestureHandler} state.
+ */
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mAllowGesture=" + mAllowGesture);
+ pw.println(innerPrefix + "mIsAttached=" + mIsAttached);
+ pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled);
+ pw.println(innerPrefix + "mEnablePinchResize=" + mEnablePinchResize);
+ pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed);
+ pw.println(innerPrefix + "mOhmOffset=" + mOhmOffset);
+ pw.println(innerPrefix + "mMinSize=" + mMinSize);
+ pw.println(innerPrefix + "mMaxSize=" + mMaxSize);
+ }
+
+ class PipResizeInputEventReceiver extends BatchedInputEventReceiver {
+ PipResizeInputEventReceiver(InputChannel channel, Looper looper) {
+ super(channel, looper, Choreographer.getInstance());
+ }
+
+ public void onInputEvent(InputEvent event) {
+ PipResizeGestureHandler.this.onInputEvent(event);
+ finishInputEvent(event, true);
+ }
+ }
+}
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 895c793007a5..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
@@ -24,23 +24,24 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.graphics.Matrix;
import android.graphics.Rect;
import android.view.SurfaceControl;
-import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.pip.PipBoundsState;
import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.function.Consumer;
/**
* Scheduler for Shell initiated PiP transitions and animations.
@@ -52,20 +53,10 @@ public class PipScheduler {
private final Context mContext;
private final PipBoundsState mPipBoundsState;
private final ShellExecutor mMainExecutor;
+ private final PipTransitionState mPipTransitionState;
private PipSchedulerReceiver mSchedulerReceiver;
private PipTransitionController mPipTransitionController;
- // pinned PiP task's WC token
- @Nullable
- private WindowContainerToken mPipTaskToken;
-
- // pinned PiP task's leash
- @Nullable
- private SurfaceControl mPinnedTaskLeash;
-
- // true if Launcher has started swipe PiP to home animation
- private boolean mInSwipePipToHomeTransition;
-
/**
* Temporary PiP CUJ codes to schedule PiP related transitions directly from Shell.
* This is used for a broadcast receiver to resolve intents. This should be removed once
@@ -101,11 +92,14 @@ public class PipScheduler {
}
}
- public PipScheduler(Context context, PipBoundsState pipBoundsState,
- ShellExecutor mainExecutor) {
+ public PipScheduler(Context context,
+ PipBoundsState pipBoundsState,
+ ShellExecutor mainExecutor,
+ PipTransitionState pipTransitionState) {
mContext = context;
mPipBoundsState = pipBoundsState;
mMainExecutor = mainExecutor;
+ mPipTransitionState = pipTransitionState;
if (PipUtils.isPip2ExperimentEnabled()) {
// temporary broadcast receiver to initiate exit PiP via expand
@@ -115,29 +109,25 @@ public class PipScheduler {
}
}
- void setPipTransitionController(PipTransitionController pipTransitionController) {
- mPipTransitionController = pipTransitionController;
+ ShellExecutor getMainExecutor() {
+ return mMainExecutor;
}
- void setPinnedTaskLeash(SurfaceControl pinnedTaskLeash) {
- mPinnedTaskLeash = pinnedTaskLeash;
- }
-
- void setPipTaskToken(@Nullable WindowContainerToken pipTaskToken) {
- mPipTaskToken = pipTaskToken;
+ void setPipTransitionController(PipTransitionController pipTransitionController) {
+ mPipTransitionController = pipTransitionController;
}
@Nullable
private WindowContainerTransaction getExitPipViaExpandTransaction() {
- if (mPipTaskToken == null) {
+ if (mPipTransitionState.mPipTaskToken == null) {
return null;
}
WindowContainerTransaction wct = new WindowContainerTransaction();
// final expanded bounds to be inherited from the parent
- wct.setBounds(mPipTaskToken, null);
+ wct.setBounds(mPipTransitionState.mPipTaskToken, null);
// if we are hitting a multi-activity case
// windowing mode change will reparent to original host task
- wct.setWindowingMode(mPipTaskToken, WINDOWING_MODE_UNDEFINED);
+ wct.setWindowingMode(mPipTransitionState.mPipTaskToken, WINDOWING_MODE_UNDEFINED);
return wct;
}
@@ -162,25 +152,78 @@ public class PipScheduler {
/**
* Animates resizing of the pinned stack given the duration.
*/
- public void scheduleAnimateResizePip(Rect toBounds, Consumer<Rect> onFinishResizeCallback) {
- if (mPipTaskToken == null) {
+ 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(mPipTaskToken, toBounds);
- mPipTransitionController.startResizeTransition(wct, onFinishResizeCallback);
+ wct.setBounds(mPipTransitionState.mPipTaskToken, toBounds);
+ if (configAtEnd) {
+ wct.deferConfigToTransitionEnd(mPipTransitionState.mPipTaskToken);
+ }
+ mPipTransitionController.startResizeTransition(wct);
}
- void setInSwipePipToHomeTransition(boolean inSwipePipToHome) {
- mInSwipePipToHomeTransition = true;
+ /**
+ * 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);
}
- boolean isInSwipePipToHomeTransition() {
- return mInSwipePipToHomeTransition;
+ /**
+ * Directly perform a scaled matrix transformation on the leash. This will not perform any
+ * {@link WindowContainerTransaction}.
+ */
+ public void scheduleUserResizePip(Rect toBounds) {
+ scheduleUserResizePip(toBounds, 0f /* degrees */);
}
- void onExitPip() {
- mPipTaskToken = null;
- mPinnedTaskLeash = null;
+ /**
+ * Directly perform a scaled matrix transformation on the leash. This will not perform any
+ * {@link WindowContainerTransaction}.
+ *
+ * @param degrees the angle to rotate the bounds to.
+ */
+ public void scheduleUserResizePip(Rect toBounds, float degrees) {
+ if (toBounds.isEmpty()) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Attempted to user resize PIP to empty bounds, aborting.", TAG);
+ return;
+ }
+ SurfaceControl leash = mPipTransitionState.mPinnedTaskLeash;
+ final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+
+ Matrix transformTensor = new Matrix();
+ final float[] mMatrixTmp = new float[9];
+ final float scale = (float) toBounds.width() / mPipBoundsState.getBounds().width();
+
+ transformTensor.setScale(scale, scale);
+ transformTensor.postTranslate(toBounds.left, toBounds.top);
+ transformTensor.postRotate(degrees, toBounds.centerX(), toBounds.centerY());
+
+ tx.setMatrix(leash, transformTensor, mMatrixTmp);
+ tx.apply();
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java
new file mode 100644
index 000000000000..efa5fc8bf8b1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchGesture.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+/**
+ * A generic interface for a touch gesture.
+ */
+public abstract class PipTouchGesture {
+
+ /**
+ * Handle the touch down.
+ */
+ public void onDown(PipTouchState touchState) {}
+
+ /**
+ * Handle the touch move, and return whether the event was consumed.
+ */
+ public boolean onMove(PipTouchState touchState) {
+ return false;
+ }
+
+ /**
+ * Handle the touch up, and return whether the gesture was consumed.
+ */
+ public boolean onUp(PipTouchState touchState) {
+ return false;
+ }
+
+ /**
+ * Cleans up the high performance hint session if needed.
+ */
+ public void cleanUpHighPerfSessionMaybe() {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
new file mode 100644
index 000000000000..56a465a4889a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
@@ -0,0 +1,1128 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import static android.view.WindowManager.INPUT_CONSUMER_PIP;
+
+import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASHING;
+import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASH_MINIMUM_VELOCITY_THRESHOLD;
+import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_LEFT;
+import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_NONE;
+import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_RIGHT;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
+import static com.android.wm.shell.pip2.phone.PhonePipMenuController.MENU_STATE_FULL;
+import static com.android.wm.shell.pip2.phone.PhonePipMenuController.MENU_STATE_NONE;
+import static com.android.wm.shell.pip2.phone.PipMenuView.ANIM_TYPE_NONE;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.provider.DeviceConfig;
+import android.util.Size;
+import android.view.DisplayCutout;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.WindowManagerGlobal;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityWindowInfo;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
+import com.android.wm.shell.common.FloatingContentCoordinator;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
+import com.android.wm.shell.common.pip.PipBoundsState;
+import com.android.wm.shell.common.pip.PipDoubleTapHelper;
+import com.android.wm.shell.common.pip.PipPerfHintController;
+import com.android.wm.shell.common.pip.PipUiEventLogger;
+import com.android.wm.shell.common.pip.PipUtils;
+import com.android.wm.shell.common.pip.SizeSpecSource;
+import com.android.wm.shell.pip.PipAnimationController;
+import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
+import com.android.wm.shell.sysui.ShellInit;
+
+import java.io.PrintWriter;
+import java.util.Optional;
+
+/**
+ * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding
+ * the PIP.
+ */
+public class PipTouchHandler implements PipTransitionState.PipTransitionStateChangedListener {
+
+ private static final String TAG = "PipTouchHandler";
+ private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f;
+
+ // Allow PIP to resize to a slightly bigger state upon touch
+ private boolean mEnableResize;
+ private final Context mContext;
+ private final ShellCommandHandler mShellCommandHandler;
+ private final PipBoundsAlgorithm mPipBoundsAlgorithm;
+ @NonNull private final PipBoundsState mPipBoundsState;
+ @NonNull private final PipTransitionState mPipTransitionState;
+ @NonNull private final PipScheduler mPipScheduler;
+ @NonNull private final SizeSpecSource mSizeSpecSource;
+ private final PipUiEventLogger mPipUiEventLogger;
+ private final PipDismissTargetHandler mPipDismissTargetHandler;
+ private final ShellExecutor mMainExecutor;
+ @Nullable private final PipPerfHintController mPipPerfHintController;
+
+ private PipResizeGestureHandler mPipResizeGestureHandler;
+
+ private final PhonePipMenuController mMenuController;
+ private final AccessibilityManager mAccessibilityManager;
+
+ /**
+ * Whether PIP stash is enabled or not. When enabled, if the user flings toward the edge of the
+ * screen, it will be shown in "stashed" mode, where PIP will only show partially.
+ */
+ private boolean mEnableStash = true;
+
+ private float mStashVelocityThreshold;
+
+ // The reference inset bounds, used to determine the dismiss fraction
+ private final Rect mInsetBounds = new Rect();
+
+ // Used to workaround an issue where the WM rotation happens before we are notified, allowing
+ // us to send stale bounds
+ private int mDeferResizeToNormalBoundsUntilRotation = -1;
+ private int mDisplayRotation;
+
+ // Behaviour states
+ private int mMenuState = MENU_STATE_NONE;
+ private boolean mIsImeShowing;
+ private int mImeHeight;
+ private int mImeOffset;
+ private boolean mIsShelfShowing;
+ private int mShelfHeight;
+ private int mMovementBoundsExtraOffsets;
+ private int mBottomOffsetBufferPx;
+ private float mSavedSnapFraction = -1f;
+ private boolean mSendingHoverAccessibilityEvents;
+ private boolean mMovementWithinDismiss;
+
+ // Touch state
+ private final PipTouchState mTouchState;
+ private final FloatingContentCoordinator mFloatingContentCoordinator;
+ private PipMotionHelper mMotionHelper;
+ private PipTouchGesture mGesture;
+ private PipInputConsumer mPipInputConsumer;
+
+ // Temp vars
+ private final Rect mTmpBounds = new Rect();
+
+ /**
+ * A listener for the PIP menu activity.
+ */
+ private class PipMenuListener implements PhonePipMenuController.Listener {
+ @Override
+ public void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback) {
+ PipTouchHandler.this.onPipMenuStateChangeStart(menuState, resize, callback);
+ }
+
+ @Override
+ public void onPipMenuStateChangeFinish(int menuState) {
+ setMenuState(menuState);
+ }
+
+ @Override
+ public void onPipExpand() {
+ mMotionHelper.expandLeavePip(false /* skipAnimation */);
+ }
+
+ @Override
+ public void onPipDismiss() {
+ mTouchState.removeDoubleTapTimeoutCallback();
+ mMotionHelper.dismissPip();
+ }
+
+ @Override
+ public void onPipShowMenu() {
+ mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(),
+ true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle());
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ public PipTouchHandler(Context context,
+ ShellInit shellInit,
+ ShellCommandHandler shellCommandHandler,
+ PhonePipMenuController menuController,
+ PipBoundsAlgorithm pipBoundsAlgorithm,
+ @NonNull PipBoundsState pipBoundsState,
+ @NonNull PipTransitionState pipTransitionState,
+ @NonNull PipScheduler pipScheduler,
+ @NonNull SizeSpecSource sizeSpecSource,
+ PipMotionHelper pipMotionHelper,
+ FloatingContentCoordinator floatingContentCoordinator,
+ PipUiEventLogger pipUiEventLogger,
+ ShellExecutor mainExecutor,
+ Optional<PipPerfHintController> pipPerfHintControllerOptional) {
+ mContext = context;
+ mShellCommandHandler = shellCommandHandler;
+ mMainExecutor = mainExecutor;
+ mPipPerfHintController = pipPerfHintControllerOptional.orElse(null);
+ mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+ mPipBoundsAlgorithm = pipBoundsAlgorithm;
+ mPipBoundsState = pipBoundsState;
+
+ mPipTransitionState = pipTransitionState;
+ mPipTransitionState.addPipTransitionStateChangedListener(this::onPipTransitionStateChanged);
+ mPipScheduler = pipScheduler;
+ mSizeSpecSource = sizeSpecSource;
+ mMenuController = menuController;
+ mPipUiEventLogger = pipUiEventLogger;
+ mFloatingContentCoordinator = floatingContentCoordinator;
+ mMenuController.addListener(new PipMenuListener());
+ mGesture = new DefaultPipTouchGesture();
+ mMotionHelper = pipMotionHelper;
+ mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger,
+ mMotionHelper, mainExecutor);
+ mTouchState = new PipTouchState(ViewConfiguration.get(context),
+ () -> {
+ if (mPipBoundsState.isStashed()) {
+ animateToUnStashedState();
+ mPipUiEventLogger.log(
+ PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED);
+ mPipBoundsState.setStashed(STASH_TYPE_NONE);
+ } else {
+ mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL,
+ mPipBoundsState.getBounds(), true /* allowMenuTimeout */,
+ willResizeMenu(),
+ shouldShowResizeHandle());
+ }
+ },
+ menuController::hideMenu,
+ mainExecutor);
+ mPipResizeGestureHandler = new PipResizeGestureHandler(context, pipBoundsAlgorithm,
+ pipBoundsState, mTouchState, mPipScheduler, mPipTransitionState,
+ this::updateMovementBounds, pipUiEventLogger, menuController, mainExecutor,
+ mPipPerfHintController);
+ mPipBoundsState.addOnAspectRatioChangedCallback(this::updateMinMaxSize);
+
+ if (PipUtils.isPip2ExperimentEnabled()) {
+ shellInit.addInitCallback(this::onInit, this);
+ }
+ }
+
+ /**
+ * Called when the touch handler is initialized.
+ */
+ public void onInit() {
+ Resources res = mContext.getResources();
+ mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu);
+ reloadResources();
+
+ mShellCommandHandler.addDumpCallback(this::dump, this);
+ mMotionHelper.init();
+ mPipResizeGestureHandler.init();
+ mPipDismissTargetHandler.init();
+
+ mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(),
+ INPUT_CONSUMER_PIP, mMainExecutor);
+ mPipInputConsumer.setInputListener(this::handleTouchEvent);
+ mPipInputConsumer.setRegistrationListener(this::onRegistrationChanged);
+
+ mEnableStash = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ PIP_STASHING,
+ /* defaultValue = */ true);
+ DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
+ mMainExecutor,
+ properties -> {
+ if (properties.getKeyset().contains(PIP_STASHING)) {
+ mEnableStash = properties.getBoolean(
+ PIP_STASHING, /* defaultValue = */ true);
+ }
+ });
+ mStashVelocityThreshold = DeviceConfig.getFloat(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ PIP_STASH_MINIMUM_VELOCITY_THRESHOLD,
+ DEFAULT_STASH_VELOCITY_THRESHOLD);
+ DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
+ mMainExecutor,
+ properties -> {
+ if (properties.getKeyset().contains(PIP_STASH_MINIMUM_VELOCITY_THRESHOLD)) {
+ mStashVelocityThreshold = properties.getFloat(
+ PIP_STASH_MINIMUM_VELOCITY_THRESHOLD,
+ DEFAULT_STASH_VELOCITY_THRESHOLD);
+ }
+ });
+ }
+
+ public PipTransitionController getTransitionHandler() {
+ // return mPipTaskOrganizer.getTransitionController();
+ return null;
+ }
+
+ private void reloadResources() {
+ final Resources res = mContext.getResources();
+ mBottomOffsetBufferPx = res.getDimensionPixelSize(R.dimen.pip_bottom_offset_buffer);
+ mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
+ mPipDismissTargetHandler.updateMagneticTargetSize();
+ }
+
+ void onOverlayChanged() {
+ // onOverlayChanged is triggered upon theme change, update the dismiss target accordingly.
+ mPipDismissTargetHandler.init();
+ }
+
+ private boolean shouldShowResizeHandle() {
+ return false;
+ }
+
+ void setTouchGesture(PipTouchGesture gesture) {
+ mGesture = gesture;
+ }
+
+ void setTouchEnabled(boolean enabled) {
+ mTouchState.setAllowTouches(enabled);
+ }
+
+ void showPictureInPictureMenu() {
+ // Only show the menu if the user isn't currently interacting with the PiP
+ if (!mTouchState.isUserInteracting()) {
+ mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(),
+ false /* allowMenuTimeout */, willResizeMenu(),
+ shouldShowResizeHandle());
+ }
+ }
+
+ void onActivityPinned() {
+ mPipDismissTargetHandler.createOrUpdateDismissTarget();
+ mPipResizeGestureHandler.onActivityPinned();
+ mFloatingContentCoordinator.onContentAdded(mMotionHelper);
+ mPipInputConsumer.registerInputConsumer();
+ }
+
+ void onActivityUnpinned() {
+ // Clean up state after the last PiP activity is removed
+ mPipDismissTargetHandler.cleanUpDismissTarget();
+ mFloatingContentCoordinator.onContentRemoved(mMotionHelper);
+ mPipResizeGestureHandler.onActivityUnpinned();
+ mPipInputConsumer.unregisterInputConsumer();
+ }
+
+ void onPinnedStackAnimationEnded(
+ @PipAnimationController.TransitionDirection int direction) {
+ // Always synchronize the motion helper bounds once PiP animations finish
+ mMotionHelper.synchronizePinnedStackBounds();
+ updateMovementBounds();
+ if (direction == TRANSITION_DIRECTION_TO_PIP) {
+ // Set the initial bounds as the user resize bounds.
+ mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds());
+ }
+ }
+
+ void onConfigurationChanged() {
+ mPipResizeGestureHandler.onConfigurationChanged();
+ mMotionHelper.synchronizePinnedStackBounds();
+ reloadResources();
+
+ /*
+ if (mPipTaskOrganizer.isInPip()) {
+ // Recreate the dismiss target for the new orientation.
+ mPipDismissTargetHandler.createOrUpdateDismissTarget();
+ }
+ */
+ }
+
+ void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
+ mIsImeShowing = imeVisible;
+ mImeHeight = imeHeight;
+ }
+
+ void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) {
+ mIsShelfShowing = shelfVisible;
+ mShelfHeight = shelfHeight;
+ }
+
+ /**
+ * Called when SysUI state changed.
+ *
+ * @param isSysUiStateValid Is SysUI valid or not.
+ */
+ public void onSystemUiStateChanged(boolean isSysUiStateValid) {
+ mPipResizeGestureHandler.onSystemUiStateChanged(isSysUiStateValid);
+ }
+
+ void adjustBoundsForRotation(Rect outBounds, Rect curBounds, Rect insetBounds) {
+ final Rect toMovementBounds = new Rect();
+ mPipBoundsAlgorithm.getMovementBounds(outBounds, insetBounds, toMovementBounds, 0);
+ final int prevBottom = mPipBoundsState.getMovementBounds().bottom
+ - mMovementBoundsExtraOffsets;
+ if ((prevBottom - mBottomOffsetBufferPx) <= curBounds.top) {
+ outBounds.offsetTo(outBounds.left, toMovementBounds.bottom);
+ }
+ }
+
+ /**
+ * Responds to IPinnedStackListener on resetting aspect ratio for the pinned window.
+ */
+ public void onAspectRatioChanged() {
+ mPipResizeGestureHandler.invalidateUserResizeBounds();
+ }
+
+ void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds,
+ boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation) {
+ // Set the user resized bounds equal to the new normal bounds in case they were
+ // invalidated (e.g. by an aspect ratio change).
+ if (mPipResizeGestureHandler.getUserResizeBounds().isEmpty()) {
+ mPipResizeGestureHandler.setUserResizeBounds(normalBounds);
+ }
+
+ final int bottomOffset = mIsImeShowing ? mImeHeight : 0;
+ final boolean fromDisplayRotationChanged = (mDisplayRotation != displayRotation);
+ if (fromDisplayRotationChanged) {
+ mTouchState.reset();
+ }
+
+ // Re-calculate the expanded bounds
+ Rect normalMovementBounds = new Rect();
+ mPipBoundsAlgorithm.getMovementBounds(normalBounds, insetBounds,
+ normalMovementBounds, bottomOffset);
+
+ if (mPipBoundsState.getMovementBounds().isEmpty()) {
+ // mMovementBounds is not initialized yet and a clean movement bounds without
+ // bottom offset shall be used later in this function.
+ mPipBoundsAlgorithm.getMovementBounds(curBounds, insetBounds,
+ mPipBoundsState.getMovementBounds(), 0 /* bottomOffset */);
+ }
+
+ // Calculate the expanded size
+ float aspectRatio = (float) normalBounds.width() / normalBounds.height();
+ Size expandedSize = mSizeSpecSource.getDefaultSize(aspectRatio);
+ mPipBoundsState.setExpandedBounds(
+ new Rect(0, 0, expandedSize.getWidth(), expandedSize.getHeight()));
+ Rect expandedMovementBounds = new Rect();
+ mPipBoundsAlgorithm.getMovementBounds(
+ mPipBoundsState.getExpandedBounds(), insetBounds, expandedMovementBounds,
+ bottomOffset);
+
+ updatePipSizeConstraints(normalBounds, aspectRatio);
+
+ // The extra offset does not really affect the movement bounds, but are applied based on the
+ // current state (ime showing, or shelf offset) when we need to actually shift
+ int extraOffset = Math.max(
+ mIsImeShowing ? mImeOffset : 0,
+ !mIsImeShowing && mIsShelfShowing ? mShelfHeight : 0);
+
+ // Update the movement bounds after doing the calculations based on the old movement bounds
+ // above
+ mPipBoundsState.setNormalMovementBounds(normalMovementBounds);
+ mPipBoundsState.setExpandedMovementBounds(expandedMovementBounds);
+ mDisplayRotation = displayRotation;
+ mInsetBounds.set(insetBounds);
+ updateMovementBounds();
+ mMovementBoundsExtraOffsets = extraOffset;
+
+ // If we have a deferred resize, apply it now
+ if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) {
+ mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction,
+ mPipBoundsState.getNormalMovementBounds(), mPipBoundsState.getMovementBounds(),
+ true /* immediate */);
+ mSavedSnapFraction = -1f;
+ mDeferResizeToNormalBoundsUntilRotation = -1;
+ }
+ }
+
+ /**
+ * Update the values for min/max allowed size of picture in picture window based on the aspect
+ * ratio.
+ * @param aspectRatio aspect ratio to use for the calculation of min/max size
+ */
+ public void updateMinMaxSize(float aspectRatio) {
+ updatePipSizeConstraints(mPipBoundsState.getNormalBounds(),
+ aspectRatio);
+ }
+
+ private void updatePipSizeConstraints(Rect normalBounds,
+ float aspectRatio) {
+ if (mPipResizeGestureHandler.isUsingPinchToZoom()) {
+ updatePinchResizeSizeConstraints(aspectRatio);
+ } else {
+ mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height());
+ mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(),
+ mPipBoundsState.getExpandedBounds().height());
+ }
+ }
+
+ private void updatePinchResizeSizeConstraints(float aspectRatio) {
+ mPipBoundsState.updateMinMaxSize(aspectRatio);
+ mPipResizeGestureHandler.updateMinSize(mPipBoundsState.getMinSize().x,
+ mPipBoundsState.getMinSize().y);
+ mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getMaxSize().x,
+ mPipBoundsState.getMaxSize().y);
+ }
+
+ /**
+ * TODO Add appropriate description
+ */
+ public void onRegistrationChanged(boolean isRegistered) {
+ if (isRegistered) {
+ // Register the accessibility connection.
+ } else {
+ mAccessibilityManager.setPictureInPictureActionReplacingConnection(null);
+ }
+ if (!isRegistered && mTouchState.isUserInteracting()) {
+ // If the input consumer is unregistered while the user is interacting, then we may not
+ // get the final TOUCH_UP event, so clean up the dismiss target as well
+ mPipDismissTargetHandler.cleanUpDismissTarget();
+ }
+ }
+
+ private void onAccessibilityShowMenu() {
+ mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(),
+ true /* allowMenuTimeout */, willResizeMenu(),
+ shouldShowResizeHandle());
+ }
+
+ /**
+ * TODO Add appropriate description
+ */
+ public boolean handleTouchEvent(InputEvent inputEvent) {
+ // Skip any non motion events
+ if (!(inputEvent instanceof MotionEvent)) {
+ return true;
+ }
+
+ // do not process input event if not allowed
+ if (!mTouchState.getAllowInputEvents()) {
+ return true;
+ }
+
+ MotionEvent ev = (MotionEvent) inputEvent;
+ if (!mPipBoundsState.isStashed() && mPipResizeGestureHandler.willStartResizeGesture(ev)) {
+ // Initialize the touch state for the gesture, but immediately reset to invalidate the
+ // gesture
+ mTouchState.onTouchEvent(ev);
+ mTouchState.reset();
+ return true;
+ }
+
+ if (mPipResizeGestureHandler.hasOngoingGesture()) {
+ mGesture.cleanUpHighPerfSessionMaybe();
+ mPipDismissTargetHandler.hideDismissTargetMaybe();
+ return true;
+ }
+
+ /*
+ if ((ev.getAction() == MotionEvent.ACTION_DOWN || mTouchState.isUserInteracting())
+ && mPipDismissTargetHandler.maybeConsumeMotionEvent(ev)) {
+ // If the first touch event occurs within the magnetic field, pass the ACTION_DOWN event
+ // to the touch state. Touch state needs a DOWN event in order to later process MOVE
+ // events it'll receive if the object is dragged out of the magnetic field.
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mTouchState.onTouchEvent(ev);
+ }
+
+ // Continue tracking velocity when the object is in the magnetic field, since we want to
+ // respect touch input velocity if the object is dragged out and then flung.
+ mTouchState.addMovementToVelocityTracker(ev);
+
+ return true;
+ }
+
+ // Ignore the motion event When the entry animation is waiting to be started
+ if (!mTouchState.isUserInteracting() && mPipTaskOrganizer.isEntryScheduled()) {
+ ProtoLog.wtf(WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Waiting to start the entry animation, skip the motion event.", TAG);
+ return true;
+ }
+ */
+
+ // Update the touch state
+ mTouchState.onTouchEvent(ev);
+
+ boolean shouldDeliverToMenu = mMenuState != MENU_STATE_NONE;
+
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ mGesture.onDown(mTouchState);
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ if (mGesture.onMove(mTouchState)) {
+ break;
+ }
+
+ shouldDeliverToMenu = !mTouchState.isDragging();
+ break;
+ }
+ case MotionEvent.ACTION_UP: {
+ // Update the movement bounds again if the state has changed since the user started
+ // dragging (ie. when the IME shows)
+ updateMovementBounds();
+
+ if (mGesture.onUp(mTouchState)) {
+ break;
+ }
+ }
+ // Fall through to clean up
+ case MotionEvent.ACTION_CANCEL: {
+ shouldDeliverToMenu = !mTouchState.startedDragging() && !mTouchState.isDragging();
+ mTouchState.reset();
+ break;
+ }
+ case MotionEvent.ACTION_HOVER_ENTER: {
+ // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably
+ // on and changing MotionEvents into HoverEvents.
+ // Let's not enable menu show/hide for a11y services.
+ if (!mAccessibilityManager.isTouchExplorationEnabled()) {
+ mTouchState.removeHoverExitTimeoutCallback();
+ mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(),
+ false /* allowMenuTimeout */, false /* willResizeMenu */,
+ shouldShowResizeHandle());
+ }
+ }
+ // Fall through
+ case MotionEvent.ACTION_HOVER_MOVE: {
+ if (!shouldDeliverToMenu && !mSendingHoverAccessibilityEvents) {
+ sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+ mSendingHoverAccessibilityEvents = true;
+ }
+ break;
+ }
+ case MotionEvent.ACTION_HOVER_EXIT: {
+ // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably
+ // on and changing MotionEvents into HoverEvents.
+ // Let's not enable menu show/hide for a11y services.
+ if (!mAccessibilityManager.isTouchExplorationEnabled()) {
+ mTouchState.scheduleHoverExitTimeoutCallback();
+ }
+ if (!shouldDeliverToMenu && mSendingHoverAccessibilityEvents) {
+ sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+ mSendingHoverAccessibilityEvents = false;
+ }
+ break;
+ }
+ }
+
+ shouldDeliverToMenu &= !mPipBoundsState.isStashed();
+
+ // Deliver the event to PipMenuActivity to handle button click if the menu has shown.
+ if (shouldDeliverToMenu) {
+ final MotionEvent cloneEvent = MotionEvent.obtain(ev);
+ // Send the cancel event and cancel menu timeout if it starts to drag.
+ if (mTouchState.startedDragging()) {
+ cloneEvent.setAction(MotionEvent.ACTION_CANCEL);
+ mMenuController.pokeMenu();
+ }
+
+ mMenuController.handlePointerEvent(cloneEvent);
+ cloneEvent.recycle();
+ }
+
+ return true;
+ }
+
+ private void sendAccessibilityHoverEvent(int type) {
+ if (!mAccessibilityManager.isEnabled()) {
+ return;
+ }
+
+ AccessibilityEvent event = AccessibilityEvent.obtain(type);
+ event.setImportantForAccessibility(true);
+ event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID);
+ event.setWindowId(
+ AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID);
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ }
+
+ /**
+ * Called when the PiP menu state is in the process of animating/changing from one to another.
+ */
+ private void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback) {
+ if (mMenuState == menuState && !resize) {
+ return;
+ }
+
+ if (menuState == MENU_STATE_FULL && mMenuState != MENU_STATE_FULL) {
+ // Save the current snap fraction and if we do not drag or move the PiP, then
+ // we store back to this snap fraction. Otherwise, we'll reset the snap
+ // fraction and snap to the closest edge.
+ if (resize) {
+ // PIP is too small to show the menu actions and thus needs to be resized to a
+ // size that can fit them all. Resize to the default size.
+ animateToNormalSize(callback);
+ }
+ } else if (menuState == MENU_STATE_NONE && mMenuState == MENU_STATE_FULL) {
+ // Try and restore the PiP to the closest edge, using the saved snap fraction
+ // if possible
+ if (resize && !mPipResizeGestureHandler.isResizing()) {
+ if (mDeferResizeToNormalBoundsUntilRotation == -1) {
+ // This is a very special case: when the menu is expanded and visible,
+ // navigating to another activity can trigger auto-enter PiP, and if the
+ // revealed activity has a forced rotation set, then the controller will get
+ // updated with the new rotation of the display. However, at the same time,
+ // SystemUI will try to hide the menu by creating an animation to the normal
+ // bounds which are now stale. In such a case we defer the animation to the
+ // normal bounds until after the next onMovementBoundsChanged() call to get the
+ // bounds in the new orientation
+ int displayRotation = mContext.getDisplay().getRotation();
+ if (mDisplayRotation != displayRotation) {
+ mDeferResizeToNormalBoundsUntilRotation = displayRotation;
+ }
+ }
+
+ if (mDeferResizeToNormalBoundsUntilRotation == -1) {
+ animateToUnexpandedState(getUserResizeBounds());
+ }
+ } else {
+ mSavedSnapFraction = -1f;
+ }
+ }
+ }
+
+ private void setMenuState(int menuState) {
+ mMenuState = menuState;
+ updateMovementBounds();
+ // If pip menu has dismissed, we should register the A11y ActionReplacingConnection for pip
+ // as well, or it can't handle a11y focus and pip menu can't perform any action.
+ onRegistrationChanged(menuState == MENU_STATE_NONE);
+ if (menuState == MENU_STATE_NONE) {
+ mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_HIDE_MENU);
+ } else if (menuState == MENU_STATE_FULL) {
+ mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_MENU);
+ }
+ }
+
+ private void animateToMaximizedState(Runnable callback) {
+ Rect maxMovementBounds = new Rect();
+ Rect maxBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x,
+ mPipBoundsState.getMaxSize().y);
+ mPipBoundsAlgorithm.getMovementBounds(maxBounds, mInsetBounds, maxMovementBounds,
+ mIsImeShowing ? mImeHeight : 0);
+ mSavedSnapFraction = mMotionHelper.animateToExpandedState(maxBounds,
+ mPipBoundsState.getMovementBounds(), maxMovementBounds,
+ callback);
+ }
+
+ private void animateToNormalSize(Runnable callback) {
+ // Save the current bounds as the user-resize bounds.
+ mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds());
+
+ final Size minMenuSize = mMenuController.getEstimatedMinMenuSize();
+ final Rect normalBounds = mPipBoundsState.getNormalBounds();
+ final Rect destBounds = mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds,
+ minMenuSize);
+ Rect restoredMovementBounds = new Rect();
+ mPipBoundsAlgorithm.getMovementBounds(destBounds,
+ mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0);
+ mSavedSnapFraction = mMotionHelper.animateToExpandedState(destBounds,
+ mPipBoundsState.getMovementBounds(), restoredMovementBounds, callback);
+ }
+
+ private void animateToUnexpandedState(Rect restoreBounds) {
+ Rect restoredMovementBounds = new Rect();
+ mPipBoundsAlgorithm.getMovementBounds(restoreBounds,
+ mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0);
+ mMotionHelper.animateToUnexpandedState(restoreBounds, mSavedSnapFraction,
+ restoredMovementBounds, mPipBoundsState.getMovementBounds(), false /* immediate */);
+ mSavedSnapFraction = -1f;
+ }
+
+ private void animateToUnStashedState() {
+ final Rect pipBounds = mPipBoundsState.getBounds();
+ final boolean onLeftEdge = pipBounds.left < mPipBoundsState.getDisplayBounds().left;
+ final Rect unStashedBounds = new Rect(0, pipBounds.top, 0, pipBounds.bottom);
+ unStashedBounds.left = onLeftEdge ? mInsetBounds.left
+ : mInsetBounds.right - pipBounds.width();
+ unStashedBounds.right = onLeftEdge ? mInsetBounds.left + pipBounds.width()
+ : mInsetBounds.right;
+ mMotionHelper.animateToUnStashedBounds(unStashedBounds);
+ }
+
+ /**
+ * @return the motion helper.
+ */
+ public PipMotionHelper getMotionHelper() {
+ return mMotionHelper;
+ }
+
+ @VisibleForTesting
+ public PipResizeGestureHandler getPipResizeGestureHandler() {
+ return mPipResizeGestureHandler;
+ }
+
+ @VisibleForTesting
+ public void setPipResizeGestureHandler(PipResizeGestureHandler pipResizeGestureHandler) {
+ mPipResizeGestureHandler = pipResizeGestureHandler;
+ }
+
+ @VisibleForTesting
+ public void setPipMotionHelper(PipMotionHelper pipMotionHelper) {
+ mMotionHelper = pipMotionHelper;
+ }
+
+ Rect getUserResizeBounds() {
+ return mPipResizeGestureHandler.getUserResizeBounds();
+ }
+
+ /**
+ * Sets the user resize bounds tracked by {@link PipResizeGestureHandler}
+ */
+ void setUserResizeBounds(Rect bounds) {
+ mPipResizeGestureHandler.setUserResizeBounds(bounds);
+ }
+
+ /**
+ * Gesture controlling normal movement of the PIP.
+ */
+ private class DefaultPipTouchGesture extends PipTouchGesture {
+ private final Point mStartPosition = new Point();
+ private final PointF mDelta = new PointF();
+ private boolean mShouldHideMenuAfterFling;
+
+ @Nullable private PipPerfHintController.PipHighPerfSession mPipHighPerfSession;
+
+ private void onHighPerfSessionTimeout(PipPerfHintController.PipHighPerfSession session) {}
+
+ @Override
+ public void cleanUpHighPerfSessionMaybe() {
+ if (mPipHighPerfSession != null) {
+ // Close the high perf session once pointer interactions are over;
+ mPipHighPerfSession.close();
+ mPipHighPerfSession = null;
+ }
+ }
+
+ @Override
+ public void onDown(PipTouchState touchState) {
+ if (!touchState.isUserInteracting()) {
+ return;
+ }
+
+ if (mPipPerfHintController != null) {
+ // Cache the PiP high perf session to close it upon touch up.
+ mPipHighPerfSession = mPipPerfHintController.startSession(
+ this::onHighPerfSessionTimeout, "DefaultPipTouchGesture#onDown");
+ }
+
+ Rect bounds = getPossiblyMotionBounds();
+ mDelta.set(0f, 0f);
+ mStartPosition.set(bounds.left, bounds.top);
+ mMovementWithinDismiss = touchState.getDownTouchPosition().y
+ >= mPipBoundsState.getMovementBounds().bottom;
+ mMotionHelper.setSpringingToTouch(false);
+ mPipDismissTargetHandler.setTaskLeash(mPipTransitionState.mPinnedTaskLeash);
+
+ // If the menu is still visible then just poke the menu
+ // so that it will timeout after the user stops touching it
+ if (mMenuState != MENU_STATE_NONE && !mPipBoundsState.isStashed()) {
+ mMenuController.pokeMenu();
+ }
+ }
+
+ @Override
+ public boolean onMove(PipTouchState touchState) {
+ if (!touchState.isUserInteracting()) {
+ return false;
+ }
+
+ if (touchState.startedDragging()) {
+ mSavedSnapFraction = -1f;
+ mPipDismissTargetHandler.showDismissTargetMaybe();
+ }
+
+ if (touchState.isDragging()) {
+ mPipBoundsState.setHasUserMovedPip(true);
+
+ // Move the pinned stack freely
+ final PointF lastDelta = touchState.getLastTouchDelta();
+ float lastX = mStartPosition.x + mDelta.x;
+ float lastY = mStartPosition.y + mDelta.y;
+ float left = lastX + lastDelta.x;
+ float top = lastY + lastDelta.y;
+
+ // Add to the cumulative delta after bounding the position
+ mDelta.x += left - lastX;
+ mDelta.y += top - lastY;
+
+ mTmpBounds.set(getPossiblyMotionBounds());
+ mTmpBounds.offsetTo((int) left, (int) top);
+ mMotionHelper.movePip(mTmpBounds, true /* isDragging */);
+
+ final PointF curPos = touchState.getLastTouchPosition();
+ if (mMovementWithinDismiss) {
+ // Track if movement remains near the bottom edge to identify swipe to dismiss
+ mMovementWithinDismiss = curPos.y >= mPipBoundsState.getMovementBounds().bottom;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onUp(PipTouchState touchState) {
+ mPipDismissTargetHandler.hideDismissTargetMaybe();
+ mPipDismissTargetHandler.setTaskLeash(null);
+
+ if (!touchState.isUserInteracting()) {
+ return false;
+ }
+
+ final PointF vel = touchState.getVelocity();
+
+ if (touchState.isDragging()) {
+ if (mMenuState != MENU_STATE_NONE) {
+ // If the menu is still visible, then just poke the menu so that
+ // it will timeout after the user stops touching it
+ mMenuController.showMenu(mMenuState, mPipBoundsState.getBounds(),
+ true /* allowMenuTimeout */, willResizeMenu(),
+ shouldShowResizeHandle());
+ }
+ mShouldHideMenuAfterFling = mMenuState == MENU_STATE_NONE;
+
+ // Reset the touch state on up before the fling settles
+ mTouchState.reset();
+ if (mEnableStash && shouldStash(vel, getPossiblyMotionBounds())) {
+ // mMotionHelper.stashToEdge(vel.x, vel.y,
+ // this::stashEndAction /* endAction */);
+ } else {
+ if (mPipBoundsState.isStashed()) {
+ // Reset stashed state if previously stashed
+ mPipUiEventLogger.log(
+ PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED);
+ mPipBoundsState.setStashed(STASH_TYPE_NONE);
+ }
+ mMotionHelper.flingToSnapTarget(vel.x, vel.y,
+ this::flingEndAction /* endAction */);
+ }
+ } else if (mTouchState.isDoubleTap() && !mPipBoundsState.isStashed()
+ && mMenuState != MENU_STATE_FULL) {
+ // If using pinch to zoom, double-tap functions as resizing between max/min size
+ if (mPipResizeGestureHandler.isUsingPinchToZoom()) {
+ final boolean toExpand = mPipBoundsState.getBounds().width()
+ < mPipBoundsState.getMaxSize().x
+ && mPipBoundsState.getBounds().height()
+ < mPipBoundsState.getMaxSize().y;
+ if (mMenuController.isMenuVisible()) {
+ mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */);
+ }
+
+ // the size to toggle to after a double tap
+ int nextSize = PipDoubleTapHelper
+ .nextSizeSpec(mPipBoundsState, getUserResizeBounds());
+
+ // actually toggle to the size chosen
+ if (nextSize == PipDoubleTapHelper.SIZE_SPEC_MAX) {
+ mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds());
+ animateToMaximizedState(null);
+ } else if (nextSize == PipDoubleTapHelper.SIZE_SPEC_DEFAULT) {
+ mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds());
+ animateToNormalSize(null);
+ } else {
+ animateToUnexpandedState(getUserResizeBounds());
+ }
+ } else {
+ // Expand to fullscreen if this is a double tap
+ // the PiP should be frozen until the transition ends
+ setTouchEnabled(false);
+ mMotionHelper.expandLeavePip(false /* skipAnimation */);
+ }
+ } else if (mMenuState != MENU_STATE_FULL) {
+ if (mPipBoundsState.isStashed()) {
+ // Unstash immediately if stashed, and don't wait for the double tap timeout
+ animateToUnStashedState();
+ mPipUiEventLogger.log(
+ PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED);
+ mPipBoundsState.setStashed(STASH_TYPE_NONE);
+ mTouchState.removeDoubleTapTimeoutCallback();
+ } else if (!mTouchState.isWaitingForDoubleTap()) {
+ // User has stalled long enough for this not to be a drag or a double tap,
+ // just expand the menu
+ mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(),
+ true /* allowMenuTimeout */, willResizeMenu(),
+ shouldShowResizeHandle());
+ } else {
+ // Next touch event _may_ be the second tap for the double-tap, schedule a
+ // fallback runnable to trigger the menu if no touch event occurs before the
+ // next tap
+ mTouchState.scheduleDoubleTapTimeoutCallback();
+ }
+ }
+ cleanUpHighPerfSessionMaybe();
+ return true;
+ }
+
+ private void stashEndAction() {
+ if (mPipBoundsState.getBounds().left < 0
+ && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT) {
+ mPipUiEventLogger.log(
+ PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_LEFT);
+ mPipBoundsState.setStashed(STASH_TYPE_LEFT);
+ } else if (mPipBoundsState.getBounds().left >= 0
+ && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) {
+ mPipUiEventLogger.log(
+ PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_RIGHT);
+ mPipBoundsState.setStashed(STASH_TYPE_RIGHT);
+ }
+ mMenuController.hideMenu();
+ }
+
+ private void flingEndAction() {
+ if (mShouldHideMenuAfterFling) {
+ // If the menu is not visible, then we can still be showing the activity for the
+ // dismiss overlay, so just finish it after the animation completes
+ mMenuController.hideMenu();
+ }
+ }
+
+ private boolean shouldStash(PointF vel, Rect motionBounds) {
+ final boolean flingToLeft = vel.x < -mStashVelocityThreshold;
+ final boolean flingToRight = vel.x > mStashVelocityThreshold;
+ final int offset = motionBounds.width() / 2;
+ final boolean droppingOnLeft =
+ motionBounds.left < mPipBoundsState.getDisplayBounds().left - offset;
+ final boolean droppingOnRight =
+ motionBounds.right > mPipBoundsState.getDisplayBounds().right + offset;
+
+ // Do not allow stash if the destination edge contains display cutout. We only
+ // compare the left and right edges since we do not allow stash on top / bottom.
+ final DisplayCutout displayCutout =
+ mPipBoundsState.getDisplayLayout().getDisplayCutout();
+ if (displayCutout != null) {
+ if ((flingToLeft || droppingOnLeft)
+ && !displayCutout.getBoundingRectLeft().isEmpty()) {
+ return false;
+ } else if ((flingToRight || droppingOnRight)
+ && !displayCutout.getBoundingRectRight().isEmpty()) {
+ return false;
+ }
+ }
+
+ // If user flings the PIP window above the minimum velocity, stash PIP.
+ // Only allow stashing to the edge if PIP wasn't previously stashed on the opposite
+ // edge.
+ final boolean stashFromFlingToEdge =
+ (flingToLeft && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT)
+ || (flingToRight && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT);
+
+ // If User releases the PIP window while it's out of the display bounds, put
+ // PIP into stashed mode.
+ final boolean stashFromDroppingOnEdge = droppingOnLeft || droppingOnRight;
+
+ return stashFromFlingToEdge || stashFromDroppingOnEdge;
+ }
+ }
+
+ /**
+ * Updates the current movement bounds based on whether the menu is currently visible and
+ * resized.
+ */
+ private void updateMovementBounds() {
+ mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(),
+ mInsetBounds, mPipBoundsState.getMovementBounds(), mIsImeShowing ? mImeHeight : 0);
+ mMotionHelper.onMovementBoundsChanged();
+ }
+
+ private Rect getMovementBounds(Rect curBounds) {
+ Rect movementBounds = new Rect();
+ mPipBoundsAlgorithm.getMovementBounds(curBounds, mInsetBounds,
+ movementBounds, mIsImeShowing ? mImeHeight : 0);
+ return movementBounds;
+ }
+
+ /**
+ * @return {@code true} if the menu should be resized on tap because app explicitly specifies
+ * PiP window size that is too small to hold all the actions.
+ */
+ private boolean willResizeMenu() {
+ if (!mEnableResize) {
+ return false;
+ }
+ final Size estimatedMinMenuSize = mMenuController.getEstimatedMinMenuSize();
+ if (estimatedMinMenuSize == null) {
+ ProtoLog.wtf(WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Failed to get estimated menu size", TAG);
+ return false;
+ }
+ final Rect currentBounds = mPipBoundsState.getBounds();
+ return currentBounds.width() < estimatedMinMenuSize.getWidth()
+ || currentBounds.height() < estimatedMinMenuSize.getHeight();
+ }
+
+ /**
+ * Returns the PIP bounds if we're not in the middle of a motion operation, or the current,
+ * temporary motion bounds otherwise.
+ */
+ Rect getPossiblyMotionBounds() {
+ return mPipBoundsState.getMotionBoundsState().isInMotion()
+ ? mPipBoundsState.getMotionBoundsState().getBoundsInMotion()
+ : mPipBoundsState.getBounds();
+ }
+
+ void setOhmOffset(int offset) {
+ mPipResizeGestureHandler.setOhmOffset(offset);
+ }
+
+ @Override
+ public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState,
+ @PipTransitionState.TransitionState int newState,
+ @Nullable Bundle extra) {
+ switch (newState) {
+ case PipTransitionState.ENTERED_PIP:
+ onActivityPinned();
+ mTouchState.setAllowInputEvents(true);
+ break;
+ case PipTransitionState.EXITED_PIP:
+ mTouchState.setAllowInputEvents(false);
+ onActivityUnpinned();
+ break;
+ case PipTransitionState.SCHEDULED_BOUNDS_CHANGE:
+ mTouchState.setAllowInputEvents(false);
+ break;
+ case PipTransitionState.CHANGED_PIP_BOUNDS:
+ mTouchState.setAllowInputEvents(true);
+ break;
+ }
+ }
+
+ /**
+ * Dumps the {@link PipTouchHandler} state.
+ */
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mMenuState=" + mMenuState);
+ pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing);
+ pw.println(innerPrefix + "mImeHeight=" + mImeHeight);
+ pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing);
+ pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight);
+ pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction);
+ pw.println(innerPrefix + "mMovementBoundsExtraOffsets=" + mMovementBoundsExtraOffsets);
+ mPipBoundsAlgorithm.dump(pw, innerPrefix);
+ mTouchState.dump(pw, innerPrefix);
+ if (mPipResizeGestureHandler != null) {
+ mPipResizeGestureHandler.dump(pw, innerPrefix);
+ }
+ }
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java
new file mode 100644
index 000000000000..d093f1e5ccc1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchState.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.pip2.phone;
+
+import android.graphics.PointF;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+
+import java.io.PrintWriter;
+
+/**
+ * This keeps track of the touch state throughout the current touch gesture.
+ */
+public class PipTouchState {
+ private static final String TAG = "PipTouchState";
+ private static final boolean DEBUG = false;
+
+ @VisibleForTesting
+ public static final long DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
+ static final long HOVER_EXIT_TIMEOUT = 50;
+
+ private final ShellExecutor mMainExecutor;
+ private final ViewConfiguration mViewConfig;
+ private final Runnable mDoubleTapTimeoutCallback;
+ private final Runnable mHoverExitTimeoutCallback;
+
+ private VelocityTracker mVelocityTracker;
+ private long mDownTouchTime = 0;
+ private long mLastDownTouchTime = 0;
+ private long mUpTouchTime = 0;
+ private final PointF mDownTouch = new PointF();
+ private final PointF mDownDelta = new PointF();
+ private final PointF mLastTouch = new PointF();
+ private final PointF mLastDelta = new PointF();
+ private final PointF mVelocity = new PointF();
+ private boolean mAllowTouches = true;
+
+ // Set to false to block both PipTouchHandler and PipResizeGestureHandler's input processing
+ private boolean mAllowInputEvents = true;
+ private boolean mIsUserInteracting = false;
+ // Set to true only if the multiple taps occur within the double tap timeout
+ private boolean mIsDoubleTap = false;
+ // Set to true only if a gesture
+ private boolean mIsWaitingForDoubleTap = false;
+ private boolean mIsDragging = false;
+ // The previous gesture was a drag
+ private boolean mPreviouslyDragging = false;
+ private boolean mStartedDragging = false;
+ private boolean mAllowDraggingOffscreen = false;
+ private int mActivePointerId;
+ private int mLastTouchDisplayId = Display.INVALID_DISPLAY;
+
+ public PipTouchState(ViewConfiguration viewConfig, Runnable doubleTapTimeoutCallback,
+ Runnable hoverExitTimeoutCallback, ShellExecutor mainExecutor) {
+ mViewConfig = viewConfig;
+ mDoubleTapTimeoutCallback = doubleTapTimeoutCallback;
+ mHoverExitTimeoutCallback = hoverExitTimeoutCallback;
+ mMainExecutor = mainExecutor;
+ }
+
+ /**
+ * @return true if input processing is enabled for PiP in general.
+ */
+ public boolean getAllowInputEvents() {
+ return mAllowInputEvents;
+ }
+
+ /**
+ * @param allowInputEvents true to enable input processing for PiP in general.
+ */
+ public void setAllowInputEvents(boolean allowInputEvents) {
+ mAllowInputEvents = allowInputEvents;
+ }
+
+ /**
+ * Resets this state.
+ */
+ public void reset() {
+ mAllowDraggingOffscreen = false;
+ mIsDragging = false;
+ mStartedDragging = false;
+ mIsUserInteracting = false;
+ mLastTouchDisplayId = Display.INVALID_DISPLAY;
+ }
+
+ /**
+ * Processes a given touch event and updates the state.
+ */
+ public void onTouchEvent(MotionEvent ev) {
+ mLastTouchDisplayId = ev.getDisplayId();
+ switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN: {
+ if (!mAllowTouches) {
+ return;
+ }
+
+ // Initialize the velocity tracker
+ initOrResetVelocityTracker();
+ addMovementToVelocityTracker(ev);
+
+ mActivePointerId = ev.getPointerId(0);
+ if (DEBUG) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Setting active pointer id on DOWN: %d", TAG, mActivePointerId);
+ }
+ mLastTouch.set(ev.getRawX(), ev.getRawY());
+ mDownTouch.set(mLastTouch);
+ mAllowDraggingOffscreen = true;
+ mIsUserInteracting = true;
+ mDownTouchTime = ev.getEventTime();
+ mIsDoubleTap = !mPreviouslyDragging
+ && (mDownTouchTime - mLastDownTouchTime) < DOUBLE_TAP_TIMEOUT;
+ mIsWaitingForDoubleTap = false;
+ mIsDragging = false;
+ mLastDownTouchTime = mDownTouchTime;
+ if (mDoubleTapTimeoutCallback != null) {
+ mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback);
+ }
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ // Skip event if we did not start processing this touch gesture
+ if (!mIsUserInteracting) {
+ break;
+ }
+
+ // Update the velocity tracker
+ addMovementToVelocityTracker(ev);
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == -1) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Invalid active pointer id on MOVE: %d", TAG, mActivePointerId);
+ break;
+ }
+
+ float x = ev.getRawX(pointerIndex);
+ float y = ev.getRawY(pointerIndex);
+ mLastDelta.set(x - mLastTouch.x, y - mLastTouch.y);
+ mDownDelta.set(x - mDownTouch.x, y - mDownTouch.y);
+
+ boolean hasMovedBeyondTap = mDownDelta.length() > mViewConfig.getScaledTouchSlop();
+ if (!mIsDragging) {
+ if (hasMovedBeyondTap) {
+ mIsDragging = true;
+ mStartedDragging = true;
+ }
+ } else {
+ mStartedDragging = false;
+ }
+ mLastTouch.set(x, y);
+ break;
+ }
+ case MotionEvent.ACTION_POINTER_UP: {
+ // Skip event if we did not start processing this touch gesture
+ if (!mIsUserInteracting) {
+ break;
+ }
+
+ // Update the velocity tracker
+ addMovementToVelocityTracker(ev);
+
+ int pointerIndex = ev.getActionIndex();
+ int pointerId = ev.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // Select a new active pointer id and reset the movement state
+ final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
+ mActivePointerId = ev.getPointerId(newPointerIndex);
+ if (DEBUG) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Relinquish active pointer id on POINTER_UP: %d",
+ TAG, mActivePointerId);
+ }
+ mLastTouch.set(ev.getRawX(newPointerIndex), ev.getRawY(newPointerIndex));
+ }
+ break;
+ }
+ case MotionEvent.ACTION_UP: {
+ // Skip event if we did not start processing this touch gesture
+ if (!mIsUserInteracting) {
+ break;
+ }
+
+ // Update the velocity tracker
+ addMovementToVelocityTracker(ev);
+ mVelocityTracker.computeCurrentVelocity(1000,
+ mViewConfig.getScaledMaximumFlingVelocity());
+ mVelocity.set(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
+
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == -1) {
+ ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Invalid active pointer id on UP: %d", TAG, mActivePointerId);
+ break;
+ }
+
+ mUpTouchTime = ev.getEventTime();
+ mLastTouch.set(ev.getRawX(pointerIndex), ev.getRawY(pointerIndex));
+ mPreviouslyDragging = mIsDragging;
+ mIsWaitingForDoubleTap = !mIsDoubleTap && !mIsDragging
+ && (mUpTouchTime - mDownTouchTime) < DOUBLE_TAP_TIMEOUT;
+
+ }
+ // fall through to clean up
+ case MotionEvent.ACTION_CANCEL: {
+ recycleVelocityTracker();
+ break;
+ }
+ case MotionEvent.ACTION_BUTTON_PRESS: {
+ removeHoverExitTimeoutCallback();
+ break;
+ }
+ }
+ }
+
+ /**
+ * @return the velocity of the active touch pointer at the point it is lifted off the screen.
+ */
+ public PointF getVelocity() {
+ return mVelocity;
+ }
+
+ /**
+ * @return the last touch position of the active pointer.
+ */
+ public PointF getLastTouchPosition() {
+ return mLastTouch;
+ }
+
+ /**
+ * @return the movement delta between the last handled touch event and the previous touch
+ * position.
+ */
+ public PointF getLastTouchDelta() {
+ return mLastDelta;
+ }
+
+ /**
+ * @return the down touch position.
+ */
+ public PointF getDownTouchPosition() {
+ return mDownTouch;
+ }
+
+ /**
+ * @return the movement delta between the last handled touch event and the down touch
+ * position.
+ */
+ public PointF getDownTouchDelta() {
+ return mDownDelta;
+ }
+
+ /**
+ * @return whether the user has started dragging.
+ */
+ public boolean isDragging() {
+ return mIsDragging;
+ }
+
+ /**
+ * @return whether the user is currently interacting with the PiP.
+ */
+ public boolean isUserInteracting() {
+ return mIsUserInteracting;
+ }
+
+ /**
+ * @return whether the user has started dragging just in the last handled touch event.
+ */
+ public boolean startedDragging() {
+ return mStartedDragging;
+ }
+
+ /**
+ * @return Display ID of the last touch event.
+ */
+ public int getLastTouchDisplayId() {
+ return mLastTouchDisplayId;
+ }
+
+ /**
+ * Sets whether touching is currently allowed.
+ */
+ public void setAllowTouches(boolean allowTouches) {
+ mAllowTouches = allowTouches;
+
+ // If the user happens to touch down before this is sent from the system during a transition
+ // then block any additional handling by resetting the state now
+ if (mIsUserInteracting) {
+ reset();
+ }
+ }
+
+ /**
+ * Disallows dragging offscreen for the duration of the current gesture.
+ */
+ public void setDisallowDraggingOffscreen() {
+ mAllowDraggingOffscreen = false;
+ }
+
+ /**
+ * @return whether dragging offscreen is allowed during this gesture.
+ */
+ public boolean allowDraggingOffscreen() {
+ return mAllowDraggingOffscreen;
+ }
+
+ /**
+ * @return whether this gesture is a double-tap.
+ */
+ public boolean isDoubleTap() {
+ return mIsDoubleTap;
+ }
+
+ /**
+ * @return whether this gesture will potentially lead to a following double-tap.
+ */
+ public boolean isWaitingForDoubleTap() {
+ return mIsWaitingForDoubleTap;
+ }
+
+ /**
+ * Schedules the callback to run if the next double tap does not occur. Only runs if
+ * isWaitingForDoubleTap() is true.
+ */
+ public void scheduleDoubleTapTimeoutCallback() {
+ if (mIsWaitingForDoubleTap) {
+ long delay = getDoubleTapTimeoutCallbackDelay();
+ mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback);
+ mMainExecutor.executeDelayed(mDoubleTapTimeoutCallback, delay);
+ }
+ }
+
+ long getDoubleTapTimeoutCallbackDelay() {
+ if (mIsWaitingForDoubleTap) {
+ return Math.max(0, DOUBLE_TAP_TIMEOUT - (mUpTouchTime - mDownTouchTime));
+ }
+ return -1;
+ }
+
+ /**
+ * Removes the timeout callback if it's in queue.
+ */
+ public void removeDoubleTapTimeoutCallback() {
+ mIsWaitingForDoubleTap = false;
+ mMainExecutor.removeCallbacks(mDoubleTapTimeoutCallback);
+ }
+
+ void scheduleHoverExitTimeoutCallback() {
+ mMainExecutor.removeCallbacks(mHoverExitTimeoutCallback);
+ mMainExecutor.executeDelayed(mHoverExitTimeoutCallback, HOVER_EXIT_TIMEOUT);
+ }
+
+ void removeHoverExitTimeoutCallback() {
+ mMainExecutor.removeCallbacks(mHoverExitTimeoutCallback);
+ }
+
+ void addMovementToVelocityTracker(MotionEvent event) {
+ if (mVelocityTracker == null) {
+ return;
+ }
+
+ // Add movement to velocity tracker using raw screen X and Y coordinates instead
+ // of window coordinates because the window frame may be moving at the same time.
+ float deltaX = event.getRawX() - event.getX();
+ float deltaY = event.getRawY() - event.getY();
+ event.offsetLocation(deltaX, deltaY);
+ mVelocityTracker.addMovement(event);
+ event.offsetLocation(-deltaX, -deltaY);
+ }
+
+ private void initOrResetVelocityTracker() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ } else {
+ mVelocityTracker.clear();
+ }
+ }
+
+ private void recycleVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ /**
+ * Dumps the {@link PipTouchState}.
+ */
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.println(prefix + TAG);
+ pw.println(innerPrefix + "mAllowTouches=" + mAllowTouches);
+ pw.println(innerPrefix + "mAllowInputEvents=" + mAllowInputEvents);
+ pw.println(innerPrefix + "mActivePointerId=" + mActivePointerId);
+ pw.println(innerPrefix + "mLastTouchDisplayId=" + mLastTouchDisplayId);
+ pw.println(innerPrefix + "mDownTouch=" + mDownTouch);
+ pw.println(innerPrefix + "mDownDelta=" + mDownDelta);
+ pw.println(innerPrefix + "mLastTouch=" + mLastTouch);
+ pw.println(innerPrefix + "mLastDelta=" + mLastDelta);
+ pw.println(innerPrefix + "mVelocity=" + mVelocity);
+ pw.println(innerPrefix + "mIsUserInteracting=" + mIsUserInteracting);
+ pw.println(innerPrefix + "mIsDragging=" + mIsDragging);
+ pw.println(innerPrefix + "mStartedDragging=" + mStartedDragging);
+ pw.println(innerPrefix + "mAllowDraggingOffscreen=" + mAllowDraggingOffscreen);
+ }
+}
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 dfb04758c851..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
@@ -17,18 +17,24 @@
package com.android.wm.shell.pip2.phone;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_PIP;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.PictureInPictureParams;
import android.content.Context;
import android.graphics.Rect;
+import android.os.Bundle;
import android.os.IBinder;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
@@ -38,28 +44,52 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.Nullable;
-import com.android.wm.shell.R;
+import com.android.internal.util.Preconditions;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
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;
-import java.util.function.Consumer;
-
/**
* Implementation of transitions for PiP on phone.
*/
-public class PipTransition extends PipTransitionController {
+public class PipTransition extends PipTransitionController implements
+ PipTransitionState.PipTransitionStateChangedListener {
private static final String TAG = PipTransition.class.getSimpleName();
+ // Used when for ENTERING_PIP state update.
+ private static final String PIP_TASK_TOKEN = "pip_task_token";
+ private static final String PIP_TASK_LEASH = "pip_task_leash";
+
+ // Used for PiP CHANGING_BOUNDS state update.
+ static final String PIP_START_TX = "pip_start_tx";
+ static final String PIP_FINISH_TX = "pip_finish_tx";
+ static final String PIP_DESTINATION_BOUNDS = "pip_dest_bounds";
+
+ /**
+ * The fixed start delay in ms when fading out the content overlay from bounds animation.
+ * The fadeout animation is guaranteed to start after the client has drawn under the new config.
+ */
+ private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 400;
+
+ //
+ // Dependencies
+ //
+
private final Context mContext;
private final PipScheduler mPipScheduler;
- @Nullable
- private WindowContainerToken mPipTaskToken;
+ private final PipTransitionState mPipTransitionState;
+
+ //
+ // Transition tokens
+ //
+
@Nullable
private IBinder mEnterTransition;
@Nullable
@@ -67,7 +97,16 @@ public class PipTransition extends PipTransitionController {
@Nullable
private IBinder mResizeTransition;
- private Consumer<Rect> mFinishResizeCallback;
+ //
+ // Internal state and relevant cached info
+ //
+
+ @Nullable
+ private WindowContainerToken mPipTaskToken;
+ @Nullable
+ private SurfaceControl mPipLeash;
+ @Nullable
+ private Transitions.TransitionFinishCallback mFinishCallback;
public PipTransition(
Context context,
@@ -77,13 +116,16 @@ public class PipTransition extends PipTransitionController {
PipBoundsState pipBoundsState,
PipMenuController pipMenuController,
PipBoundsAlgorithm pipBoundsAlgorithm,
- PipScheduler pipScheduler) {
+ PipScheduler pipScheduler,
+ PipTransitionState pipTransitionState) {
super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController,
pipBoundsAlgorithm);
mContext = context;
mPipScheduler = pipScheduler;
mPipScheduler.setPipTransitionController(this);
+ mPipTransitionState = pipTransitionState;
+ mPipTransitionState.addPipTransitionStateChangedListener(this);
}
@Override
@@ -93,6 +135,10 @@ public class PipTransition extends PipTransitionController {
}
}
+ //
+ // Transition collection stage lifecycle hooks
+ //
+
@Override
public void startExitTransition(int type, WindowContainerTransaction out,
@Nullable Rect destinationBounds) {
@@ -106,13 +152,11 @@ public class PipTransition extends PipTransitionController {
}
@Override
- public void startResizeTransition(WindowContainerTransaction wct,
- Consumer<Rect> onFinishResizeCallback) {
+ public void startResizeTransition(WindowContainerTransaction wct) {
if (wct == null) {
return;
}
mResizeTransition = mTransitions.startTransition(TRANSIT_RESIZE_PIP, wct, this);
- mFinishResizeCallback = onFinishResizeCallback;
}
@Nullable
@@ -135,6 +179,10 @@ public class PipTransition extends PipTransitionController {
}
}
+ //
+ // Transition playing stage lifecycle hooks
+ //
+
@Override
public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
@@ -150,9 +198,21 @@ public class PipTransition extends PipTransitionController {
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
- if (transition == mEnterTransition) {
+ if (transition == mEnterTransition || info.getType() == TRANSIT_PIP) {
mEnterTransition = null;
- if (mPipScheduler.isInSwipePipToHomeTransition()) {
+ // If we are in swipe PiP to Home transition we are ENTERING_PIP as a jumpcut transition
+ // is being carried out.
+ TransitionInfo.Change pipChange = getPipChange(info);
+
+ // If there is no PiP change, exit this transition handler and potentially try others.
+ if (pipChange == null) return false;
+
+ Bundle extra = new Bundle();
+ extra.putParcelable(PIP_TASK_TOKEN, pipChange.getContainer());
+ extra.putParcelable(PIP_TASK_LEASH, pipChange.getLeash());
+ mPipTransitionState.setState(PipTransitionState.ENTERING_PIP, extra);
+
+ if (mPipTransitionState.isInSwipePipToHomeTransition()) {
// If this is the second transition as a part of swipe PiP to home cuj,
// handle this transition as a special case with no-op animation.
return handleSwipePipToHomeTransition(info, startTransaction, finishTransaction,
@@ -168,14 +228,23 @@ public class PipTransition extends PipTransitionController {
finishCallback);
} else if (transition == mExitViaExpandTransition) {
mExitViaExpandTransition = null;
+ mPipTransitionState.setState(PipTransitionState.EXITING_PIP);
return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback);
} else if (transition == mResizeTransition) {
mResizeTransition = null;
return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback);
}
+
+ if (isRemovePipTransition(info)) {
+ return removePipImmediately(info, startTransaction, finishTransaction, finishCallback);
+ }
return false;
}
+ //
+ // Animation schedulers and entry points
+ //
+
private boolean startResizeAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@@ -185,31 +254,27 @@ public class PipTransition extends PipTransitionController {
return false;
}
SurfaceControl pipLeash = pipChange.getLeash();
- Rect destinationBounds = pipChange.getEndAbsBounds();
// Even though the final bounds and crop are applied with finishTransaction since
// this is a visible change, we still need to handle the app draw coming in. Snapshot
// covering app draw during collection will be removed by startTransaction. So we make
- // the crop equal to the final bounds and then scale the leash back to starting bounds.
+ // the crop equal to the final bounds and then let the current
+ // animator scale the leash back to starting bounds.
+ // Note: animator is responsible for applying the startTx but NOT finishTx.
startTransaction.setWindowCrop(pipLeash, pipChange.getEndAbsBounds().width(),
pipChange.getEndAbsBounds().height());
- startTransaction.setScale(pipLeash,
- (float) mPipBoundsState.getBounds().width() / destinationBounds.width(),
- (float) mPipBoundsState.getBounds().height() / destinationBounds.height());
- startTransaction.apply();
-
- finishTransaction.setScale(pipLeash,
- (float) mPipBoundsState.getBounds().width() / destinationBounds.width(),
- (float) mPipBoundsState.getBounds().height() / destinationBounds.height());
- // We are done with the transition, but will continue animating leash to final bounds.
- finishCallback.onTransitionFinished(null);
-
- // Animate the pip leash with the new buffer
- final int duration = mContext.getResources().getInteger(
- R.integer.config_pipResizeAnimationDuration);
// TODO: b/275910498 Couple this routine with a new implementation of the PiP animator.
- startResizeAnimation(pipLeash, mPipBoundsState.getBounds(), destinationBounds, duration);
+ // Classes interested in continuing the animation would subscribe to this state update
+ // getting info such as endBounds, startTx, and finishTx as an extra Bundle once
+ // animators are in place. Once done state needs to be updated to CHANGED_PIP_BOUNDS.
+ Bundle extra = new Bundle();
+ extra.putParcelable(PIP_START_TX, startTransaction);
+ extra.putParcelable(PIP_FINISH_TX, finishTransaction);
+ extra.putParcelable(PIP_DESTINATION_BOUNDS, pipChange.getEndAbsBounds());
+
+ mFinishCallback = finishCallback;
+ mPipTransitionState.setState(PipTransitionState.CHANGING_PIP_BOUNDS, extra);
return true;
}
@@ -221,17 +286,80 @@ public class PipTransition extends PipTransitionController {
if (pipChange == null) {
return false;
}
- mPipScheduler.setInSwipePipToHomeTransition(false);
- mPipTaskToken = pipChange.getContainer();
+ WindowContainerToken pipTaskToken = pipChange.getContainer();
+ SurfaceControl pipLeash = pipChange.getLeash();
- // cache the PiP task token and leash
- mPipScheduler.setPipTaskToken(mPipTaskToken);
+ if (pipTaskToken == null || pipLeash == null) {
+ return false;
+ }
+
+ SurfaceControl overlayLeash = mPipTransitionState.getSwipePipToHomeOverlay();
+ PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams;
+
+ 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();
+ 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);
+
+ // Overlay needs to be adjusted once a new draw comes in resetting surface transform.
+ tx.setScale(overlayLeash, 1f, 1f);
+ tx.setPosition(overlayLeash, (destinationBounds.width() - overlaySize) / 2f,
+ (destinationBounds.height() - overlaySize) / 2f);
+ }
startTransaction.apply();
- finishCallback.onTransitionFinished(null);
+
+ tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(),
+ this::onClientDrawAtTransitionEnd);
+ finishWct.setBoundsChangeTransaction(pipTaskToken, tx);
+
+ // Note that finishWct should be free of any actual WM state changes; we are using
+ // it for syncing with the client draw after delayed configuration changes are dispatched.
+ finishCallback.onTransitionFinished(finishWct.isEmpty() ? null : finishWct);
return true;
}
+ private void startOverlayFadeoutAnimation() {
+ ValueAnimator animator = ValueAnimator.ofFloat(1f, 0f);
+ animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+ tx.remove(mPipTransitionState.getSwipePipToHomeOverlay());
+ tx.apply();
+
+ // We have fully completed enter-PiP animation after the overlay is gone.
+ mPipTransitionState.setState(PipTransitionState.ENTERED_PIP);
+ }
+ });
+ animator.addUpdateListener(animation -> {
+ float alpha = (float) animation.getAnimatedValue();
+ SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+ tx.setAlpha(mPipTransitionState.getSwipePipToHomeOverlay(), alpha).apply();
+ });
+ animator.start();
+ }
+
private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@@ -240,12 +368,11 @@ public class PipTransition extends PipTransitionController {
if (pipChange == null) {
return false;
}
- mPipTaskToken = pipChange.getContainer();
-
// cache the PiP task token and leash
- mPipScheduler.setPipTaskToken(mPipTaskToken);
+ WindowContainerToken pipTaskToken = pipChange.getContainer();
startTransaction.apply();
+ // TODO: b/275910498 Use a new implementation of the PiP animator here.
finishCallback.onTransitionFinished(null);
return true;
}
@@ -258,26 +385,53 @@ public class PipTransition extends PipTransitionController {
if (pipChange == null) {
return false;
}
- mPipTaskToken = pipChange.getContainer();
- // cache the PiP task token and leash
- mPipScheduler.setPipTaskToken(mPipTaskToken);
+ 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;
+ }
+ private boolean startExpandAnimation(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
startTransaction.apply();
+ // TODO: b/275910498 Use a new implementation of the PiP animator here.
finishCallback.onTransitionFinished(null);
+ mPipTransitionState.setState(PipTransitionState.EXITED_PIP);
return true;
}
- private boolean startExpandAnimation(@NonNull TransitionInfo info,
+ private boolean removePipImmediately(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
startTransaction.apply();
finishCallback.onTransitionFinished(null);
- onExitPip();
+ mPipTransitionState.setState(PipTransitionState.EXITED_PIP);
return true;
}
+ //
+ // Various helpers to resolve transition requests and infos
+ //
+
@Nullable
private TransitionInfo.Change getPipChange(TransitionInfo info) {
for (TransitionInfo.Change change : info.getChanges()) {
@@ -303,6 +457,7 @@ public class PipTransition extends PipTransitionController {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.movePipActivityToPinnedRootTask(pipTask.token, entryBounds);
+ wct.deferConfigToTransitionEnd(pipTask.token);
return wct;
}
@@ -328,20 +483,82 @@ public class PipTransition extends PipTransitionController {
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);
}
- /**
- * TODO: b/275910498 Use a new implementation of the PiP animator here.
- */
- private void startResizeAnimation(SurfaceControl leash, Rect startBounds,
- Rect endBounds, int duration) {}
+ private boolean isRemovePipTransition(@NonNull TransitionInfo info) {
+ if (mPipTransitionState.mPipTaskToken == null) {
+ // PiP removal makes sense if enter-PiP has cached a valid pinned task token.
+ return false;
+ }
+ TransitionInfo.Change pipChange = info.getChange(mPipTransitionState.mPipTaskToken);
+ if (pipChange == null) {
+ // Search for the PiP change by token since the windowing mode might be FULLSCREEN now.
+ return false;
+ }
- private void onExitPip() {
- mPipTaskToken = null;
- mPipScheduler.onExitPip();
+ boolean isPipMovedToBack = info.getType() == TRANSIT_TO_BACK
+ && pipChange.getMode() == TRANSIT_TO_BACK;
+ boolean isPipClosed = info.getType() == TRANSIT_CLOSE
+ && pipChange.getMode() == TRANSIT_CLOSE;
+ // PiP is being removed if the pinned task is either moved to back or closed.
+ return isPipMovedToBack || isPipClosed;
+ }
+
+ //
+ // Miscellaneous callbacks and listeners
+ //
+
+ private void onClientDrawAtTransitionEnd() {
+ if (mPipTransitionState.getSwipePipToHomeOverlay() != null) {
+ startOverlayFadeoutAnimation();
+ } else if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) {
+ // If we were entering PiP (i.e. playing the animation) with a valid srcRectHint,
+ // and then we get a signal on client finishing its draw after the transition
+ // has ended, then we have fully entered PiP.
+ mPipTransitionState.setState(PipTransitionState.ENTERED_PIP);
+ }
+ }
+
+ @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) {
+ case PipTransitionState.ENTERING_PIP:
+ Preconditions.checkState(extra != null,
+ "No extra bundle for " + mPipTransitionState);
+
+ mPipTransitionState.mPipTaskToken = extra.getParcelable(
+ PIP_TASK_TOKEN, WindowContainerToken.class);
+ mPipTransitionState.mPinnedTaskLeash = extra.getParcelable(
+ PIP_TASK_LEASH, SurfaceControl.class);
+ boolean hasValidTokenAndLeash = mPipTransitionState.mPipTaskToken != null
+ && mPipTransitionState.mPinnedTaskLeash != null;
+
+ Preconditions.checkState(hasValidTokenAndLeash,
+ "Unexpected bundle for " + mPipTransitionState);
+ break;
+ case PipTransitionState.EXITED_PIP:
+ mPipTransitionState.mPipTaskToken = null;
+ mPipTransitionState.mPinnedTaskLeash = null;
+ break;
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
new file mode 100644
index 000000000000..9d599caf13dd
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
@@ -0,0 +1,322 @@
+/*
+ * 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.phone;
+
+import android.annotation.IntDef;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.SurfaceControl;
+import android.window.WindowContainerToken;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.util.Preconditions;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Contains the state relevant to carry out or probe the status of PiP transitions.
+ *
+ * <p>Existing and new PiP components can subscribe to PiP transition related state changes
+ * via <code>PipTransitionStateChangedListener</code>.</p>
+ *
+ * <p><code>PipTransitionState</code> users shouldn't rely on listener execution ordering.
+ * For example, if a class <code>Foo</code> wants to change some arbitrary state A that belongs
+ * to some other class <code>Bar</code>, a special care must be given when manipulating state A in
+ * <code>Foo#onPipTransitionStateChanged()</code>, since that's the responsibility of
+ * the class <code>Bar</code>.</p>
+ *
+ * <p>Hence, the recommended usage for classes who want to subscribe to
+ * <code>PipTransitionState</code> changes is to manipulate only their own internal state or
+ * <code>PipTransitionState</code> state.</p>
+ *
+ * <p>If there is some state that must be manipulated in another class <code>Bar</code>, it should
+ * just be moved to <code>PipTransitionState</code> and become a shared state
+ * between Foo and Bar.</p>
+ *
+ * <p>Moreover, <code>onPipTransitionStateChanged(oldState, newState, extra)</code>
+ * receives a <code>Bundle</code> extra object that can be optionally set via
+ * <code>setState(state, extra)</code>. This can be used to resolve extra information to update
+ * relevant internal or <code>PipTransitionState</code> state. However, each listener
+ * needs to check for whether the extra passed is correct for a particular state,
+ * and throw an <code>IllegalStateException</code> otherwise.</p>
+ */
+public class PipTransitionState {
+ public static final int UNDEFINED = 0;
+
+ // State for Launcher animating the swipe PiP to home animation.
+ public static final int SWIPING_TO_PIP = 1;
+
+ // State for Shell animating enter PiP or jump-cutting to PiP mode after Launcher animation.
+ public static final int ENTERING_PIP = 2;
+
+ // State for app finishing drawing in PiP mode as a final step in enter PiP flow.
+ public static final int ENTERED_PIP = 3;
+
+ // State to indicate we have scheduled a PiP bounds change transition.
+ public static final int SCHEDULED_BOUNDS_CHANGE = 4;
+
+ // State for the start of playing a transition to change PiP bounds. At this point, WM Core
+ // is aware of the new PiP bounds, but Shell might still be continuing animating.
+ public static final int CHANGING_PIP_BOUNDS = 5;
+
+ // State for finishing animating into new PiP bounds after resize is complete.
+ public static final int CHANGED_PIP_BOUNDS = 6;
+
+ // State for starting exiting PiP.
+ public static final int EXITING_PIP = 7;
+
+ // State for finishing exit PiP flow.
+ public static final int EXITED_PIP = 8;
+
+ private static final int FIRST_CUSTOM_STATE = 1000;
+
+ private int mPrevCustomState = FIRST_CUSTOM_STATE;
+
+ @IntDef(prefix = { "TRANSITION_STATE_" }, value = {
+ UNDEFINED,
+ SWIPING_TO_PIP,
+ ENTERING_PIP,
+ ENTERED_PIP,
+ SCHEDULED_BOUNDS_CHANGE,
+ CHANGING_PIP_BOUNDS,
+ CHANGED_PIP_BOUNDS,
+ EXITING_PIP,
+ EXITED_PIP,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TransitionState {}
+
+ @TransitionState
+ private int mState;
+
+ //
+ // Dependencies
+ //
+
+ @ShellMainThread
+ private final Handler mMainHandler;
+
+ //
+ // Swipe up to enter PiP related state
+ //
+
+ // true if Launcher has started swipe PiP to home animation
+ private boolean mInSwipePipToHomeTransition;
+
+ // App bounds used when as a starting point to swipe PiP to home animation in Launcher;
+ // these are also used to calculate the app icon overlay buffer size.
+ @NonNull
+ private final Rect mSwipePipToHomeAppBounds = new Rect();
+
+ //
+ // Tokens and leashes
+ //
+
+ // pinned PiP task's WC token
+ @Nullable
+ WindowContainerToken mPipTaskToken;
+
+ // pinned PiP task's leash
+ @Nullable
+ SurfaceControl mPinnedTaskLeash;
+
+ // Overlay leash potentially used during swipe PiP to home transition;
+ // if null while mInSwipePipToHomeTransition is true, then srcRectHint was invalid.
+ @Nullable
+ private SurfaceControl mSwipePipToHomeOverlay;
+
+ /**
+ * An interface to track state updates as we progress through PiP transitions.
+ */
+ public interface PipTransitionStateChangedListener {
+
+ /** Reports changes in PiP transition state. */
+ void onPipTransitionStateChanged(@TransitionState int oldState,
+ @TransitionState int newState, @Nullable Bundle extra);
+ }
+
+ private final List<PipTransitionStateChangedListener> mCallbacks = new ArrayList<>();
+
+ public PipTransitionState(@ShellMainThread Handler handler) {
+ mMainHandler = handler;
+ }
+
+ /**
+ * @return the state of PiP in the context of transitions.
+ */
+ @TransitionState
+ public int getState() {
+ return mState;
+ }
+
+ /**
+ * Sets the state of PiP in the context of transitions.
+ */
+ public void setState(@TransitionState int state) {
+ setState(state, null /* extra */);
+ }
+
+ /**
+ * Sets the state of PiP in the context of transitions
+ *
+ * @param extra a bundle passed to the subscribed listeners to resolve/cache extra info.
+ */
+ public void setState(@TransitionState int state, @Nullable Bundle extra) {
+ if (state == ENTERING_PIP || state == SWIPING_TO_PIP
+ || state == SCHEDULED_BOUNDS_CHANGE || state == CHANGING_PIP_BOUNDS) {
+ // States listed above require extra bundles to be provided.
+ Preconditions.checkArgument(extra != null && !extra.isEmpty(),
+ "No extra bundle for " + stateToString(state) + " state.");
+ }
+ if (mState != state) {
+ dispatchPipTransitionStateChanged(mState, state, extra);
+ mState = state;
+ }
+ }
+
+ /**
+ * Posts the state update for PiP in the context of transitions onto the main handler.
+ *
+ * <p>This is done to guarantee that any callback dispatches for the present state are
+ * complete. This is relevant for states that have multiple listeners, such as
+ * <code>SCHEDULED_BOUNDS_CHANGE</code> that helps turn off touch interactions along with
+ * the actual transition scheduling.</p>
+ */
+ public void postState(@TransitionState int state) {
+ postState(state, null /* extra */);
+ }
+
+ /**
+ * Posts the state update for PiP in the context of transitions onto the main handler.
+ *
+ * <p>This is done to guarantee that any callback dispatches for the present state are
+ * complete. This is relevant for states that have multiple listeners, such as
+ * <code>SCHEDULED_BOUNDS_CHANGE</code> that helps turn off touch interactions along with
+ * the actual transition scheduling.</p>
+ *
+ * @param extra a bundle passed to the subscribed listeners to resolve/cache extra info.
+ */
+ public void postState(@TransitionState int state, @Nullable Bundle extra) {
+ mMainHandler.post(() -> setState(state, extra));
+ }
+
+ private void dispatchPipTransitionStateChanged(@TransitionState int oldState,
+ @TransitionState int newState, @Nullable Bundle extra) {
+ mCallbacks.forEach(l -> l.onPipTransitionStateChanged(oldState, newState, extra));
+ }
+
+ /**
+ * Adds a {@link PipTransitionStateChangedListener} for future PiP transition state updates.
+ */
+ public void addPipTransitionStateChangedListener(PipTransitionStateChangedListener listener) {
+ if (mCallbacks.contains(listener)) {
+ return;
+ }
+ mCallbacks.add(listener);
+ }
+
+ /**
+ * @return true if provided {@link PipTransitionStateChangedListener}
+ * is registered before removing it.
+ */
+ public boolean removePipTransitionStateChangedListener(
+ PipTransitionStateChangedListener listener) {
+ return mCallbacks.remove(listener);
+ }
+
+ /**
+ * @return true if we have fully entered PiP.
+ */
+ public boolean isInPip() {
+ return mState > ENTERING_PIP && mState < EXITING_PIP;
+ }
+
+ void setSwipePipToHomeState(@Nullable SurfaceControl overlayLeash,
+ @NonNull Rect appBounds) {
+ mInSwipePipToHomeTransition = true;
+ if (overlayLeash != null && !appBounds.isEmpty()) {
+ mSwipePipToHomeOverlay = overlayLeash;
+ mSwipePipToHomeAppBounds.set(appBounds);
+ }
+ }
+
+ void resetSwipePipToHomeState() {
+ mInSwipePipToHomeTransition = false;
+ mSwipePipToHomeOverlay = null;
+ mSwipePipToHomeAppBounds.setEmpty();
+ }
+
+ /**
+ * @return true if in swipe PiP to home. Note that this is true until overlay fades if used too.
+ */
+ public boolean isInSwipePipToHomeTransition() {
+ return mInSwipePipToHomeTransition;
+ }
+
+ /**
+ * @return the overlay used during swipe PiP to home for invalid srcRectHints in auto-enter PiP;
+ * null if srcRectHint provided is valid.
+ */
+ @Nullable
+ public SurfaceControl getSwipePipToHomeOverlay() {
+ return mSwipePipToHomeOverlay;
+ }
+
+ /**
+ * @return app bounds used to calculate
+ */
+ @NonNull
+ public Rect getSwipePipToHomeAppBounds() {
+ return mSwipePipToHomeAppBounds;
+ }
+
+ /**
+ * @return a custom state solely for internal use by the caller.
+ */
+ @TransitionState
+ public int getCustomState() {
+ return ++mPrevCustomState;
+ }
+
+ private static String stateToString(int state) {
+ switch (state) {
+ case UNDEFINED: return "undefined";
+ case SWIPING_TO_PIP: return "swiping_to_pip";
+ case ENTERING_PIP: return "entering-pip";
+ case ENTERED_PIP: return "entered-pip";
+ case SCHEDULED_BOUNDS_CHANGE: return "scheduled_bounds_change";
+ case CHANGING_PIP_BOUNDS: return "changing-bounds";
+ case CHANGED_PIP_BOUNDS: return "changed-bounds";
+ case EXITING_PIP: return "exiting-pip";
+ case EXITED_PIP: return "exited-pip";
+ }
+ throw new IllegalStateException("Unknown state: " + state);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b)",
+ stateToString(mState), mInSwipePipToHomeTransition);
+ }
+}
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 ad29d15019c5..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.
*
@@ -52,7 +54,7 @@ public enum ShellProtoLogGroup implements IProtoLogGroup {
WM_SHELL_SYSUI_EVENTS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
Consts.TAG_WM_SHELL),
WM_SHELL_DESKTOP_MODE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true,
- Consts.TAG_WM_SHELL),
+ Consts.TAG_WM_DESKTOP_MODE),
WM_SHELL_FLOATING_APPS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
Consts.TAG_WM_SHELL),
WM_SHELL_FOLDABLE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
@@ -116,12 +118,22 @@ 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";
private static final String TAG_WM_SPLIT_SCREEN = "ShellSplitScreen";
+ private static final String TAG_WM_DESKTOP_MODE = "ShellDesktopMode";
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 e8f58fe2bfad..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
@@ -37,4 +37,12 @@ oneway interface IRecentTasksListener {
* Called when a running task vanishes.
*/
void onRunningTaskVanished(in RunningTaskInfo taskInfo);
-}
+
+ /**
+ * 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/RecentTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java
index 2616b8b08bf1..77b8663861ab 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java
@@ -16,7 +16,10 @@
package com.android.wm.shell.recents;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import android.annotation.Nullable;
+import android.graphics.Color;
+
+import com.android.wm.shell.shared.annotations.ExternalThread;
import com.android.wm.shell.util.GroupedRecentTaskInfo;
import java.util.List;
@@ -40,4 +43,12 @@ public interface RecentTasks {
*/
default void addAnimationStateListener(Executor listenerExecutor, Consumer<Boolean> listener) {
}
+
+ /**
+ * Sets a background color on the transition root layered behind the outgoing task. {@code null}
+ * may be used to clear any previously set colors to avoid showing a background at all. The
+ * color is always shown at full opacity.
+ */
+ default void setTransitionBackgroundColor(@Nullable Color color) {
+ }
}
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 1c54754e9953..03c8cf8cc795 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
@@ -19,17 +19,18 @@ package com.android.wm.shell.recents;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.content.pm.PackageManager.FEATURE_PC;
-import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
+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;
import android.app.ActivityTaskManager;
import android.app.IApplicationThread;
import android.app.PendingIntent;
-import android.app.TaskInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.graphics.Color;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Slog;
@@ -49,14 +50,15 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SingleInstanceRemoteListener;
import com.android.wm.shell.common.TaskStackListenerCallback;
import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.common.annotations.ExternalThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.DesktopModeStatus;
+import com.android.wm.shell.shared.annotations.ExternalThread;
+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;
@@ -73,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;
@@ -84,9 +87,10 @@ 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 mIsDesktopMode;
+ private final boolean mPcFeatureEnabled;
// Mapping of split task ids, mappings are symmetrical (ie. if t1 is the taskid of a task in a
// pair, then mSplitTasks[t1] = t2, and mSplitTasks[t2] = t1)
@@ -112,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,
@@ -128,14 +134,16 @@ public class RecentTasksController implements TaskStackListenerCallback,
TaskStackListenerImpl taskStackListener,
ActivityTaskManager activityTaskManager,
Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ TaskStackTransitionObserver taskStackTransitionObserver,
ShellExecutor mainExecutor) {
mContext = context;
mShellController = shellController;
mShellCommandHandler = shellCommandHandler;
mActivityTaskManager = activityTaskManager;
- mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
+ mPcFeatureEnabled = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
mTaskStackListener = taskStackListener;
mDesktopModeTaskRepository = desktopModeTaskRepository;
+ mTaskStackTransitionObserver = taskStackTransitionObserver;
mMainExecutor = mainExecutor;
shellInit.addInitCallback(this::onInit, this);
}
@@ -154,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) {
@@ -252,8 +264,14 @@ public class RecentTasksController implements TaskStackListenerCallback,
notifyRunningTaskVanished(taskInfo);
}
- public void onTaskWindowingModeChanged(TaskInfo taskInfo) {
+ /**
+ * Notify listeners that the running infos related to recent tasks was updated.
+ *
+ * This currently includes windowing mode and visibility.
+ */
+ public void onTaskRunningInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
notifyRecentTasksChanged();
+ notifyRunningTaskChanged(taskInfo);
}
@Override
@@ -261,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");
@@ -278,7 +302,9 @@ public class RecentTasksController implements TaskStackListenerCallback,
* Notify the running task listener that a task appeared on desktop environment.
*/
private void notifyRunningTaskAppeared(ActivityManager.RunningTaskInfo taskInfo) {
- if (mListener == null || !mIsDesktopMode || taskInfo.realActivity == null) {
+ if (mListener == null
+ || !shouldEnableRunningTasksForDesktopMode()
+ || taskInfo.realActivity == null) {
return;
}
try {
@@ -292,7 +318,9 @@ public class RecentTasksController implements TaskStackListenerCallback,
* Notify the running task listener that a task was removed on desktop environment.
*/
private void notifyRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
- if (mListener == null || !mIsDesktopMode || taskInfo.realActivity == null) {
+ if (mListener == null
+ || !shouldEnableRunningTasksForDesktopMode()
+ || taskInfo.realActivity == null) {
return;
}
try {
@@ -302,6 +330,41 @@ public class RecentTasksController implements TaskStackListenerCallback,
}
}
+ /**
+ * Notify the running task listener that a task was changed on desktop environment.
+ */
+ private void notifyRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
+ if (mListener == null
+ || !shouldEnableRunningTasksForDesktopMode()
+ || taskInfo.realActivity == null) {
+ return;
+ }
+ try {
+ mListener.onRunningTaskChanged(taskInfo);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed call onRunningTaskChanged", e);
+ }
+ }
+
+ 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)
+ && enableDesktopWindowingTaskbarRunningApps());
+ }
+
@VisibleForTesting
void registerRecentTasksListener(IRecentTasksListener listener) {
mListener = listener;
@@ -332,6 +395,8 @@ public class RecentTasksController implements TaskStackListenerCallback,
ArrayList<ActivityManager.RecentTaskInfo> freeformTasks = new ArrayList<>();
+ int mostRecentFreeformTaskIndex = Integer.MAX_VALUE;
+
// Pull out the pairs as we iterate back in the list
ArrayList<GroupedRecentTaskInfo> recentTasks = new ArrayList<>();
for (int i = 0; i < rawList.size(); i++) {
@@ -341,9 +406,17 @@ public class RecentTasksController implements TaskStackListenerCallback,
continue;
}
- if (DesktopModeStatus.isEnabled() && mDesktopModeTaskRepository.isPresent()
+ 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();
+ }
freeformTasks.add(taskInfo);
continue;
}
@@ -362,7 +435,7 @@ public class RecentTasksController implements TaskStackListenerCallback,
// Add a special entry for freeform tasks
if (!freeformTasks.isEmpty()) {
- recentTasks.add(0, GroupedRecentTaskInfo.forFreeformTasks(
+ recentTasks.add(mostRecentFreeformTaskIndex, GroupedRecentTaskInfo.forFreeformTasks(
freeformTasks.toArray(new ActivityManager.RecentTaskInfo[0])));
}
@@ -403,6 +476,26 @@ public class RecentTasksController implements TaskStackListenerCallback,
return null;
}
+ /**
+ * Find the background task that match the given taskId.
+ */
+ @Nullable
+ public ActivityManager.RecentTaskInfo findTaskInBackground(int taskId) {
+ List<ActivityManager.RecentTaskInfo> tasks = mActivityTaskManager.getRecentTasks(
+ Integer.MAX_VALUE, ActivityManager.RECENT_IGNORE_UNAVAILABLE,
+ ActivityManager.getCurrentUser());
+ for (int i = 0; i < tasks.size(); i++) {
+ final ActivityManager.RecentTaskInfo task = tasks.get(i);
+ if (task.isVisible) {
+ continue;
+ }
+ if (taskId == task.taskId) {
+ return task;
+ }
+ }
+ return null;
+ }
+
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
@@ -444,6 +537,16 @@ public class RecentTasksController implements TaskStackListenerCallback,
});
});
}
+
+ @Override
+ public void setTransitionBackgroundColor(@Nullable Color color) {
+ mMainExecutor.execute(() -> {
+ if (mTransitionHandler == null) {
+ return;
+ }
+ mTransitionHandler.setTransitionBackgroundColor(color);
+ });
+ }
}
@@ -471,6 +574,16 @@ public class RecentTasksController implements TaskStackListenerCallback,
public void onRunningTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
mListener.call(l -> l.onRunningTaskVanished(taskInfo));
}
+
+ @Override
+ 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 b5ea1b1b43ea..3a266d9bb3ef 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
@@ -22,10 +22,12 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.view.WindowManager.KEYGUARD_VISIBILITY_TRANSIT_FLAGS;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
+import static android.view.WindowManager.TRANSIT_PIP;
import static android.view.WindowManager.TRANSIT_SLEEP;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
+import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION;
import static com.android.wm.shell.util.SplitBounds.KEY_EXTRA_SPLIT_BOUNDS;
import android.annotation.Nullable;
@@ -35,6 +37,7 @@ import android.app.ActivityTaskManager;
import android.app.IApplicationThread;
import android.app.PendingIntent;
import android.content.Intent;
+import android.graphics.Color;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
@@ -52,13 +55,17 @@ import android.window.PictureInPictureSurfaceTransaction;
import android.window.TaskSnapshot;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
+import android.window.WindowAnimationState;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
+import androidx.annotation.NonNull;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.IResultReceiver;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.shared.TransitionUtil;
import com.android.wm.shell.sysui.ShellInit;
@@ -90,6 +97,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
private final ArrayList<RecentsMixedHandler> mMixers = new ArrayList<>();
private final HomeTransitionObserver mHomeTransitionObserver;
+ private @Nullable Color mBackgroundColor;
public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions,
@Nullable RecentTasksController recentTasksController,
@@ -121,6 +129,15 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
mStateListeners.add(listener);
}
+ /**
+ * Sets a background color on the transition root layered behind the outgoing task. {@code null}
+ * may be used to clear any previously set colors to avoid showing a background at all. The
+ * color is always shown at full opacity.
+ */
+ public void setTransitionBackgroundColor(@Nullable Color color) {
+ mBackgroundColor = color;
+ }
+
@VisibleForTesting
public IBinder startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options,
IApplicationThread appThread, IRecentsAnimationRunner listener) {
@@ -268,6 +285,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
private IBinder mTransition = null;
private boolean mKeyguardLocked = false;
private boolean mWillFinishToHome = false;
+ private Transitions.TransitionHandler mTakeoverHandler = null;
/** The animation is idle, waiting for the user to choose a task to switch to. */
private static final int STATE_NORMAL = 0;
@@ -384,6 +402,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
}
}
+ /**
+ * Cleans up the recents transition. This should generally not be called directly
+ * to cancel a transition after it has started, instead callers should call one of
+ * the cancel() methods to ensure that Launcher is notified.
+ */
void cleanUp() {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.cleanup", mInstanceId);
@@ -418,7 +441,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.start", mInstanceId);
if (mListener == null || mTransition == null) {
- cleanUp();
+ Slog.e(TAG, "Missing listener or transition, hasListener=" + (mListener != null) +
+ " hasTransition=" + (mTransition != null));
+ cancel("No listener (" + (mListener == null)
+ + ") or no transition (" + (mTransition == null) + ")");
return false;
}
// First see if this is a valid recents transition.
@@ -442,7 +468,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
if (mRecentsTask == null && !hasPausingTasks) {
// Recents is already running apparently, so this is a no-op.
Slog.e(TAG, "Tried to start recents while it is already running.");
- cleanUp();
+ cancel("No recents task and no pausing tasks");
return false;
}
@@ -465,6 +491,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
final int belowLayers = info.getChanges().size();
final int middleLayers = info.getChanges().size() * 2;
final int aboveLayers = info.getChanges().size() * 3;
+
+ // Add a background color to each transition root in this transition.
+ if (mBackgroundColor != null) {
+ info.getChanges().stream()
+ .mapToInt((change) -> TransitionUtil.rootIndexFor(change, info))
+ .distinct()
+ .mapToObj((rootIndex) -> info.getRoot(rootIndex).getLeash())
+ .forEach((root) -> createBackgroundSurface(t, root, middleLayers));
+ }
+
for (int i = 0; i < info.getChanges().size(); ++i) {
final TransitionInfo.Change change = info.getChanges().get(i);
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
@@ -531,21 +567,35 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
// Put into the "below" layer space.
t.setLayer(change.getLeash(), layer);
mOpeningTasks.add(new TaskState(change, null /* leash */));
+ } else {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " unhandled root taskId=%d", taskInfo.taskId);
}
} else if (TransitionUtil.isDividerBar(change)) {
final RemoteAnimationTarget target = TransitionUtil.newTarget(change,
belowLayers - i, info, t, mLeashMap);
// Add this as a app and we will separate them on launcher side by window type.
apps.add(target);
+ } else {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " unhandled change taskId=%d",
+ taskInfo != null ? taskInfo.taskId : -1);
}
}
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "Applying transaction=%d", t.getId());
t.apply();
- Bundle b = new Bundle(1 /*capacity*/);
+
+ mTakeoverHandler = mTransitions.getHandlerForTakeover(mTransition, info);
+
+ Bundle b = new Bundle(2 /*capacity*/);
b.putParcelable(KEY_EXTRA_SPLIT_BOUNDS,
mRecentTasksController.getSplitBoundsForTaskId(closingSplitTaskId));
+ b.putBoolean(KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION, mTakeoverHandler != null);
try {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
- "[%d] RecentsController.start: calling onAnimationStart", mInstanceId);
+ "[%d] RecentsController.start: calling onAnimationStart with %d apps",
+ mInstanceId, apps.size());
mListener.onAnimationStart(this,
apps.toArray(new RemoteAnimationTarget[apps.size()]),
wallpapers.toArray(new RemoteAnimationTarget[wallpapers.size()]),
@@ -560,6 +610,63 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
return true;
}
+ @Override
+ public void handOffAnimation(
+ RemoteAnimationTarget[] targets, WindowAnimationState[] states) {
+ mExecutor.execute(() -> {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.handOffAnimation", mInstanceId);
+
+ if (mTakeoverHandler == null) {
+ Slog.e(TAG, "Tried to hand off an animation without a valid takeover "
+ + "handler.");
+ return;
+ }
+
+ if (targets.length != states.length) {
+ Slog.e(TAG, "Tried to hand off an animation, but the number of targets "
+ + "(" + targets.length + ") doesn't match the number of states "
+ + "(" + states.length + ")");
+ return;
+ }
+
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.handOffAnimation: got %d states for %d "
+ + "changes", mInstanceId, states.length, mInfo.getChanges().size());
+ WindowAnimationState[] updatedStates =
+ new WindowAnimationState[mInfo.getChanges().size()];
+
+ // Ensure that the ordering of animation states is the same as that of matching
+ // changes in mInfo. prefixOrderIndex is set up in reverse order to that of the
+ // changes, so that's what we use to get to the correct ordering.
+ for (int i = 0; i < targets.length; i++) {
+ RemoteAnimationTarget target = targets[i];
+ updatedStates[updatedStates.length - target.prefixOrderIndex] = states[i];
+ }
+
+ Transitions.TransitionFinishCallback finishCB = mFinishCB;
+ // Reset the callback here, so any stray calls that aren't coming from the new
+ // handler are ignored.
+ mFinishCB = null;
+
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.handOffAnimation: calling "
+ + "takeOverAnimation with %d states", mInstanceId,
+ updatedStates.length);
+ mTakeoverHandler.takeOverAnimation(
+ mTransition, mInfo, new SurfaceControl.Transaction(),
+ wct -> {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "[%d] RecentsController.handOffAnimation: finish "
+ + "callback", mInstanceId);
+ // Set the callback once again so we can finish correctly.
+ mFinishCB = finishCB;
+ finishInner(true /* toHome */, false /* userLeave */,
+ null /* finishCb */);
+ }, updatedStates);
+ });
+ }
+
/**
* Updates this controller when a new transition is requested mid-recents transition.
*/
@@ -1011,13 +1118,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
}
if (mPipTransaction != null && sendUserLeaveHint) {
SurfaceControl pipLeash = null;
+ TransitionInfo.Change pipChange = null;
if (mPipTask != null) {
- pipLeash = mInfo.getChange(mPipTask).getLeash();
+ pipChange = mInfo.getChange(mPipTask);
+ pipLeash = pipChange.getLeash();
} else if (mPipTaskId != -1) {
// find a task with taskId from #setFinishTaskTransaction()
for (TransitionInfo.Change change : mInfo.getChanges()) {
if (change.getTaskInfo() != null
&& change.getTaskInfo().taskId == mPipTaskId) {
+ pipChange = change;
pipLeash = change.getLeash();
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"RecentsController.finishInner:"
@@ -1036,6 +1146,28 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"RecentsController.finishInner: PiP transaction %s merged",
mPipTransaction);
+ if (PipUtils.isPip2ExperimentEnabled()) {
+ // If this path is triggered, we are in auto-enter PiP flow in gesture
+ // navigation mode, which means "Recents" transition should be followed
+ // by a TRANSIT_PIP. Hence, we take the WCT was about to be sent
+ // to Core to be applied during finishTransition(), we modify it to
+ // factor in PiP changes, and we send it as a direct startWCT for
+ // a new TRANSIT_PIP type transition. Recents still sends
+ // finishTransition() to update visibilities, but with finishWCT=null.
+ TransitionRequestInfo requestInfo = new TransitionRequestInfo(
+ TRANSIT_PIP, null /* triggerTask */, pipChange.getTaskInfo(),
+ null /* remote */, null /* displayChange */, 0 /* flags */);
+ // Use mTransition IBinder token temporarily just to get PipTransition
+ // to return from its handleRequest(). The actual TRANSIT_PIP will have
+ // anew token once it arrives into PipTransition#startAnimation().
+ Pair<Transitions.TransitionHandler, WindowContainerTransaction>
+ requestRes = mTransitions.dispatchRequest(mTransition,
+ requestInfo, null /* skip */);
+ wct.merge(requestRes.second, true);
+ mTransitions.startTransition(TRANSIT_PIP, wct, null /* handler */);
+ // We need to clear the WCT to send finishWCT=null for Recents.
+ wct.clear();
+ }
}
mPipTaskId = -1;
mPipTask = null;
@@ -1057,7 +1189,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
}
private boolean allAppsAreTranslucent(ArrayList<TaskState> tasks) {
- if (tasks == null || tasks.isEmpty()) {
+ if (tasks == null) {
return false;
}
for (int i = tasks.size() - 1; i >= 0; --i) {
@@ -1068,6 +1200,29 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
return true;
}
+ private void createBackgroundSurface(SurfaceControl.Transaction transaction,
+ SurfaceControl parent, int layer) {
+ if (mBackgroundColor == null) {
+ return;
+ }
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ " adding background color to layer=%d", layer);
+ final SurfaceControl background = new SurfaceControl.Builder()
+ .setName("recents_background")
+ .setColorLayer()
+ .setOpaque(true)
+ .setParent(parent)
+ .build();
+ transaction.setColor(background, colorToFloatArray(mBackgroundColor));
+ transaction.setLayer(background, layer);
+ transaction.setAlpha(background, 1F);
+ transaction.show(background);
+ }
+
+ private static float[] colorToFloatArray(@NonNull Color color) {
+ return new float[]{color.red(), color.green(), color.blue()};
+ }
+
private void cleanUpPausingOrClosingTask(TaskState task, WindowContainerTransaction wct,
SurfaceControl.Transaction finishTransaction, boolean sendUserLeaveHint) {
if (!sendUserLeaveHint && task.isLeaf()) {
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/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
index ad4049320d93..8df287d12cbc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
@@ -18,11 +18,16 @@ package com.android.wm.shell.splitscreen;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.graphics.Rect;
+import android.os.Bundle;
+import android.window.RemoteTransition;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.internal.logging.InstanceId;
+import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
+import com.android.wm.shell.shared.annotations.ExternalThread;
import java.util.concurrent.Executor;
@@ -64,7 +69,9 @@ public interface SplitScreen {
default void onSplitVisibilityChanged(boolean visible) {}
}
- /** Callback interface for listening to requests to enter split select */
+ /**
+ * Callback interface for listening to requests to enter split select. Used for desktop -> split
+ */
interface SplitSelectListener {
default boolean onRequestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
int splitPosition, Rect taskBounds) {
@@ -72,6 +79,12 @@ public interface SplitScreen {
}
}
+ /** Launches a pair of tasks into splitscreen */
+ void startTasks(int taskId1, @Nullable Bundle options1, int taskId2,
+ @Nullable Bundle options2, @SplitPosition int splitPosition,
+ @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition,
+ InstanceId instanceId);
+
/** Registers listener that gets split screen callback. */
void registerSplitScreenListener(@NonNull SplitScreenListener listener,
@NonNull Executor executor);
@@ -79,12 +92,33 @@ public interface SplitScreen {
/** Unregisters listener that gets split screen callback. */
void unregisterSplitScreenListener(@NonNull SplitScreenListener listener);
+ interface SplitInvocationListener {
+ /**
+ * Called whenever shell starts or stops the split screen animation
+ * @param animationRunning if {@code true} the animation has begun, if {@code false} the
+ * animation has finished
+ */
+ default void onSplitAnimationInvoked(boolean animationRunning) { }
+ }
+
+ /**
+ * Registers a {@link SplitInvocationListener} to notify when the animation to enter split
+ * screen has started and stopped
+ *
+ * @param executor callbacks to the listener will be executed on this executor
+ */
+ void registerSplitAnimationListener(@NonNull SplitInvocationListener listener,
+ @NonNull Executor executor);
+
/** Called when device waking up finished. */
void onFinishedWakingUp();
/** Called when requested to go to fullscreen from the current active split app. */
void goToFullscreenFromSplit();
+ /** Called when splitscreen focused app is changed. */
+ void setSplitscreenFocus(boolean leftOrTop);
+
/** Get a string representation of a stage type */
static String stageTypeToString(@StageType int stage) {
switch (stage) {
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 952e2d4b3b9a..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;
@@ -90,7 +89,6 @@ 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.TransactionPool;
-import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
import com.android.wm.shell.common.split.SplitScreenUtils;
@@ -99,6 +97,7 @@ import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.draganddrop.DragAndDropPolicy;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.shared.annotations.ExternalThread;
import com.android.wm.shell.splitscreen.SplitScreen.StageType;
import com.android.wm.shell.sysui.KeyguardChangeListener;
import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -138,6 +137,7 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
public static final int EXIT_REASON_RECREATE_SPLIT = 10;
public static final int EXIT_REASON_FULLSCREEN_SHORTCUT = 11;
public static final int EXIT_REASON_DESKTOP_MODE = 12;
+ public static final int EXIT_REASON_FULLSCREEN_REQUEST = 13;
@IntDef(value = {
EXIT_REASON_UNKNOWN,
EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW,
@@ -151,7 +151,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
EXIT_REASON_CHILD_TASK_ENTER_PIP,
EXIT_REASON_RECREATE_SPLIT,
EXIT_REASON_FULLSCREEN_SHORTCUT,
- EXIT_REASON_DESKTOP_MODE
+ EXIT_REASON_DESKTOP_MODE,
+ EXIT_REASON_FULLSCREEN_REQUEST
})
@Retention(RetentionPolicy.SOURCE)
@interface ExitReason{}
@@ -436,7 +437,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
public void exitSplitScreen(int toTopTaskId, @ExitReason int exitReason) {
- mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason);
+ if (ENABLE_SHELL_TRANSITIONS) {
+ mStageCoordinator.dismissSplitScreen(toTopTaskId, exitReason);
+ } else {
+ mStageCoordinator.exitSplitScreen(toTopTaskId, exitReason);
+ }
}
@Override
@@ -481,6 +486,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
}
+ public void setSplitscreenFocus(boolean leftOrTop) {
+ if (mStageCoordinator.isSplitActive()) {
+ mStageCoordinator.grantFocusToPosition(leftOrTop);
+ }
+ }
+
/** Move the specified task to fullscreen, regardless of focus state. */
public void moveTaskToFullscreen(int taskId, int exitReason) {
mStageCoordinator.moveTaskToFullscreen(taskId, exitReason);
@@ -494,6 +505,15 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
return mStageCoordinator.getActivateSplitPosition(taskInfo);
}
+ /** Start two tasks in parallel as a splitscreen pair. */
+ public void startTasks(int taskId1, @Nullable Bundle options1, int taskId2,
+ @Nullable Bundle options2, @SplitPosition int splitPosition,
+ @PersistentSnapPosition int snapPosition,
+ @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+ mStageCoordinator.startTasks(taskId1, options1, taskId2, options2, splitPosition,
+ snapPosition, remoteTransition, instanceId);
+ }
+
/**
* Move a task to split select
* @param taskInfo the task being moved to split select
@@ -1044,6 +1064,8 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
return "RECREATE_SPLIT";
case EXIT_REASON_DESKTOP_MODE:
return "DESKTOP_MODE";
+ case EXIT_REASON_FULLSCREEN_REQUEST:
+ return "FULLSCREEN_REQUEST";
default:
return "unknown reason, reason int = " + exitReason;
}
@@ -1106,6 +1128,15 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
};
@Override
+ public void startTasks(int taskId1, @Nullable Bundle options1, int taskId2,
+ @Nullable Bundle options2, int splitPosition, int snapPosition,
+ @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
+ mMainExecutor.execute(() -> SplitScreenController.this.startTasks(
+ taskId1, options1, taskId2, options2, splitPosition, snapPosition,
+ remoteTransition, instanceId));
+ }
+
+ @Override
public void registerSplitScreenListener(SplitScreenListener listener, Executor executor) {
if (mExecutors.containsKey(listener)) return;
@@ -1134,6 +1165,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
}
@Override
+ public void registerSplitAnimationListener(@NonNull SplitInvocationListener listener,
+ @NonNull Executor executor) {
+ mStageCoordinator.registerSplitAnimationListener(listener, executor);
+ }
+
+ @Override
public void onFinishedWakingUp() {
mMainExecutor.execute(SplitScreenController.this::onFinishedWakingUp);
}
@@ -1142,6 +1179,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter,
public void goToFullscreenFromSplit() {
mMainExecutor.execute(SplitScreenController.this::goToFullscreenFromSplit);
}
+
+ @Override
+ public void setSplitscreenFocus(boolean leftOrTop) {
+ mMainExecutor.execute(
+ () -> SplitScreenController.this.setSplitscreenFocus(leftOrTop));
+ }
}
/**
@@ -1197,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/SplitScreenShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java
index 7f16c5e3592e..af11ebc515d7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java
@@ -17,6 +17,7 @@
package com.android.wm.shell.splitscreen;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_UNKNOWN;
import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -45,6 +46,8 @@ public class SplitScreenShellCommandHandler implements
return runSetSideStagePosition(args, pw);
case "switchSplitPosition":
return runSwitchSplitPosition();
+ case "exitSplitScreen":
+ return runExitSplitScreen(args, pw);
default:
pw.println("Invalid command: " + args[0]);
return false;
@@ -91,6 +94,17 @@ public class SplitScreenShellCommandHandler implements
return true;
}
+ private boolean runExitSplitScreen(String[] args, PrintWriter pw) {
+ if (args.length < 2) {
+ // First argument is the action name.
+ pw.println("Error: task id should be provided as arguments");
+ return false;
+ }
+ final int taskId = Integer.parseInt(args[1]);
+ mController.exitSplitScreen(taskId, EXIT_REASON_UNKNOWN);
+ return true;
+ }
+
@Override
public void printShellCommandHelp(PrintWriter pw, String prefix) {
pw.println(prefix + "moveToSideStage <taskId> <SideStagePosition>");
@@ -101,5 +115,7 @@ public class SplitScreenShellCommandHandler implements
pw.println(prefix + " Sets the position of the side-stage.");
pw.println(prefix + "switchSplitPosition");
pw.println(prefix + " Reverses the split.");
+ pw.println(prefix + "exitSplitScreen <taskId>");
+ pw.println(prefix + " Exits split screen and leaves the provided split task on top.");
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 1a53a1d10dd2..0541a0287179 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -55,6 +55,7 @@ import com.android.wm.shell.transition.OneShotRemoteHandler;
import com.android.wm.shell.transition.Transitions;
import java.util.ArrayList;
+import java.util.concurrent.Executor;
/** Manages transition animations for split-screen. */
class SplitScreenTransitions {
@@ -79,6 +80,8 @@ class SplitScreenTransitions {
private Transitions.TransitionFinishCallback mFinishCallback = null;
private SurfaceControl.Transaction mFinishTransaction;
+ private SplitScreen.SplitInvocationListener mSplitInvocationListener;
+ private Executor mSplitInvocationListenerExecutor;
SplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions,
@NonNull Runnable onFinishCallback, StageCoordinator stageCoordinator) {
@@ -353,6 +356,10 @@ class SplitScreenTransitions {
+ " skip to start enter split transition since it already exist. ");
return null;
}
+ if (mSplitInvocationListenerExecutor != null && mSplitInvocationListener != null) {
+ mSplitInvocationListenerExecutor.execute(() -> mSplitInvocationListener
+ .onSplitAnimationInvoked(true /*animationRunning*/));
+ }
final IBinder transition = mTransitions.startTransition(transitType, wct, handler);
setEnterTransition(transition, remoteTransition, extraTransitType, resizeAnim);
return transition;
@@ -457,6 +464,7 @@ class SplitScreenTransitions {
mPendingEnter.onConsumed(aborted);
mPendingEnter = null;
+ mStageCoordinator.notifySplitAnimationFinished();
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTransitionConsumed for enter transition");
} else if (isPendingDismiss(transition)) {
mPendingDismiss.onConsumed(aborted);
@@ -493,10 +501,11 @@ class SplitScreenTransitions {
mAnimatingTransition = null;
mOnFinish.run();
- if (mFinishCallback != null) {
- mFinishCallback.onTransitionFinished(wct /* wct */);
- mFinishCallback = null;
- }
+ if (mFinishCallback != null) {
+ Transitions.TransitionFinishCallback currentFinishCallback = mFinishCallback;
+ mFinishCallback = null;
+ currentFinishCallback.onTransitionFinished(wct /* wct */);
+ }
}
private void startFadeAnimation(@NonNull SurfaceControl leash, boolean show) {
@@ -529,6 +538,12 @@ class SplitScreenTransitions {
mTransitions.getAnimExecutor().execute(va::start);
}
+ public void registerSplitAnimListener(@NonNull SplitScreen.SplitInvocationListener listener,
+ @NonNull Executor executor) {
+ mSplitInvocationListener = listener;
+ mSplitInvocationListenerExecutor = executor;
+ }
+
/** Calls when the transition got consumed. */
interface TransitionConsumedCallback {
void onConsumed(boolean aborted);
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 ec907fd9bd12..b6a18e537600 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
@@ -16,10 +16,8 @@
package com.android.wm.shell.splitscreen;
-import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED;
-import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
@@ -43,6 +41,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSIT
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
import static com.android.wm.shell.common.split.SplitScreenConstants.splitPositionToString;
+import static com.android.wm.shell.common.split.SplitScreenUtils.getResizingBackgroundColor;
import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN;
@@ -59,6 +58,7 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DESKTOP_MODE;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_REQUEST;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RECREATE_SPLIT;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
@@ -66,6 +66,7 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_UNKNOWN;
import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonToString;
+import static com.android.wm.shell.transition.MixedTransitionHelper.getPipReplacingChange;
import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
@@ -156,6 +157,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.Executor;
/**
* Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and
@@ -200,7 +202,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private final DisplayImeController mDisplayImeController;
private final DisplayInsetsController mDisplayInsetsController;
private final TransactionPool mTransactionPool;
- private final SplitScreenTransitions mSplitTransitions;
+ private SplitScreenTransitions mSplitTransitions;
private final SplitscreenEventLogger mLogger;
private final ShellExecutor mMainExecutor;
// Cache live tile tasks while entering recents, evict them from stages in finish transaction
@@ -236,6 +238,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
private DefaultMixedHandler mMixedHandler;
private final Toast mSplitUnsupportedToast;
private SplitRequest mSplitRequest;
+ /** Used to notify others of when shell is animating into split screen */
+ private SplitScreen.SplitInvocationListener mSplitInvocationListener;
+ private Executor mSplitInvocationListenerExecutor;
/**
* Since StageCoordinator only coordinates MainStage and SideStage, it shouldn't support
@@ -246,6 +251,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
return false;
}
+ /** NOTE: Will overwrite any previously set {@link #mSplitInvocationListener} */
+ public void registerSplitAnimationListener(
+ @NonNull SplitScreen.SplitInvocationListener listener, @NonNull Executor executor) {
+ mSplitInvocationListener = listener;
+ mSplitInvocationListenerExecutor = executor;
+ mSplitTransitions.registerSplitAnimListener(listener, executor);
+ }
+
class SplitRequest {
@SplitPosition
int mActivatePosition;
@@ -398,6 +411,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
return mSplitTransitions;
}
+ @VisibleForTesting
+ void setSplitTransitions(SplitScreenTransitions splitScreenTransitions) {
+ mSplitTransitions = splitScreenTransitions;
+ }
+
public boolean isSplitScreenVisible() {
return mSideStageListener.mVisible && mMainStageListener.mVisible;
}
@@ -529,7 +547,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
null /* childrenToTop */, EXIT_REASON_UNKNOWN));
Log.w(TAG, splitFailureMessage("startShortcut",
"side stage was not populated"));
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
}
if (finishedCallback != null) {
@@ -582,7 +600,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
wct.startTask(taskId, options);
// If this should be mixed, send the task to avoid split handle transition directly.
- if (mMixedHandler != null && mMixedHandler.shouldSplitEnterMixed(taskId, mTaskOrganizer)) {
+ if (mMixedHandler != null && mMixedHandler.isTaskInPip(taskId, mTaskOrganizer)) {
mTaskOrganizer.applyTransaction(wct);
return;
}
@@ -621,7 +639,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
wct.sendPendingIntent(intent, fillInIntent, options);
// If this should be mixed, just send the intent to avoid split handle transition directly.
- if (mMixedHandler != null && mMixedHandler.shouldSplitEnterMixed(intent)) {
+ if (mMixedHandler != null && mMixedHandler.isIntentInPip(intent)) {
mTaskOrganizer.applyTransaction(wct);
return;
}
@@ -660,7 +678,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
null /* childrenToTop */, EXIT_REASON_UNKNOWN));
Log.w(TAG, splitFailureMessage("startIntentLegacy",
"side stage was not populated"));
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
}
if (apps != null) {
@@ -710,16 +728,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
taskId1, taskId2, splitPosition, snapPosition);
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (taskId2 == INVALID_TASK_ID) {
- if (mMainStage.containsTask(taskId1) || mSideStage.containsTask(taskId1)) {
- prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct);
- }
- if (mRecentTasks.isPresent()) {
- mRecentTasks.get().removeSplitPair(taskId1);
- }
- options1 = options1 != null ? options1 : new Bundle();
- addActivityOptions(options1, null);
- wct.startTask(taskId1, options1);
- mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
+ startSingleTask(taskId1, options1, wct, remoteTransition);
return;
}
@@ -740,11 +749,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
"startIntentAndTask: intent=%s task1=%d position=%d snapPosition=%d",
pendingIntent.getIntent(), taskId, splitPosition, snapPosition);
final WindowContainerTransaction wct = new WindowContainerTransaction();
- if (taskId == INVALID_TASK_ID) {
- options1 = options1 != null ? options1 : new Bundle();
- addActivityOptions(options1, null);
- wct.sendPendingIntent(pendingIntent, fillInIntent, options1);
- mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
+ boolean firstIntentPipped = mMixedHandler.isIntentInPip(pendingIntent);
+ boolean secondTaskPipped = mMixedHandler.isTaskInPip(taskId, mTaskOrganizer);
+ if (taskId == INVALID_TASK_ID || secondTaskPipped) {
+ startSingleIntent(pendingIntent, fillInIntent, options1, wct, remoteTransition);
+ return;
+ }
+
+ if (firstIntentPipped) {
+ startSingleTask(taskId, options2, wct, remoteTransition);
return;
}
@@ -756,6 +769,24 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId);
}
+ /**
+ * @param taskId Starts this task in fullscreen, removing it from existing pairs if it was part
+ * of one.
+ */
+ private void startSingleTask(int taskId, Bundle options, WindowContainerTransaction wct,
+ RemoteTransition remoteTransition) {
+ if (mMainStage.containsTask(taskId) || mSideStage.containsTask(taskId)) {
+ prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct);
+ }
+ if (mRecentTasks.isPresent()) {
+ mRecentTasks.get().removeSplitPair(taskId);
+ }
+ options = options != null ? options : new Bundle();
+ addActivityOptions(options, null);
+ wct.startTask(taskId, options);
+ mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
+ }
+
/** Starts a shortcut and a task to a split pair in one transition. */
void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1,
int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
@@ -843,6 +874,21 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
return;
}
+ boolean handledForPipSplitLaunch = handlePippedSplitIntentsLaunch(
+ pendingIntent1,
+ pendingIntent2,
+ options1,
+ options2,
+ shortcutInfo1,
+ shortcutInfo2,
+ wct,
+ fillInIntent1,
+ fillInIntent2,
+ remoteTransition);
+ if (handledForPipSplitLaunch) {
+ return;
+ }
+
if (!mMainStage.isActive()) {
// Build a request WCT that will launch both apps such that task 0 is on the main stage
// while task 1 is on the side stage.
@@ -877,6 +923,46 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
setEnterInstanceId(instanceId);
}
+ /**
+ * Checks if either of the apps in the desired split launch is currently in Pip. If so, it will
+ * launch the non-pipped app as a fullscreen app, otherwise no-op.
+ */
+ private boolean handlePippedSplitIntentsLaunch(PendingIntent pendingIntent1,
+ PendingIntent pendingIntent2, Bundle options1, Bundle options2,
+ ShortcutInfo shortcutInfo1, ShortcutInfo shortcutInfo2, WindowContainerTransaction wct,
+ Intent fillInIntent1, Intent fillInIntent2, RemoteTransition remoteTransition) {
+ // If one of the split apps to start is in Pip, only launch the non-pip app in fullscreen
+ boolean firstIntentPipped = mMixedHandler.isIntentInPip(pendingIntent1);
+ boolean secondIntentPipped = mMixedHandler.isIntentInPip(pendingIntent2);
+ if (firstIntentPipped || secondIntentPipped) {
+ Bundle options = secondIntentPipped ? options1 : options2;
+ options = options == null ? new Bundle() : options;
+ addActivityOptions(options, null);
+ if (shortcutInfo1 != null || shortcutInfo2 != null) {
+ ShortcutInfo infoToLaunch = secondIntentPipped ? shortcutInfo1 : shortcutInfo2;
+ wct.startShortcut(mContext.getPackageName(), infoToLaunch, options);
+ mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
+ } else {
+ PendingIntent intentToLaunch = secondIntentPipped ? pendingIntent1 : pendingIntent2;
+ Intent fillInIntentToLaunch = secondIntentPipped ? fillInIntent1 : fillInIntent2;
+ startSingleIntent(intentToLaunch, fillInIntentToLaunch, options, wct,
+ remoteTransition);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /** @param pendingIntent Starts this intent in fullscreen */
+ private void startSingleIntent(PendingIntent pendingIntent, Intent fillInIntent, Bundle options,
+ WindowContainerTransaction wct,
+ RemoteTransition remoteTransition) {
+ Bundle optionsToLaunch = options != null ? options : new Bundle();
+ addActivityOptions(optionsToLaunch, null);
+ wct.sendPendingIntent(pendingIntent, fillInIntent, optionsToLaunch);
+ mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
+ }
+
/** Starts a pair of tasks using legacy transition. */
void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1,
int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition,
@@ -1213,7 +1299,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
Log.w(TAG, splitFailureMessage("onRemoteAnimationFinishedOrCancelled",
"main or side stage was not populated."));
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
} else {
mSyncQueue.queue(evictWct);
mSyncQueue.runInSync(t -> {
@@ -1234,7 +1320,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
Log.w(TAG, splitFailureMessage("onRemoteAnimationFinished",
"main or side stage was not populated"));
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
return;
}
@@ -1438,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,
@@ -1451,6 +1538,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mExitSplitScreenOnHide = exitSplitScreenOnHide;
}
+ /** Exits split screen with legacy transition */
void exitSplitScreen(int toTopTaskId, @ExitReason int exitReason) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "exitSplitScreen: topTaskId=%d reason=%s active=%b",
toTopTaskId, exitReasonToString(exitReason), mMainStage.isActive());
@@ -1470,6 +1558,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
applyExitSplitScreen(childrenToTop, wct, exitReason);
}
+ /** Exits split screen with legacy transition */
private void exitSplitScreen(@Nullable StageTaskListener childrenToTop,
@ExitReason int exitReason) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "exitSplitScreen: mainStageToTop=%b reason=%s active=%b",
@@ -1547,6 +1636,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
+ void dismissSplitScreen(int toTopTaskId, @ExitReason int exitReason) {
+ if (!mMainStage.isActive()) return;
+ final int stage = getStageOfTask(toTopTaskId);
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ prepareExitSplitScreen(stage, wct);
+ mSplitTransitions.startDismissTransition(wct, this, stage, exitReason);
+ }
+
/**
* Overridden by child classes.
*/
@@ -1582,6 +1679,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
+ protected void grantFocusToPosition(boolean leftOrTop) {
+ int stageToFocus;
+ if (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT) {
+ stageToFocus = leftOrTop ? getMainStagePosition() : getSideStagePosition();
+ } else {
+ stageToFocus = leftOrTop ? getSideStagePosition() : getMainStagePosition();
+ }
+ grantFocusToStage(stageToFocus);
+ }
+
private void clearRequestIfPresented() {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "clearRequestIfPresented");
if (mSideStageListener.mVisible && mSideStageListener.mHasChildren
@@ -1612,6 +1719,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// User has used a keyboard shortcut to go back to fullscreen from split
case EXIT_REASON_DESKTOP_MODE:
// One of the children enters desktop mode
+ case EXIT_REASON_UNKNOWN:
+ // Unknown reason
return true;
default:
return false;
@@ -1738,11 +1847,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
void finishEnterSplitScreen(SurfaceControl.Transaction finishT) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "finishEnterSplitScreen");
- mSplitLayout.update(finishT, true /* resetImePosition */);
- mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash,
- getMainStageBounds());
- mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash,
- getSideStageBounds());
+ mSplitLayout.update(null, true /* resetImePosition */);
+ mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash);
+ mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash);
setDividerVisibility(true, finishT);
// Ensure divider surface are re-parented back into the hierarchy at the end of the
// transition. See Transition#buildFinishTransaction for more detail.
@@ -1779,13 +1886,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
private void addActivityOptions(Bundle opts, @Nullable StageTaskListener launchTarget) {
+ ActivityOptions options = ActivityOptions.fromBundle(opts);
if (launchTarget != null) {
- opts.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, launchTarget.mRootTaskInfo.token);
+ options.setLaunchRootTask(launchTarget.mRootTaskInfo.token);
}
// Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split
// will be canceled.
- opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED, true);
- opts.putBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION, true);
+ 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());
}
void updateActivityOptions(Bundle opts, @SplitPosition int position) {
@@ -2021,7 +2134,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSkipEvictingMainStageChildren = false;
} else {
mShowDecorImmediately = true;
- mSplitLayout.flingDividerToCenter();
+ mSplitLayout.flingDividerToCenter(/*finishCallback*/ null);
}
});
}
@@ -2221,7 +2334,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSkipEvictingMainStageChildren = false;
} else {
mShowDecorImmediately = true;
- mSplitLayout.flingDividerToCenter();
+ mSplitLayout.flingDividerToCenter(/*finishCallback*/ null);
}
});
}
@@ -2280,14 +2393,20 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
@Override
- public void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY) {
+ public void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY,
+ boolean shouldUseParallaxEffect) {
final SurfaceControl.Transaction t = mTransactionPool.acquire();
t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
- updateSurfaceBounds(layout, t, true /* applyResizingOffset */);
+ updateSurfaceBounds(layout, t, shouldUseParallaxEffect);
getMainStageBounds(mTempRect1);
getSideStageBounds(mTempRect2);
- mMainStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately);
- mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately);
+ // TODO (b/307490004): "commonColor" below is a temporary fix to ensure the colors on both
+ // sides match. When b/307490004 is fixed, this code can be reverted.
+ float[] commonColor = getResizingBackgroundColor(mSideStage.mRootTaskInfo).getComponents();
+ mMainStage.onResizing(
+ mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately, commonColor);
+ mSideStage.onResizing(
+ mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately, commonColor);
t.apply();
mTransactionPool.release(t);
}
@@ -2593,6 +2712,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
prepareEnterSplitScreen(out);
mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(),
TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering);
+ } else if (inFullscreen && isSplitScreenVisible()) {
+ // If the trigger task is in fullscreen and in split, exit split and place
+ // task on top
+ final int stageType = getStageOfTask(triggerTask.taskId);
+ prepareExitSplitScreen(stageType, out);
+ mSplitTransitions.setDismissTransition(transition, stageType,
+ EXIT_REASON_FULLSCREEN_REQUEST);
}
} else if (isOpening && inFullscreen) {
final int activityType = triggerTask.getActivityType();
@@ -2636,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());
@@ -2723,7 +2857,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitLayout.setFreezeDividerWindow(false);
final StageChangeRecord record = new StageChangeRecord();
final int transitType = info.getType();
- boolean hasEnteringPip = false;
+ TransitionInfo.Change pipChange = null;
for (int iC = 0; iC < info.getChanges().size(); ++iC) {
final TransitionInfo.Change change = info.getChanges().get(iC);
if (change.getMode() == TRANSIT_CHANGE
@@ -2734,7 +2868,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
if (mMixedHandler.isEnteringPip(change, transitType)) {
- hasEnteringPip = true;
+ pipChange = change;
}
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
@@ -2786,9 +2920,20 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
}
- if (hasEnteringPip) {
+ if (pipChange != null) {
+ TransitionInfo.Change pipReplacingChange = getPipReplacingChange(info, pipChange,
+ mMainStage.mRootTaskInfo.taskId, mSideStage.mRootTaskInfo.taskId,
+ getSplitItemStage(pipChange.getLastParent()));
+ if (pipReplacingChange != null) {
+ // Set an enter transition for when startAnimation gets called again
+ mSplitTransitions.setEnterTransition(transition, /*remoteTransition*/ null,
+ TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, /*resizeAnim*/ false);
+ }
+
mMixedHandler.animatePendingEnterPipFromSplit(transition, info,
- startTransaction, finishTransaction, finishCallback);
+ startTransaction, finishTransaction, finishCallback,
+ pipReplacingChange != null);
+ notifySplitAnimationFinished();
return true;
}
@@ -2823,6 +2968,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
// the transition, or synchronize task-org callbacks.
}
// Use normal animations.
+ notifySplitAnimationFinished();
return false;
} else if (mMixedHandler != null && TransitionUtil.hasDisplayChange(info)) {
// A display-change has been un-expectedly inserted into the transition. Redirect
@@ -2836,6 +2982,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitLayout.update(startTransaction, true /* resetImePosition */);
startTransaction.apply();
}
+ notifySplitAnimationFinished();
return true;
}
}
@@ -2969,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.
@@ -2980,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."));
@@ -3009,7 +3157,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
pendingEnter.mRemoteHandler.onTransitionConsumed(transition,
false /*aborted*/, finishT);
}
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
return true;
}
}
@@ -3038,6 +3186,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
final TransitionInfo.Change finalMainChild = mainChild;
final TransitionInfo.Change finalSideChild = sideChild;
enterTransition.setFinishedCallback((callbackWct, callbackT) -> {
+ if (!enterTransition.mResizeAnim) {
+ // If resizing, we'll call notify at the end of the resizing animation (below)
+ notifySplitAnimationFinished();
+ }
if (finalMainChild != null) {
if (!mainNotContainOpenTask) {
mMainStage.evictOtherChildren(callbackWct, finalMainChild.getTaskInfo().taskId);
@@ -3057,12 +3209,28 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
if (enterTransition.mResizeAnim) {
mShowDecorImmediately = true;
- mSplitLayout.flingDividerToCenter();
+ mSplitLayout.flingDividerToCenter(this::notifySplitAnimationFinished);
}
callbackWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false);
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;
@@ -3323,6 +3491,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
true /* reparentLeafTaskIfRelaunch */);
}
+ /** Call this when the animation from split screen to desktop is started. */
+ public void onSplitToDesktop() {
+ setSplitsVisible(false);
+ }
+
/** Call this when the recents animation finishes by doing pair-to-pair switch. */
public void onRecentsPairToPairAnimationFinish(WindowContainerTransaction finishWct) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsPairToPairAnimationFinish");
@@ -3454,6 +3627,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitLayout.isLeftRightSplit());
}
+ private void handleUnsupportedSplitStart() {
+ mSplitUnsupportedToast.show();
+ notifySplitAnimationFinished();
+ }
+
+ void notifySplitAnimationFinished() {
+ if (mSplitInvocationListener == null || mSplitInvocationListenerExecutor == null) {
+ return;
+ }
+ mSplitInvocationListenerExecutor.execute(() ->
+ mSplitInvocationListener.onSplitAnimationInvoked(false /*animationRunning*/));
+ }
+
/**
* Logs the exit of splitscreen to a specific stage. This must be called before the exit is
* executed.
@@ -3516,7 +3702,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
if (!ENABLE_SHELL_TRANSITIONS) {
StageCoordinator.this.exitSplitScreen(isMainStage ? mMainStage : mSideStage,
EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
return;
}
@@ -3536,7 +3722,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
"app package " + taskInfo.baseActivity.getPackageName()
+ " does not support splitscreen, or is a controlled activity type"));
if (splitScreenVisible) {
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index f33ab33dafcc..0f3d6cade95a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -177,9 +177,11 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
@Override
@CallSuper
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
- ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskAppeared: task=%d taskParent=%d rootTask=%d",
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskAppeared: taskId=%d taskParent=%d rootTask=%d "
+ + "taskActivity=%s",
taskInfo.taskId, taskInfo.parentTaskId,
- mRootTaskInfo != null ? mRootTaskInfo.taskId : -1);
+ mRootTaskInfo != null ? mRootTaskInfo.taskId : -1,
+ taskInfo.baseActivity);
if (mRootTaskInfo == null) {
mRootLeash = leash;
mRootTaskInfo = taskInfo;
@@ -213,13 +215,14 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
@Override
@CallSuper
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: taskId=%d taskAct=%s",
+ taskInfo.taskId, taskInfo.baseActivity);
mWindowDecorViewModel.ifPresent(viewModel -> viewModel.onTaskInfoChanged(taskInfo));
if (mRootTaskInfo.taskId == taskInfo.taskId) {
// Inflates split decor view only when the root task is visible.
if (!ENABLE_SHELL_TRANSITIONS && mRootTaskInfo.isVisible != taskInfo.isVisible) {
if (taskInfo.isVisible) {
- mSplitDecorManager.inflate(mContext, mRootLeash,
- taskInfo.configuration.windowConfiguration.getBounds());
+ mSplitDecorManager.inflate(mContext, mRootLeash);
} else {
mSyncQueue.runInSync(t -> mSplitDecorManager.release(t));
}
@@ -261,6 +264,7 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskVanished: task=%d", taskInfo.taskId);
final int taskId = taskInfo.taskId;
+ mWindowDecorViewModel.ifPresent(vm -> vm.onTaskVanished(taskInfo));
if (mRootTaskInfo.taskId == taskId) {
mCallbacks.onRootTaskVanished();
mRootTaskInfo = null;
@@ -310,10 +314,10 @@ class StageTaskListener implements ShellTaskOrganizer.TaskListener {
}
void onResizing(Rect newBounds, Rect sideBounds, SurfaceControl.Transaction t, int offsetX,
- int offsetY, boolean immediately) {
+ int offsetY, boolean immediately, float[] veilColor) {
if (mSplitDecorManager != null && mRootTaskInfo != null) {
mSplitDecorManager.onResizing(mRootTaskInfo, newBounds, sideBounds, t, offsetX,
- offsetY, immediately);
+ offsetY, immediately, veilColor);
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index da2965c05ee4..2b12a22f907d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -251,9 +251,14 @@ public class SplashscreenContentDrawer {
final ActivityInfo activityInfo = windowInfo.targetActivityInfo != null
? windowInfo.targetActivityInfo
: taskInfo.topActivityInfo;
+ final boolean isEdgeToEdgeEnforced = PhoneWindow.isEdgeToEdgeEnforced(
+ activityInfo.applicationInfo, false /* local */, a);
+ if (isEdgeToEdgeEnforced) {
+ params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_EDGE_TO_EDGE_ENFORCED;
+ }
params.layoutInDisplayCutoutMode = a.getInt(
R.styleable.Window_windowLayoutInDisplayCutoutMode,
- PhoneWindow.isEdgeToEdgeEnforced(activityInfo.applicationInfo, false /* local */, a)
+ isEdgeToEdgeEnforced
? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
: params.layoutInDisplayCutoutMode);
params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
index e419462012e3..e07e1b460168 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenIconDrawableFactory.java
@@ -45,6 +45,7 @@ import android.window.SplashScreenView;
import com.android.internal.R;
+import java.io.Closeable;
import java.util.function.LongConsumer;
/**
@@ -100,7 +101,7 @@ public class SplashscreenIconDrawableFactory {
* Drawable pre-drawing the scaled icon in a separate thread to increase the speed of the
* final drawing.
*/
- private static class ImmobileIconDrawable extends Drawable {
+ private static class ImmobileIconDrawable extends Drawable implements Closeable {
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG
| Paint.FILTER_BITMAP_FLAG);
private final Matrix mMatrix = new Matrix();
@@ -154,6 +155,16 @@ public class SplashscreenIconDrawableFactory {
public int getOpacity() {
return 1;
}
+
+ @Override
+ public void close() {
+ synchronized (mPaint) {
+ if (mIconBitmap != null) {
+ mIconBitmap.recycle();
+ mIconBitmap = null;
+ }
+ }
+ }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java
index 31fc98b713ab..e552e6cdacf3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenWindowCreator.java
@@ -30,7 +30,6 @@ import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
-import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.hardware.display.DisplayManager;
@@ -54,7 +53,6 @@ import android.window.SplashScreenView;
import android.window.StartingWindowInfo;
import android.window.StartingWindowRemovalInfo;
-import com.android.internal.R;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.ContrastColorUtil;
import com.android.wm.shell.common.ShellExecutor;
@@ -206,7 +204,6 @@ class SplashscreenWindowCreator extends AbsSplashWindowCreator {
final SplashWindowRecord record =
(SplashWindowRecord) mStartingWindowRecordManager.getRecord(taskId);
if (record != null) {
- record.parseAppSystemBarColor(context);
// Block until we get the background color.
final SplashScreenView contentView = viewSupplier.get();
if (suggestType != STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
@@ -427,8 +424,6 @@ class SplashscreenWindowCreator extends AbsSplashWindowCreator {
private boolean mSetSplashScreen;
private SplashScreenView mSplashView;
- private int mSystemBarAppearance;
- private boolean mDrawsSystemBarBackgrounds;
SplashWindowRecord(IBinder appToken, View decorView,
@StartingWindowInfo.StartingWindowType int suggestType) {
@@ -448,38 +443,6 @@ class SplashscreenWindowCreator extends AbsSplashWindowCreator {
mSetSplashScreen = true;
}
- void parseAppSystemBarColor(Context context) {
- final TypedArray a = context.obtainStyledAttributes(R.styleable.Window);
- mDrawsSystemBarBackgrounds = a.getBoolean(
- R.styleable.Window_windowDrawsSystemBarBackgrounds, false);
- if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) {
- mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
- }
- if (a.getBoolean(R.styleable.Window_windowLightNavigationBar, false)) {
- mSystemBarAppearance |= WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
- }
- a.recycle();
- }
-
- // Reset the system bar color which set by splash screen, make it align to the app.
- void clearSystemBarColor() {
- if (mRootView == null || !mRootView.isAttachedToWindow()) {
- return;
- }
- if (mRootView.getLayoutParams() instanceof WindowManager.LayoutParams) {
- final WindowManager.LayoutParams lp =
- (WindowManager.LayoutParams) mRootView.getLayoutParams();
- if (mDrawsSystemBarBackgrounds) {
- lp.flags |= WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
- } else {
- lp.flags &= ~WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
- }
- mRootView.setLayoutParams(lp);
- }
- mRootView.getWindowInsetsController().setSystemBarsAppearance(
- mSystemBarAppearance, LIGHT_BARS_MASK);
- }
-
@Override
public boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) {
if (mRootView == null) {
@@ -491,7 +454,6 @@ class SplashscreenWindowCreator extends AbsSplashWindowCreator {
removeWindowInner(mRootView, false);
return true;
}
- clearSystemBarColor();
if (immediately
|| mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
removeWindowInner(mRootView, false);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
index e2be1533118a..3353c7bd81c2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
@@ -18,10 +18,12 @@ package com.android.wm.shell.startingsurface;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.window.StartingWindowRemovalInfo.DEFER_MODE_NONE;
import static android.window.StartingWindowRemovalInfo.DEFER_MODE_NORMAL;
import static android.window.StartingWindowRemovalInfo.DEFER_MODE_ROTATION;
import android.annotation.CallSuper;
+import android.annotation.NonNull;
import android.app.TaskInfo;
import android.app.WindowConfiguration;
import android.content.Context;
@@ -45,8 +47,8 @@ import com.android.internal.protolog.common.ProtoLog;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.annotations.ShellSplashscreenThread;
/**
* A class which able to draw splash screen or snapshot as the starting window for a task.
@@ -269,21 +271,18 @@ public class StartingSurfaceDrawer {
@Override
public final boolean removeIfPossible(StartingWindowRemovalInfo info, boolean immediately) {
- if (immediately) {
+ if (immediately
+ // Show the latest content as soon as possible for unlocking to home.
+ || mActivityType == ACTIVITY_TYPE_HOME
+ || info.deferRemoveMode == DEFER_MODE_NONE) {
removeImmediately();
- } else {
- scheduleRemove(info.deferRemoveForImeMode);
- return false;
+ return true;
}
- return true;
+ scheduleRemove(info.deferRemoveMode);
+ return false;
}
void scheduleRemove(@StartingWindowRemovalInfo.DeferMode int deferRemoveForImeMode) {
- // Show the latest content as soon as possible for unlocking to home.
- if (mActivityType == ACTIVITY_TYPE_HOME) {
- removeImmediately();
- return;
- }
mRemoveExecutor.removeCallbacks(mScheduledRunnable);
final long delayRemovalTime;
switch (deferRemoveForImeMode) {
@@ -306,7 +305,7 @@ public class StartingSurfaceDrawer {
@CallSuper
protected void removeImmediately() {
mRemoveExecutor.removeCallbacks(mScheduledRunnable);
- mRecordManager.onRecordRemoved(mTaskId);
+ mRecordManager.onRecordRemoved(this, mTaskId);
}
}
@@ -327,6 +326,11 @@ public class StartingSurfaceDrawer {
}
void addRecord(int taskId, StartingWindowRecord record) {
+ final StartingWindowRecord original = mStartingWindowRecords.get(taskId);
+ if (original != null) {
+ mTmpRemovalInfo.taskId = taskId;
+ original.removeIfPossible(mTmpRemovalInfo, true /* immediately */);
+ }
mStartingWindowRecords.put(taskId, record);
}
@@ -346,8 +350,11 @@ public class StartingSurfaceDrawer {
removeWindow(mTmpRemovalInfo, true/* immediately */);
}
- void onRecordRemoved(int taskId) {
- mStartingWindowRecords.remove(taskId);
+ void onRecordRemoved(@NonNull StartingWindowRecord record, int taskId) {
+ final StartingWindowRecord currentRecord = mStartingWindowRecords.get(taskId);
+ if (currentRecord == record) {
+ mStartingWindowRecords.remove(taskId);
+ }
}
StartingWindowRecord getRecord(int taskId) {
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 1a0c011205fb..66b3553bea09 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,12 +21,14 @@ 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;
import android.app.ActivityManager;
import android.app.ActivityManager.TaskDescription;
import android.graphics.Paint;
-import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
@@ -42,6 +44,8 @@ import android.view.SurfaceControl;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManagerGlobal;
+import android.view.WindowRelayoutResult;
+import android.window.ActivityWindowInfo;
import android.window.ClientWindowFrames;
import android.window.SnapshotDrawerUtils;
import android.window.StartingWindowInfo;
@@ -98,8 +102,6 @@ public class TaskSnapshotWindow {
return null;
}
- final Point taskSize = snapshot.getTaskSize();
- final Rect taskBounds = new Rect(0, 0, taskSize.x, taskSize.y);
final int orientation = snapshot.getOrientation();
final int displayId = runningTaskInfo.displayId;
@@ -137,9 +139,16 @@ public class TaskSnapshotWindow {
}
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout");
- session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0,
- tmpFrames, tmpMergedConfiguration, surfaceControl, tmpInsetsState,
- tmpControls, new Bundle());
+ 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());
+ }
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
} catch (RemoteException e) {
snapshotSurface.clearWindowSynced();
@@ -148,7 +157,7 @@ public class TaskSnapshotWindow {
}
SnapshotDrawerUtils.drawSnapshotOnSurface(info, layoutParams, surfaceControl, snapshot,
- taskBounds, tmpFrames.frame, topWindowInsetsState, true /* releaseAfterDraw */);
+ info.taskBounds, topWindowInsetsState, true /* releaseAfterDraw */);
snapshotSurface.mHasDrawn = true;
snapshotSurface.reportDrawn();
@@ -214,7 +223,7 @@ public class TaskSnapshotWindow {
public void resized(ClientWindowFrames frames, boolean reportDraw,
MergedConfiguration mergedConfiguration, InsetsState insetsState,
boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int seqId,
- boolean dragResizing) {
+ boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) {
final TaskSnapshotWindow snapshot = mOuter.get();
if (snapshot == null) {
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java
index fed2f34b5e0c..5c814dcc9b16 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java
@@ -23,7 +23,6 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.content.Context;
-import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.view.Display;
@@ -77,15 +76,13 @@ class WindowlessSnapshotWindowCreator {
runningTaskInfo.configuration, rootSurface);
final SurfaceControlViewHost mViewHost = new SurfaceControlViewHost(
mContext, display, wlw, "WindowlessSnapshotWindowCreator");
- final Point taskSize = snapshot.getTaskSize();
- final Rect snapshotBounds = new Rect(0, 0, taskSize.x, taskSize.y);
final Rect windowBounds = runningTaskInfo.configuration.windowConfiguration.getBounds();
final InsetsState topWindowInsetsState = info.topOpaqueWindowInsetsState;
final FrameLayout rootLayout = new FrameLayout(
mSplashscreenContentDrawer.createViewContextWrapper(mContext));
mViewHost.setView(rootLayout, lp);
SnapshotDrawerUtils.drawSnapshotOnSurface(info, lp, wlw.mChildSurface, snapshot,
- snapshotBounds, windowBounds, topWindowInsetsState, false /* releaseAfterDraw */);
+ windowBounds, topWindowInsetsState, false /* releaseAfterDraw */);
final ActivityManager.TaskDescription taskDescription =
SnapshotDrawerUtils.getOrCreateTaskDescription(runningTaskInfo);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java
new file mode 100644
index 000000000000..a94f80241d4f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/DisplayImeChangeListener.java
@@ -0,0 +1,34 @@
+/*
+ * 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.sysui;
+
+import android.graphics.Rect;
+
+/**
+ * Callbacks for when the Display IME changes.
+ */
+public interface DisplayImeChangeListener {
+ /**
+ * Called when the ime bounds change.
+ */
+ default void onImeBoundsChanged(int displayId, Rect bounds) {}
+
+ /**
+ * Called when the IME visibility change.
+ */
+ default void onImeVisibilityChanged(int displayId, boolean isShowing) {}
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
index a7843e218a8a..5ced1fb41a41 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
@@ -30,21 +30,28 @@ import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
+import android.graphics.Rect;
import android.os.Bundle;
import android.util.ArrayMap;
+import android.view.InsetsSource;
+import android.view.InsetsState;
import android.view.SurfaceControlRegistry;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.common.DisplayInsetsController;
+import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
import java.io.PrintWriter;
import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
import java.util.function.Supplier;
/**
@@ -57,6 +64,7 @@ public class ShellController {
private final ShellInit mShellInit;
private final ShellCommandHandler mShellCommandHandler;
private final ShellExecutor mMainExecutor;
+ private final DisplayInsetsController mDisplayInsetsController;
private final ShellInterfaceImpl mImpl = new ShellInterfaceImpl();
private final CopyOnWriteArrayList<ConfigurationChangeListener> mConfigChangeListeners =
@@ -65,6 +73,8 @@ public class ShellController {
new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<UserChangeListener> mUserChangeListeners =
new CopyOnWriteArrayList<>();
+ private final ConcurrentHashMap<DisplayImeChangeListener, Executor> mDisplayImeChangeListeners =
+ new ConcurrentHashMap<>();
private ArrayMap<String, Supplier<ExternalInterfaceBinder>> mExternalInterfaceSuppliers =
new ArrayMap<>();
@@ -73,20 +83,53 @@ public class ShellController {
private Configuration mLastConfiguration;
+ private OnInsetsChangedListener mInsetsChangeListener = new OnInsetsChangedListener() {
+ private InsetsState mInsetsState = new InsetsState();
+
+ @Override
+ public void insetsChanged(InsetsState insetsState) {
+ if (mInsetsState == insetsState) {
+ return;
+ }
+
+ InsetsSource oldSource = mInsetsState.peekSource(InsetsSource.ID_IME);
+ boolean wasVisible = (oldSource != null && oldSource.isVisible());
+ Rect oldFrame = wasVisible ? oldSource.getFrame() : null;
+
+ InsetsSource newSource = insetsState.peekSource(InsetsSource.ID_IME);
+ boolean isVisible = (newSource != null && newSource.isVisible());
+ Rect newFrame = isVisible ? newSource.getFrame() : null;
+
+ if (wasVisible != isVisible) {
+ onImeVisibilityChanged(isVisible);
+ }
+
+ if (newFrame != null && !newFrame.equals(oldFrame)) {
+ onImeBoundsChanged(newFrame);
+ }
+
+ mInsetsState = insetsState;
+ }
+ };
+
public ShellController(Context context,
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
+ DisplayInsetsController displayInsetsController,
ShellExecutor mainExecutor) {
mContext = context;
mShellInit = shellInit;
mShellCommandHandler = shellCommandHandler;
+ mDisplayInsetsController = displayInsetsController;
mMainExecutor = mainExecutor;
shellInit.addInitCallback(this::onInit, this);
}
private void onInit() {
mShellCommandHandler.addDumpCallback(this::dump, this);
+ mDisplayInsetsController.addInsetsChangedListener(
+ mContext.getDisplayId(), mInsetsChangeListener);
}
/**
@@ -259,6 +302,25 @@ public class ShellController {
}
}
+ @VisibleForTesting
+ void onImeBoundsChanged(Rect bounds) {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Display Ime bounds changed");
+ mDisplayImeChangeListeners.forEach(
+ (DisplayImeChangeListener listener, Executor executor) ->
+ executor.execute(() -> listener.onImeBoundsChanged(
+ mContext.getDisplayId(), bounds)));
+ }
+
+ @VisibleForTesting
+ void onImeVisibilityChanged(boolean isShowing) {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Display Ime visibility changed: isShowing=%b",
+ isShowing);
+ mDisplayImeChangeListeners.forEach(
+ (DisplayImeChangeListener listener, Executor executor) ->
+ executor.execute(() -> listener.onImeVisibilityChanged(
+ mContext.getDisplayId(), isShowing)));
+ }
+
private void handleInit() {
SurfaceControlRegistry.createProcessInstance(mContext);
mShellInit.init();
@@ -329,6 +391,19 @@ public class ShellController {
}
@Override
+ public void addDisplayImeChangeListener(DisplayImeChangeListener listener,
+ Executor executor) {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Adding new DisplayImeChangeListener");
+ mDisplayImeChangeListeners.put(listener, executor);
+ }
+
+ @Override
+ public void removeDisplayImeChangeListener(DisplayImeChangeListener listener) {
+ ProtoLog.v(WM_SHELL_SYSUI_EVENTS, "Removing DisplayImeChangeListener");
+ mDisplayImeChangeListeners.remove(listener);
+ }
+
+ @Override
public boolean handleCommand(String[] args, PrintWriter pw) {
try {
boolean[] result = new boolean[1];
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
index bc5dd11ef54e..bd1c64a0d182 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInterface.java
@@ -25,6 +25,7 @@ import androidx.annotation.NonNull;
import java.io.PrintWriter;
import java.util.List;
+import java.util.concurrent.Executor;
/**
* General interface for notifying the Shell of common SysUI events like configuration or keyguard
@@ -65,6 +66,18 @@ public interface ShellInterface {
default void onUserProfilesChanged(@NonNull List<UserInfo> profiles) {}
/**
+ * Registers a DisplayImeChangeListener to monitor for changes on Ime
+ * position and visibility.
+ */
+ default void addDisplayImeChangeListener(DisplayImeChangeListener listener,
+ Executor executor) {}
+
+ /**
+ * Removes a registered DisplayImeChangeListener.
+ */
+ default void removeDisplayImeChangeListener(DisplayImeChangeListener listener) {}
+
+ /**
* Handles a shell command.
*/
default boolean handleCommand(final String[] args, PrintWriter pw) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java
index 56c0d0e67cab..c886cc999216 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellSharedConstants.java
@@ -42,4 +42,7 @@ public class ShellSharedConstants {
public static final String KEY_EXTRA_SHELL_DESKTOP_MODE = "extra_shell_desktop_mode";
// See IDragAndDrop.aidl
public static final String KEY_EXTRA_SHELL_DRAG_AND_DROP = "extra_shell_drag_and_drop";
+ // See IRecentsAnimationController.aidl
+ public static final String KEY_EXTRA_SHELL_CAN_HAND_OFF_ANIMATION =
+ "extra_shell_can_hand_off_animation";
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
index 35a1fa0a92f6..a85188a9e04d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java
@@ -30,7 +30,6 @@ import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Handler;
-import android.os.Looper;
import android.view.SurfaceControl;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
@@ -121,6 +120,11 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback,
@Override
public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
+ if (mTaskViewTaskController.isUsingShellTransitions()) {
+ // No need for additional work as it is already taken care of during
+ // prepareOpenAnimation().
+ return;
+ }
onLocationChanged();
if (taskInfo.taskDescription != null) {
final int bgColor = taskInfo.taskDescription.getBackgroundColor();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java
index a7e4b0119480..f0a2315d7deb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java
@@ -19,7 +19,7 @@ package com.android.wm.shell.taskview;
import android.annotation.UiContext;
import android.content.Context;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java
index 7eed5883043d..e4fcff0c372a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java
@@ -22,7 +22,7 @@ import android.content.Context;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
index 196e04edbb10..a126cbe41b00 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
@@ -17,6 +17,7 @@
package com.android.wm.shell.taskview;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.view.WindowManager.TRANSIT_CHANGE;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -29,6 +30,7 @@ import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.graphics.Rect;
+import android.gui.TrustedOverlay;
import android.os.Binder;
import android.util.CloseGuard;
import android.util.Slog;
@@ -48,6 +50,23 @@ import java.util.concurrent.Executor;
* TaskView} to {@link TaskViewTaskController} interactions are done via direct method calls.
*
* The reverse communication is done via the {@link TaskViewBase} interface.
+ *
+ * <ul>
+ * <li>The entry point for an activity based task view is {@link
+ * TaskViewTaskController#startActivity(PendingIntent, Intent, ActivityOptions, Rect)}</li>
+ *
+ * <li>The entry point for an activity (represented by {@link ShortcutInfo}) based task view
+ * is {@link TaskViewTaskController#startShortcutActivity(ShortcutInfo, ActivityOptions, Rect)}
+ * </li>
+ *
+ * <li>The entry point for a root-task based task view is {@link
+ * TaskViewTaskController#startRootTask(ActivityManager.RunningTaskInfo, SurfaceControl,
+ * WindowContainerTransaction)}.
+ * This method is special as it doesn't create a root task and instead expects that the
+ * launch root task is already created and started. This method just attaches the taskInfo to
+ * the TaskView.
+ * </li>
+ * </ul>
*/
public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener {
@@ -155,8 +174,8 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener {
* <p>The owner of this container must be allowed to access the shortcut information,
* as defined in {@link LauncherApps#hasShortcutHostPermission()} to use this method.
*
- * @param shortcut the shortcut used to launch the activity.
- * @param options options for the activity.
+ * @param shortcut the shortcut used to launch the activity.
+ * @param options options for the activity.
* @param launchBounds the bounds (window size and position) that the activity should be
* launched in, in pixels and in screen coordinates.
*/
@@ -183,10 +202,10 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener {
* Launch a new activity.
*
* @param pendingIntent Intent used to launch an activity.
- * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()}
- * @param options options for the activity.
- * @param launchBounds the bounds (window size and position) that the activity should be
- * launched in, in pixels and in screen coordinates.
+ * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()}
+ * @param options options for the activity.
+ * @param launchBounds the bounds (window size and position) that the activity should be
+ * launched in, in pixels and in screen coordinates.
*/
public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent,
@NonNull ActivityOptions options, @Nullable Rect launchBounds) {
@@ -208,6 +227,35 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener {
}
}
+
+ /**
+ * Attaches the given root task {@code taskInfo} in the task view.
+ *
+ * <p> Since {@link ShellTaskOrganizer#createRootTask(int, int,
+ * ShellTaskOrganizer.TaskListener)} does not use the shell transitions flow, this method is
+ * used as an entry point for an already-created root-task in the task view.
+ *
+ * @param taskInfo the task info of the root task.
+ * @param leash the {@link android.content.pm.ShortcutInfo.Surface} of the root task
+ * @param wct The Window container work that should happen as part of this set up.
+ */
+ public void startRootTask(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash,
+ @Nullable WindowContainerTransaction wct) {
+ if (wct == null) {
+ wct = new WindowContainerTransaction();
+ }
+ // This method skips the regular flow where an activity task is launched as part of a new
+ // transition in taskview and then transition is intercepted using the launchcookie.
+ // The task here is already created and running, it just needs to be reparented, resized
+ // and tracked correctly inside taskview. Which is done by calling
+ // prepareOpenAnimationInternal() and then manually enqueuing the resulting window container
+ // transaction.
+ prepareOpenAnimationInternal(true /* newTask */, mTransaction /* startTransaction */,
+ null /* finishTransaction */, taskInfo, leash, wct);
+ mTransaction.apply();
+ mTaskViewTransitions.startInstantTransition(TRANSIT_CHANGE, wct);
+ }
+
private void prepareActivityOptions(ActivityOptions options, Rect launchBounds) {
final Binder launchCookie = new Binder();
mShellExecutor.execute(() -> {
@@ -342,7 +390,6 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener {
final SurfaceControl taskLeash = mTaskLeash;
handleAndNotifyTaskRemoval(mTaskInfo);
- // Unparent the task when this surface is destroyed
mTransaction.reparent(taskLeash, null).apply();
resetTaskInfo();
}
@@ -402,6 +449,14 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener {
mSurfaceCreated = true;
mIsInitialized = true;
mSurfaceControl = surfaceControl;
+ // SurfaceControl is expected to be null only in the case of unit tests. Guard against it
+ // to avoid runtime exception in SurfaceControl.Transaction.
+ if (surfaceControl != null) {
+ // TaskView is meant to contain app activities which shouldn't have trusted overlays
+ // flag set even when itself reparented in a window which is trusted.
+ mTransaction.setTrustedOverlay(surfaceControl, TrustedOverlay.DISABLED)
+ .apply();
+ }
notifyInitialized();
mShellExecutor.execute(() -> {
if (mTaskToken == null) {
@@ -597,6 +652,15 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener {
@NonNull SurfaceControl.Transaction finishTransaction,
ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash,
WindowContainerTransaction wct) {
+ prepareOpenAnimationInternal(newTask, startTransaction, finishTransaction, taskInfo, leash,
+ wct);
+ }
+
+ private void prepareOpenAnimationInternal(final boolean newTask,
+ SurfaceControl.Transaction startTransaction,
+ SurfaceControl.Transaction finishTransaction,
+ ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash,
+ WindowContainerTransaction wct) {
mPendingInfo = null;
mTaskInfo = taskInfo;
mTaskToken = mTaskInfo.token;
@@ -608,10 +672,12 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener {
// Also reparent on finishTransaction since the finishTransaction will reparent back
// to its "original" parent by default.
Rect boundsOnScreen = mTaskViewBase.getCurrentBoundsOnScreen();
- finishTransaction.reparent(mTaskLeash, mSurfaceControl)
- .setPosition(mTaskLeash, 0, 0)
- // TODO: maybe once b/280900002 is fixed this will be unnecessary
- .setWindowCrop(mTaskLeash, boundsOnScreen.width(), boundsOnScreen.height());
+ if (finishTransaction != null) {
+ finishTransaction.reparent(mTaskLeash, mSurfaceControl)
+ .setPosition(mTaskLeash, 0, 0)
+ // TODO: maybe once b/280900002 is fixed this will be unnecessary
+ .setWindowCrop(mTaskLeash, boundsOnScreen.width(), boundsOnScreen.height());
+ }
mTaskViewTransitions.updateBoundsState(this, boundsOnScreen);
mTaskViewTransitions.updateVisibilityState(this, true /* visible */);
wct.setBounds(mTaskToken, boundsOnScreen);
@@ -632,6 +698,7 @@ public class TaskViewTaskController implements ShellTaskOrganizer.TaskListener {
mTaskViewBase.setResizeBgColor(startTransaction, backgroundColor);
}
+ mTaskViewBase.onTaskAppeared(mTaskInfo, mTaskLeash);
if (mListener != null) {
final int taskId = mTaskInfo.taskId;
final ComponentName baseActivity = mTaskInfo.baseActivity;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
index 198ec82b5f21..e6d1b4593a46 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java
@@ -53,7 +53,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler {
new ArrayMap<>();
private final ArrayList<PendingTransition> mPending = new ArrayList<>();
private final Transitions mTransitions;
- private final boolean[] mRegistered = new boolean[]{ false };
+ private final boolean[] mRegistered = new boolean[]{false};
/**
* TaskView makes heavy use of startTransition. Only one shell-initiated transition can be
@@ -122,6 +122,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler {
/**
* Looks through the pending transitions for a closing transaction that matches the provided
* `taskView`.
+ *
* @param taskView the pending transition should be for this.
*/
private PendingTransition findPendingCloseTransition(TaskViewTaskController taskView) {
@@ -135,8 +136,17 @@ public class TaskViewTransitions implements Transitions.TransitionHandler {
}
/**
+ * Starts a transition outside of the handler associated with {@link TaskViewTransitions}.
+ */
+ public void startInstantTransition(@WindowManager.TransitionType int type,
+ WindowContainerTransaction wct) {
+ mTransitions.startTransition(type, wct, null);
+ }
+
+ /**
* Looks through the pending transitions for a opening transaction that matches the provided
* `taskView`.
+ *
* @param taskView the pending transition should be for this.
*/
@VisibleForTesting
@@ -152,8 +162,9 @@ public class TaskViewTransitions implements Transitions.TransitionHandler {
/**
* Looks through the pending transitions for one matching `taskView`.
+ *
* @param taskView the pending transition should be for this.
- * @param type the type of transition it's looking for
+ * @param type the type of transition it's looking for
*/
PendingTransition findPending(TaskViewTaskController taskView, int type) {
for (int i = mPending.size() - 1; i >= 0; --i) {
@@ -220,7 +231,24 @@ public class TaskViewTransitions implements Transitions.TransitionHandler {
startNextTransition();
}
- void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) {
+ /** Starts a new transition to make the given {@code taskView} visible. */
+ public void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) {
+ setTaskViewVisible(taskView, visible, false /* reorder */);
+ }
+
+ /**
+ * Starts a new transition to make the given {@code taskView} visible and optionally change
+ * the task order.
+ *
+ * @param taskView the task view which the visibility is being changed for
+ * @param visible the new visibility of the task view
+ * @param reorder whether to reorder the task or not. If this is {@code true}, the task will be
+ * reordered as per the given {@code visible}. For {@code visible = true}, task
+ * will be reordered to top. For {@code visible = false}, task will be reordered
+ * to the bottom
+ */
+ public void setTaskViewVisible(TaskViewTaskController taskView, boolean visible,
+ boolean reorder) {
if (mTaskViews.get(taskView) == null) return;
if (mTaskViews.get(taskView).mVisible == visible) return;
if (taskView.getTaskInfo() == null) {
@@ -231,6 +259,9 @@ public class TaskViewTransitions implements Transitions.TransitionHandler {
final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setHidden(taskView.getTaskInfo().token, !visible /* hidden */);
wct.setBounds(taskView.getTaskInfo().token, mTaskViews.get(taskView).mBounds);
+ if (reorder) {
+ wct.reorder(taskView.getTaskInfo().token, visible /* onTop */);
+ }
PendingTransition pending = new PendingTransition(
visible ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, wct, taskView, null /* cookie */);
mPending.add(pending);
@@ -238,6 +269,22 @@ public class TaskViewTransitions implements Transitions.TransitionHandler {
// visibility is reported in transition.
}
+ /** Starts a new transition to reorder the given {@code taskView}'s task. */
+ public void reorderTaskViewTask(TaskViewTaskController taskView, boolean onTop) {
+ if (mTaskViews.get(taskView) == null) return;
+ if (taskView.getTaskInfo() == null) {
+ // Nothing to update, task is not yet available
+ return;
+ }
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.reorder(taskView.getTaskInfo().token, onTop /* onTop */);
+ PendingTransition pending = new PendingTransition(
+ onTop ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, wct, taskView, null /* cookie */);
+ mPending.add(pending);
+ startNextTransition();
+ // visibility is reported in transition.
+ }
+
void updateBoundsState(TaskViewTaskController taskView, Rect boundsOnScreen) {
TaskViewRequestedState state = mTaskViews.get(taskView);
if (state == null) return;
@@ -380,7 +427,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler {
}
startTransaction.reparent(chg.getLeash(), tv.getSurfaceControl());
finishTransaction.reparent(chg.getLeash(), tv.getSurfaceControl())
- .setPosition(chg.getLeash(), 0, 0);
+ .setPosition(chg.getLeash(), 0, 0);
changesHandled++;
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 8746b8c8d55c..8ee1efa90a30 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -61,9 +61,10 @@ import java.util.function.Consumer;
/**
* A handler for dealing with transitions involving multiple other handlers. For example: an
- * activity in split-screen going into PiP.
+ * activity in split-screen going into PiP. Note this is provided as a handset-specific
+ * implementation of {@code MixedTransitionHandler}.
*/
-public class DefaultMixedHandler implements Transitions.TransitionHandler,
+public class DefaultMixedHandler implements MixedTransitionHandler,
RecentsTransitionHandler.RecentsMixedHandler {
private final Transitions mPlayer;
@@ -76,6 +77,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler,
private ActivityEmbeddingController mActivityEmbeddingController;
abstract static class MixedTransition {
+ /** Entering Pip from split, breaks split. */
static final int TYPE_ENTER_PIP_FROM_SPLIT = 1;
/** Both the display and split-state (enter/exit) is changing */
@@ -102,6 +104,12 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler,
/** Enter pip from one of the Activity Embedding windows. */
static final int TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING = 9;
+ /** Entering Pip from split, but replace the Pip stage instead of breaking split. */
+ static final int TYPE_ENTER_PIP_REPLACE_FROM_SPLIT = 10;
+
+ /** The display changes when pip is entering. */
+ static final int TYPE_ENTER_PIP_WITH_DISPLAY_CHANGE = 11;
+
/** The default animation for this mixed transition. */
static final int ANIM_TYPE_DEFAULT = 0;
@@ -116,7 +124,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler,
final IBinder mTransition;
protected final Transitions mPlayer;
- protected final DefaultMixedHandler mMixedHandler;
+ protected final MixedTransitionHandler mMixedHandler;
protected final PipTransitionController mPipHandler;
protected final StageCoordinator mSplitHandler;
protected final KeyguardTransitionHandler mKeyguardHandler;
@@ -142,7 +150,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler,
int mInFlightSubAnimations = 0;
MixedTransition(int type, IBinder transition, Transitions player,
- DefaultMixedHandler mixedHandler, PipTransitionController pipHandler,
+ MixedTransitionHandler mixedHandler, PipTransitionController pipHandler,
StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler) {
mType = type;
mTransition = transition;
@@ -228,6 +236,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler,
// Add after dependencies because it is higher priority
shellInit.addInitCallback(() -> {
mPipHandler = pipTransitionController;
+ pipTransitionController.setMixedHandler(this);
mSplitHandler = splitScreenControllerOptional.get().getTransitionHandler();
mPlayer.addHandler(this);
if (mSplitHandler != null) {
@@ -425,7 +434,8 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler,
ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
"Converting mixed transition into a keyguard transition");
// Consume the original mixed transition
- onTransitionConsumed(transition, false, null);
+ mActiveTransitions.remove(mixed);
+ mixed.onTransitionConsumed(transition, false, null);
return true;
} else {
// Keyguard handler cannot handle it, process through original mixed
@@ -482,9 +492,11 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler,
// TODO(b/287704263): Remove when split/mixed are reversed.
public boolean animatePendingEnterPipFromSplit(IBinder transition, TransitionInfo info,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
- Transitions.TransitionFinishCallback finishCallback) {
- final MixedTransition mixed = createDefaultMixedTransition(
- MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition);
+ Transitions.TransitionFinishCallback finishCallback, boolean replacingPip) {
+ int type = replacingPip
+ ? MixedTransition.TYPE_ENTER_PIP_REPLACE_FROM_SPLIT
+ : MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT;
+ final MixedTransition mixed = createDefaultMixedTransition(type, transition);
mActiveTransitions.add(mixed);
Transitions.TransitionFinishCallback callback = wct -> {
mActiveTransitions.remove(mixed);
@@ -542,6 +554,47 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler,
return true;
}
+ /**
+ * For example: pip is entering in rotation 0, and then the display changes to rotation 90
+ * before the pip transition is ready. So the info contains both the entering pip and display
+ * change. In this case, the pip can go to the end state in new rotation directly, and let the
+ * display level animation cover all changed participates.
+ */
+ public void animateEnteringPipWithDisplayChange(@NonNull IBinder transition,
+ @NonNull TransitionInfo info, @NonNull TransitionInfo.Change pipChange,
+ @NonNull SurfaceControl.Transaction startT,
+ @NonNull SurfaceControl.Transaction finishT,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ // In order to play display level animation, force the type to CHANGE (it could be PIP).
+ final TransitionInfo changeInfo = info.getType() != TRANSIT_CHANGE
+ ? subCopy(info, TRANSIT_CHANGE, true /* withChanges */) : info;
+ final MixedTransition mixed = createDefaultMixedTransition(
+ MixedTransition.TYPE_ENTER_PIP_WITH_DISPLAY_CHANGE, transition);
+ mActiveTransitions.add(mixed);
+ mixed.mInFlightSubAnimations = 2;
+ final Transitions.TransitionFinishCallback finishCB = wct -> {
+ --mixed.mInFlightSubAnimations;
+ mixed.joinFinishArgs(wct);
+ if (mixed.mInFlightSubAnimations > 0) return;
+ mActiveTransitions.remove(mixed);
+ finishCallback.onTransitionFinished(mixed.mFinishWCT);
+ };
+ // Perform the display animation first.
+ mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, changeInfo,
+ startT, finishT, finishCB, mPipHandler);
+ // Use a standalone finish transaction for pip because it will apply immediately.
+ final SurfaceControl.Transaction pipFinishT = new SurfaceControl.Transaction();
+ mPipHandler.startEnterAnimation(pipChange, startT, pipFinishT, wct -> {
+ // Apply immediately to avoid potential flickering by bounds change at the end of
+ // display animation.
+ mPipHandler.applyTransaction(wct);
+ finishCB.onTransitionFinished(null /* wct */);
+ });
+ // Jump to the pip end state directly and make sure the real finishT have the latest state.
+ mPipHandler.end();
+ mPipHandler.syncPipSurfaceState(info, startT, finishT);
+ }
+
private static boolean animateKeyguard(@NonNull final MixedTransition mixed,
@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@@ -562,22 +615,23 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler,
/** Use to when split use intent to enter, check if this enter transition should be mixed or
* not.*/
- public boolean shouldSplitEnterMixed(PendingIntent intent) {
+ public boolean isIntentInPip(PendingIntent intent) {
// Check if this intent package is same as pip one or not, if true we want let the pip
// task enter split.
if (mPipHandler != null) {
- return mPipHandler.isInPipPackage(SplitScreenUtils.getPackageName(intent.getIntent()));
+ return mPipHandler
+ .isPackageActiveInPip(SplitScreenUtils.getPackageName(intent.getIntent()));
}
return false;
}
/** Use to when split use taskId to enter, check if this enter transition should be mixed or
* not.*/
- public boolean shouldSplitEnterMixed(int taskId, ShellTaskOrganizer shellTaskOrganizer) {
+ public boolean isTaskInPip(int taskId, ShellTaskOrganizer shellTaskOrganizer) {
// Check if this intent package is same as pip one or not, if true we want let the pip
// task enter split.
if (mPipHandler != null) {
- return mPipHandler.isInPipPackage(
+ return mPipHandler.isPackageActiveInPip(
SplitScreenUtils.getPackageName(taskId, shellTaskOrganizer));
}
return false;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
index e9cd73b0df5e..c33fb80fdefc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
@@ -41,7 +41,7 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
private final ActivityEmbeddingController mActivityEmbeddingController;
DefaultMixedTransition(int type, IBinder transition, Transitions player,
- DefaultMixedHandler mixedHandler, PipTransitionController pipHandler,
+ MixedTransitionHandler mixedHandler, PipTransitionController pipHandler,
StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler,
UnfoldTransitionHandler unfoldHandler,
ActivityEmbeddingController activityEmbeddingController) {
@@ -70,13 +70,18 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback) {
return switch (mType) {
- case TYPE_DISPLAY_AND_SPLIT_CHANGE -> false;
+ case TYPE_DISPLAY_AND_SPLIT_CHANGE, TYPE_ENTER_PIP_WITH_DISPLAY_CHANGE -> false;
case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING ->
animateEnterPipFromActivityEmbedding(
info, startTransaction, finishTransaction, finishCallback);
case TYPE_ENTER_PIP_FROM_SPLIT ->
animateEnterPipFromSplit(this, info, startTransaction, finishTransaction,
- finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
+ finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler,
+ /*replacingPip*/ false);
+ case TYPE_ENTER_PIP_REPLACE_FROM_SPLIT ->
+ animateEnterPipFromSplit(this, info, startTransaction, finishTransaction,
+ finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler,
+ /*replacingPip*/ true);
case TYPE_KEYGUARD ->
animateKeyguard(this, info, startTransaction, finishTransaction, finishCallback,
mKeyguardHandler, mPipHandler);
@@ -248,6 +253,7 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
@NonNull Transitions.TransitionFinishCallback finishCallback) {
switch (mType) {
case TYPE_DISPLAY_AND_SPLIT_CHANGE:
+ case TYPE_ENTER_PIP_WITH_DISPLAY_CHANGE:
// queue since no actual animation.
return;
case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
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 74e85f8dd468..018c9044e2f7 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;
@@ -507,6 +508,15 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
final Point animRelOffset = new Point(
change.getEndAbsBounds().left - animRoot.getOffset().x,
change.getEndAbsBounds().top - animRoot.getOffset().y);
+
+ if (change.getActivityComponent() != null) {
+ // For appcompat letterbox: we intentionally report the task-bounds so that we
+ // can animate as-if letterboxes are "part of" the activity. This means we can't
+ // always rely solely on endAbsBounds and need to also max with endRelOffset.
+ animRelOffset.x = Math.max(animRelOffset.x, change.getEndRelOffset().x);
+ animRelOffset.y = Math.max(animRelOffset.y, change.getEndRelOffset().y);
+ }
+
if (change.getActivityComponent() != null && !isActivityLevel) {
// At this point, this is an independent activity change in a non-activity
// transition. This means that an activity transition got erroneously combined
@@ -534,7 +544,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);
}
@@ -585,7 +601,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
.setName("animation-background")
.setCallsite("DefaultTransitionHandler")
.setColorLayer();
- final SurfaceControl backgroundSurface = colorLayerBuilder.build();
// Attaching the background surface to the transition root could unexpectedly make it
// cover one of the split root tasks. To avoid this, put the background surface just
@@ -596,8 +611,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
if (isSplitTaskInvolved) {
mRootTDAOrganizer.attachToDisplayArea(displayId, colorLayerBuilder);
} else {
- startTransaction.reparent(backgroundSurface, info.getRootLeash());
+ colorLayerBuilder.setParent(info.getRootLeash());
}
+
+ final SurfaceControl backgroundSurface = colorLayerBuilder.build();
startTransaction.setColor(backgroundSurface, colorArray)
.setLayer(backgroundSurface, -1)
.show(backgroundSurface);
@@ -715,7 +732,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 cb2944c120e0..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
@@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP;
import static com.android.wm.shell.transition.Transitions.TransitionObserver;
import android.annotation.NonNull;
@@ -32,6 +33,7 @@ import android.window.TransitionInfo;
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.shared.IHomeTransitionListener;
import com.android.wm.shell.shared.TransitionUtil;
/**
@@ -59,7 +61,8 @@ public class HomeTransitionObserver implements TransitionObserver,
@NonNull SurfaceControl.Transaction finishTransaction) {
for (TransitionInfo.Change change : info.getChanges()) {
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
- if (taskInfo == null
+ if (info.getType() == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP
+ || taskInfo == null
|| taskInfo.displayId != DEFAULT_DISPLAY
|| taskInfo.taskId == -1
|| !taskInfo.isRunning) {
@@ -130,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/LegacyTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
index 61e11e877b90..89b0e25b306b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/LegacyTransitions.java
@@ -16,6 +16,8 @@
package com.android.wm.shell.transition;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TRANSITIONS;
+
import android.annotation.NonNull;
import android.os.RemoteException;
import android.view.IRemoteAnimationFinishedCallback;
@@ -26,6 +28,8 @@ import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.IWindowContainerTransactionCallback;
+import com.android.internal.protolog.common.ProtoLog;
+
/**
* Utilities and interfaces for transition-like usage on top of the legacy app-transition and
* synctransaction tools.
@@ -87,9 +91,11 @@ public class LegacyTransitions {
@Override
public void onTransactionReady(int id, SurfaceControl.Transaction t)
throws RemoteException {
+ ProtoLog.v(WM_SHELL_TRANSITIONS,
+ "LegacyTransitions.onTransactionReady(): syncId=%d", id);
mSyncId = id;
mTransaction = t;
- checkApply();
+ checkApply(true /* log */);
}
}
@@ -103,20 +109,29 @@ public class LegacyTransitions {
mWallpapers = wallpapers;
mNonApps = nonApps;
mFinishCallback = finishedCallback;
- checkApply();
+ checkApply(false /* log */);
}
@Override
public void onAnimationCancelled() throws RemoteException {
mCancelled = true;
mApps = mWallpapers = mNonApps = null;
- checkApply();
+ checkApply(false /* log */);
}
}
- private void checkApply() throws RemoteException {
- if (mSyncId < 0 || (mFinishCallback == null && !mCancelled)) return;
+ private void checkApply(boolean log) throws RemoteException {
+ if (mSyncId < 0 || (mFinishCallback == null && !mCancelled)) {
+ if (log) {
+ ProtoLog.v(WM_SHELL_TRANSITIONS, "\tSkipping hasFinishedCb=%b canceled=%b",
+ mFinishCallback != null, mCancelled);
+ }
+ return;
+ }
+ if (log) {
+ ProtoLog.v(WM_SHELL_TRANSITIONS, "\tapply");
+ }
mLegacyTransition.onAnimationStart(mTransit, mApps, mWallpapers,
mNonApps, mFinishCallback, mTransaction);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHandler.java
new file mode 100644
index 000000000000..ff429fb12c94
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHandler.java
@@ -0,0 +1,31 @@
+/*
+ * 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.transition;
+
+/**
+ * Interface for a {@link Transitions.TransitionHandler} that can take the subset of transitions
+ * that it handles and further decompose those transitions into sub-transitions which can be
+ * independently delegated to separate handlers.
+ */
+public interface MixedTransitionHandler extends Transitions.TransitionHandler {
+
+ // TODO(b/335685449) this currently exists purely as a marker interface for use in form-factor
+ // specific/sysui dagger modules. Going forward, we should define this in a meaningful
+ // way so as to provide a clear basis for expectations/behaviours associated with mixed
+ // transitions and their default handlers.
+
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
index 0974cd13f249..e8b01b5880fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
@@ -23,11 +23,15 @@ import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
+import static com.android.wm.shell.shared.TransitionUtil.isOpeningMode;
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
import static com.android.wm.shell.transition.DefaultMixedHandler.subCopy;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
@@ -44,8 +48,9 @@ public class MixedTransitionHelper {
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull Transitions.TransitionFinishCallback finishCallback,
- @NonNull Transitions player, @NonNull DefaultMixedHandler mixedHandler,
- @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler) {
+ @NonNull Transitions player, @NonNull MixedTransitionHandler mixedHandler,
+ @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler,
+ boolean replacingPip) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
+ "entering PIP while Split-Screen is foreground.");
TransitionInfo.Change pipChange = null;
@@ -99,7 +104,7 @@ public class MixedTransitionHelper {
// we need a separate one to send over to launcher.
SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
@SplitScreen.StageType int topStageToKeep = STAGE_TYPE_UNDEFINED;
- if (splitHandler.isSplitScreenVisible()) {
+ if (splitHandler.isSplitScreenVisible() && !replacingPip) {
// The non-going home case, we could be pip-ing one of the split stages and keep
// showing the other
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
@@ -115,11 +120,12 @@ public class MixedTransitionHelper {
break;
}
}
+
+ // Let split update internal state for dismiss.
+ splitHandler.prepareDismissAnimation(topStageToKeep,
+ EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT,
+ finishTransaction);
}
- // Let split update internal state for dismiss.
- splitHandler.prepareDismissAnimation(topStageToKeep,
- EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT,
- finishTransaction);
// We are trying to accommodate launcher's close animation which can't handle the
// divider-bar, so if split-handler is closing the divider-bar, just hide it and
@@ -152,6 +158,44 @@ public class MixedTransitionHelper {
return true;
}
+ /**
+ * Check to see if we're only closing split to enter pip or if we're replacing pip with
+ * another task. If we are replacing, this will return the change for the task we are replacing
+ * pip with
+ *
+ * @param info Any number of changes
+ * @param pipChange TransitionInfo.Change indicating the task that is being pipped
+ * @param splitMainStageRootId MainStage's rootTaskInfo's id
+ * @param splitSideStageRootId SideStage's rootTaskInfo's id
+ * @param lastPipSplitStage The last stage that {@param pipChange} was in
+ * @return The change from {@param info} that is replacing the {@param pipChange}, {@code null}
+ * otherwise
+ */
+ @Nullable
+ public static TransitionInfo.Change getPipReplacingChange(TransitionInfo info,
+ TransitionInfo.Change pipChange, int splitMainStageRootId, int splitSideStageRootId,
+ @SplitScreen.StageType int lastPipSplitStage) {
+ int lastPipParentTask = -1;
+ if (lastPipSplitStage == STAGE_TYPE_MAIN) {
+ lastPipParentTask = splitMainStageRootId;
+ } else if (lastPipSplitStage == STAGE_TYPE_SIDE) {
+ lastPipParentTask = splitSideStageRootId;
+ }
+
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ TransitionInfo.Change change = info.getChanges().get(i);
+ if (change == pipChange || !isOpeningMode(change.getMode())) {
+ // Ignore the change/task that's going into Pip or not opening
+ continue;
+ }
+
+ if (change.getTaskInfo().parentTaskId == lastPipParentTask) {
+ return change;
+ }
+ }
+ return null;
+ }
+
private static boolean isHomeOpening(@NonNull TransitionInfo.Change change) {
return change.getTaskInfo() != null
&& change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
index 94519a0d118c..69c41675e989 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java
@@ -27,6 +27,7 @@ import android.window.IRemoteTransitionFinishedCallback;
import android.window.RemoteTransition;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
+import android.window.WindowAnimationState;
import android.window.WindowContainerTransaction;
import com.android.internal.protolog.common.ProtoLog;
@@ -65,30 +66,9 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Using registered One-shot remote"
+ " transition %s for (#%d).", mRemote, info.getDebugId());
- final IBinder.DeathRecipient remoteDied = () -> {
- Log.e(Transitions.TAG, "Remote transition died, finishing");
- mMainExecutor.execute(
- () -> finishCallback.onTransitionFinished(null /* wct */));
- };
- IRemoteTransitionFinishedCallback cb = new IRemoteTransitionFinishedCallback.Stub() {
- @Override
- public void onTransitionFinished(WindowContainerTransaction wct,
- SurfaceControl.Transaction sct) {
- ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
- "Finished one-shot remote transition %s for (#%d).", mRemote,
- info.getDebugId());
- if (mRemote.asBinder() != null) {
- mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */);
- }
- if (sct != null) {
- finishTransaction.merge(sct);
- }
- mMainExecutor.execute(() -> {
- finishCallback.onTransitionFinished(wct);
- mRemote = null;
- });
- }
- };
+ final IBinder.DeathRecipient remoteDied = createDeathRecipient(finishCallback);
+ IRemoteTransitionFinishedCallback cb =
+ createFinishedCallback(info, finishTransaction, finishCallback, remoteDied);
Transitions.setRunningRemoteTransitionDelegate(mRemote.getAppThread());
try {
if (mRemote.asBinder() != null) {
@@ -152,6 +132,51 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler {
}
@Override
+ public boolean takeOverAnimation(
+ @NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction transaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback,
+ @NonNull WindowAnimationState[] states) {
+ if (mTransition != transition) return false;
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "Using registered One-shot "
+ + "remote transition %s to take over (#%d).", mRemote, info.getDebugId());
+
+ final IBinder.DeathRecipient remoteDied = createDeathRecipient(finishCallback);
+ IRemoteTransitionFinishedCallback cb = createFinishedCallback(
+ info, null /* finishTransaction */, finishCallback, remoteDied);
+
+ Transitions.setRunningRemoteTransitionDelegate(mRemote.getAppThread());
+
+ try {
+ if (mRemote.asBinder() != null) {
+ mRemote.asBinder().linkToDeath(remoteDied, 0 /* flags */);
+ }
+
+ // If the remote is actually in the same process, then make a copy of parameters since
+ // remote impls assume that they have to clean-up native references.
+ final SurfaceControl.Transaction remoteStartT =
+ RemoteTransitionHandler.copyIfLocal(transaction, mRemote.getRemoteTransition());
+ final TransitionInfo remoteInfo =
+ remoteStartT == transaction ? info : info.localRemoteCopy();
+ mRemote.getRemoteTransition().takeOverAnimation(
+ transition, remoteInfo, remoteStartT, cb, states);
+
+ // Assume that remote will apply the transaction.
+ transaction.clear();
+ return true;
+ } catch (RemoteException e) {
+ Log.e(Transitions.TAG, "Error running remote transition takeover.", e);
+ if (mRemote.asBinder() != null) {
+ mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */);
+ }
+ finishCallback.onTransitionFinished(null /* wct */);
+ mRemote = null;
+ }
+
+ return false;
+ }
+
+ @Override
@Nullable
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@Nullable TransitionRequestInfo request) {
@@ -174,6 +199,41 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler {
}
}
+ private IBinder.DeathRecipient createDeathRecipient(
+ Transitions.TransitionFinishCallback finishCallback) {
+ return () -> {
+ Log.e(Transitions.TAG, "Remote transition died, finishing");
+ mMainExecutor.execute(
+ () -> finishCallback.onTransitionFinished(null /* wct */));
+ };
+ }
+
+ private IRemoteTransitionFinishedCallback createFinishedCallback(
+ @NonNull TransitionInfo info,
+ @Nullable SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback,
+ @NonNull IBinder.DeathRecipient remoteDied) {
+ return new IRemoteTransitionFinishedCallback.Stub() {
+ @Override
+ public void onTransitionFinished(WindowContainerTransaction wct,
+ SurfaceControl.Transaction sct) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+ "Finished one-shot remote transition %s for (#%d).", mRemote,
+ info.getDebugId());
+ if (mRemote.asBinder() != null) {
+ mRemote.asBinder().unlinkToDeath(remoteDied, 0 /* flags */);
+ }
+ if (finishTransaction != null && sct != null) {
+ finishTransaction.merge(sct);
+ }
+ mMainExecutor.execute(() -> {
+ finishCallback.onTransitionFinished(wct);
+ mRemote = null;
+ });
+ }
+ };
+ }
+
@Override
public String toString() {
return "OneShotRemoteHandler:" + mRemote.getDebugName() + ":"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
index 4ea71490798c..9fc6702562bb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
@@ -43,7 +43,7 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition {
private final DesktopTasksController mDesktopTasksController;
RecentsMixedTransition(int type, IBinder transition, Transitions player,
- DefaultMixedHandler mixedHandler, PipTransitionController pipHandler,
+ MixedTransitionHandler mixedHandler, PipTransitionController pipHandler,
StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler,
RecentsTransitionHandler recentsHandler,
DesktopTasksController desktopTasksController) {
@@ -142,7 +142,8 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition {
&& mSplitHandler.getSplitItemPosition(change.getLastParent())
!= SPLIT_POSITION_UNDEFINED) {
return animateEnterPipFromSplit(this, info, startTransaction, finishTransaction,
- finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
+ finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler,
+ /*replacingPip*/ false);
}
}
@@ -212,6 +213,7 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition {
switch (mType) {
case TYPE_RECENTS_DURING_DESKTOP:
case TYPE_RECENTS_DURING_SPLIT:
+ case TYPE_RECENTS_DURING_KEYGUARD:
mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
break;
default:
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
index 4c4c5806ea55..d6860464d055 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java
@@ -16,6 +16,8 @@
package com.android.wm.shell.transition;
+import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.IBinder;
@@ -32,6 +34,7 @@ import android.window.RemoteTransition;
import android.window.TransitionFilter;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
+import android.window.WindowAnimationState;
import android.window.WindowContainerTransaction;
import androidx.annotation.BinderThread;
@@ -41,7 +44,9 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.shared.TransitionUtil;
+import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Arrays;
/**
* Handler that deals with RemoteTransitions. It will only request to handle a transition
@@ -58,6 +63,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
/** Ordered by specificity. Last filters will be checked first */
private final ArrayList<Pair<TransitionFilter, RemoteTransition>> mFilters =
new ArrayList<>();
+ private final ArrayList<Pair<TransitionFilter, RemoteTransition>> mTakeoverFilters =
+ new ArrayList<>();
private final ArrayMap<IBinder, RemoteDeathHandler> mDeathHandlers = new ArrayMap<>();
@@ -70,14 +77,23 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
mFilters.add(new Pair<>(filter, remote));
}
+ void addFilteredForTakeover(TransitionFilter filter, RemoteTransition remote) {
+ handleDeath(remote.asBinder(), null /* finishCallback */);
+ mTakeoverFilters.add(new Pair<>(filter, remote));
+ }
+
void removeFiltered(RemoteTransition remote) {
boolean removed = false;
- for (int i = mFilters.size() - 1; i >= 0; --i) {
- if (mFilters.get(i).second.asBinder().equals(remote.asBinder())) {
- mFilters.remove(i);
- removed = true;
+ for (ArrayList<Pair<TransitionFilter, RemoteTransition>> filters
+ : Arrays.asList(mFilters, mTakeoverFilters)) {
+ for (int i = filters.size() - 1; i >= 0; --i) {
+ if (filters.get(i).second.asBinder().equals(remote.asBinder())) {
+ filters.remove(i);
+ removed = true;
+ }
}
}
+
if (removed) {
unhandleDeath(remote.asBinder(), null /* finishCallback */);
}
@@ -237,6 +253,47 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
}
}
+ @Nullable
+ @Override
+ public Transitions.TransitionHandler getHandlerForTakeover(
+ @NonNull IBinder transition, @NonNull TransitionInfo info) {
+ if (!returnAnimationFrameworkLibrary()) {
+ return null;
+ }
+
+ for (Pair<TransitionFilter, RemoteTransition> registered : mTakeoverFilters) {
+ if (registered.first.matches(info)) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "Found matching remote to takeover (#%d)", info.getDebugId());
+
+ OneShotRemoteHandler oneShot =
+ new OneShotRemoteHandler(mMainExecutor, registered.second);
+ oneShot.setTransition(transition);
+ return oneShot;
+ }
+ }
+
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "No matching remote found to takeover (#%d)", info.getDebugId());
+ return null;
+ }
+
+ @Override
+ public boolean takeOverAnimation(
+ @NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction transaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback,
+ @NonNull WindowAnimationState[] states) {
+ Transitions.TransitionHandler handler = getHandlerForTakeover(transition, info);
+ if (handler == null) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "Take over request failed: no matching remote for (#%d)", info.getDebugId());
+ return false;
+ }
+ ((OneShotRemoteHandler) handler).setTransition(transition);
+ return handler.takeOverAnimation(transition, info, transaction, finishCallback, states);
+ }
+
@Override
@Nullable
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@@ -284,6 +341,34 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler {
}
}
+ void dump(@NonNull PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+
+ pw.println(prefix + "Registered Remotes:");
+ if (mFilters.isEmpty()) {
+ pw.println(innerPrefix + "none");
+ } else {
+ for (Pair<TransitionFilter, RemoteTransition> entry : mFilters) {
+ dumpRemote(pw, innerPrefix, entry.second);
+ }
+ }
+
+ pw.println(prefix + "Registered Takeover Remotes:");
+ if (mTakeoverFilters.isEmpty()) {
+ pw.println(innerPrefix + "none");
+ } else {
+ for (Pair<TransitionFilter, RemoteTransition> entry : mTakeoverFilters) {
+ dumpRemote(pw, innerPrefix, entry.second);
+ }
+ }
+ }
+
+ private void dumpRemote(@NonNull PrintWriter pw, String prefix, RemoteTransition remote) {
+ pw.print(prefix);
+ pw.print(remote.getDebugName());
+ pw.println(" (" + Integer.toHexString(System.identityHashCode(remote)) + ")");
+ }
+
/** NOTE: binder deaths can alter the filter order */
private class RemoteDeathHandler implements IBinder.DeathRecipient {
private final IBinder mRemote;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index 9ce22094d56b..e196254628d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -167,7 +167,7 @@ class ScreenRotationAnimation {
t.show(mScreenshotLayer);
if (!isCustomRotate()) {
mStartLuma = TransitionAnimation.getBorderLuma(hardwareBuffer,
- screenshotBuffer.getColorSpace());
+ screenshotBuffer.getColorSpace(), mSurfaceControl);
}
hardwareBuffer.close();
}
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 1be85d05c16e..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();
@@ -280,6 +286,7 @@ public class TransitionAnimationHelper {
.setParent(rootLeash)
.setColorLayer()
.setOpaque(true)
+ .setCallsite("TransitionAnimationHelper.addBackgroundToTransition")
.build();
startTransaction
.setLayer(animationBackgroundSurface, Integer.MIN_VALUE)
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 888105d4a20a..f257e207673d 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
@@ -31,11 +31,10 @@ import static android.view.WindowManager.fixScale;
import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;
-import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static android.window.TransitionInfo.FLAG_NO_ANIMATION;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
-import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
+import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary;
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;
@@ -43,25 +42,30 @@ import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SH
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityTaskManager;
+import android.app.AppGlobals;
import android.app.IApplicationThread;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.os.Handler;
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;
import android.view.WindowManager;
import android.window.ITransitionPlayer;
import android.window.RemoteTransition;
+import android.window.TaskFragmentOrganizer;
import android.window.TransitionFilter;
import android.window.TransitionInfo;
import android.window.TransitionMetrics;
import android.window.TransitionRequestInfo;
+import android.window.WindowAnimationState;
import android.window.WindowContainerTransaction;
import androidx.annotation.BinderThread;
@@ -76,10 +80,13 @@ import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.IHomeTransitionListener;
+import com.android.wm.shell.shared.IShellTransitions;
+import com.android.wm.shell.shared.ShellTransitions;
import com.android.wm.shell.shared.TransitionUtil;
+import com.android.wm.shell.shared.annotations.ExternalThread;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -121,8 +128,7 @@ public class Transitions implements RemoteCallable<Transitions>,
static final String TAG = "ShellTransitions";
/** Set to {@code true} to enable shell transitions. */
- public static final boolean ENABLE_SHELL_TRANSITIONS =
- SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
+ public static final boolean ENABLE_SHELL_TRANSITIONS = getShellTransitEnabled();
public static final boolean SHELL_TRANSITIONS_ROTATION = ENABLE_SHELL_TRANSITIONS
&& SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false);
@@ -160,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;
@@ -171,12 +174,20 @@ 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;
+ /**
+ * The task fragment drag resize transition used by activity embedding.
+ */
+ public static final int TRANSIT_TASK_FRAGMENT_DRAG_RESIZE =
+ // 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;
@@ -216,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;
@@ -226,6 +238,10 @@ public class Transitions implements RemoteCallable<Transitions>,
/** Ordered list of transitions which have been merged into this one. */
private ArrayList<ActiveTransition> mMerged;
+ ActiveTransition(IBinder token) {
+ mToken = token;
+ }
+
boolean isSync() {
return (mInfo.getFlags() & TransitionInfo.FLAG_SYNC) != 0;
}
@@ -255,6 +271,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<>();
@@ -416,12 +435,24 @@ public class Transitions implements RemoteCallable<Transitions>,
mHandlers.set(0, handler);
}
- /** Register a remote transition to be used when `filter` matches an incoming transition */
+ /**
+ * Register a remote transition to be used for all operations except takeovers when `filter`
+ * matches an incoming transition.
+ */
public void registerRemote(@NonNull TransitionFilter filter,
@NonNull RemoteTransition remoteTransition) {
mRemoteTransitionHandler.addFiltered(filter, remoteTransition);
}
+ /**
+ * Register a remote transition to be used for all operations except takeovers when `filter`
+ * matches an incoming transition.
+ */
+ public void registerRemoteForTakeover(@NonNull TransitionFilter filter,
+ @NonNull RemoteTransition remoteTransition) {
+ mRemoteTransitionHandler.addFilteredForTakeover(filter, remoteTransition);
+ }
+
/** Unregisters a remote transition and all associated filters */
public void unregisterRemote(@NonNull RemoteTransition remoteTransition) {
mRemoteTransitionHandler.removeFiltered(remoteTransition);
@@ -495,6 +526,7 @@ public class Transitions implements RemoteCallable<Transitions>,
if (mode == TRANSIT_TO_FRONT) {
// When the window is moved to front, make sure the crop is updated to prevent it
// from using the old crop.
+ t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y);
t.setWindowCrop(leash, change.getEndAbsBounds().width(),
change.getEndAbsBounds().height());
}
@@ -506,6 +538,8 @@ public class Transitions implements RemoteCallable<Transitions>,
t.setMatrix(leash, 1, 0, 0, 1);
t.setAlpha(leash, 1.f);
t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y);
+ t.setWindowCrop(leash, change.getEndAbsBounds().width(),
+ change.getEndAbsBounds().height());
}
continue;
}
@@ -538,15 +572,15 @@ public class Transitions implements RemoteCallable<Transitions>,
final int mode = change.getMode();
// Put all the OPEN/SHOW on top
if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
- if (isOpening
- // This is for when an activity launches while a different transition is
- // collecting.
- || change.hasFlags(FLAG_MOVED_TO_TOP)) {
+ if (isOpening) {
// put on top
return zSplitLine + numChanges - i;
- } else {
+ } else if (isClosing) {
// put on bottom
return zSplitLine - i;
+ } else {
+ // maintain relative ordering (put all changes in the animating layer)
+ return zSplitLine + numChanges - i;
}
} else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
if (isOpening) {
@@ -629,8 +663,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;
}
}
@@ -662,7 +698,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.
@@ -677,8 +713,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;
}
@@ -718,7 +754,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;
@@ -836,6 +872,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();
@@ -856,7 +893,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);
@@ -915,8 +952,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);
@@ -925,8 +964,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);
@@ -934,8 +973,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);
}
/**
@@ -1011,10 +1050,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());
@@ -1067,54 +1111,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.
@@ -1154,7 +1169,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
@@ -1173,14 +1187,38 @@ 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;
}
/**
+ * Checks whether a handler exists capable of taking over the given transition, and returns it.
+ * Otherwise it returns null.
+ */
+ @Nullable
+ public TransitionHandler getHandlerForTakeover(
+ @NonNull IBinder transition, @NonNull TransitionInfo info) {
+ if (!returnAnimationFrameworkLibrary()) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
+ "Trying to get a handler for takeover but the flag is disabled");
+ return null;
+ }
+
+ for (TransitionHandler handler : mHandlers) {
+ TransitionHandler candidate = handler.getHandlerForTakeover(transition, info);
+ if (candidate != null) {
+ return candidate;
+ }
+ }
+
+ return null;
+ }
+
+ /**
* Finish running animations (almost) immediately when a SLEEP transition comes in. We use this
* as both a way to reduce unnecessary work (animations not visible while screen off) and as a
* failsafe to unblock "stuck" animations (in particular remote animations).
@@ -1192,14 +1230,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;
}
@@ -1219,7 +1257,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()) {
@@ -1322,6 +1360,49 @@ public class Transitions implements RemoteCallable<Transitions>,
@NonNull TransitionFinishCallback finishCallback) { }
/**
+ * Checks whether this handler is capable of taking over a transition matching `info`.
+ * {@link TransitionHandler#takeOverAnimation(IBinder, TransitionInfo,
+ * SurfaceControl.Transaction, TransitionFinishCallback, WindowAnimationState[])} is
+ * guaranteed to succeed if called on the handler returned by this method.
+ *
+ * Note that the handler returned by this method can either be itself, or a different one
+ * selected by this handler to take care of the transition on its behalf.
+ *
+ * @param transition The transition that should be taken over.
+ * @param info Information about the transition to be taken over.
+ * @return A handler capable of taking over a matching transition, or null.
+ */
+ @Nullable
+ default TransitionHandler getHandlerForTakeover(
+ @NonNull IBinder transition, @NonNull TransitionInfo info) {
+ return null;
+ }
+
+ /**
+ * Attempt to take over a running transition. This must succeed if this handler was returned
+ * by {@link TransitionHandler#getHandlerForTakeover(IBinder, TransitionInfo)}.
+ *
+ * @param transition The transition that should be taken over.
+ * @param info Information about the what is changing in the transition.
+ * @param transaction Contains surface changes that resulted from the transition. Any
+ * additional changes should be added to this transaction and committed
+ * inside this method.
+ * @param finishCallback Call this at the end of the animation, if the take-over succeeds.
+ * Note that this will be called instead of the callback originally
+ * passed to startAnimation(), so the caller should make sure all
+ * necessary cleanup happens here. This MUST be called on main thread.
+ * @param states The animation states of the transition's window at the time this method was
+ * called.
+ * @return true if the transition was taken over, false if not.
+ */
+ default boolean takeOverAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction transaction,
+ @NonNull TransitionFinishCallback finishCallback,
+ @NonNull WindowAnimationState[] states) {
+ return false;
+ }
+
+ /**
* Potentially handles a startTransition request.
*
* @param transition The transition whose start is being requested.
@@ -1405,6 +1486,8 @@ public class Transitions implements RemoteCallable<Transitions>,
public void onTransitionReady(IBinder iBinder, TransitionInfo transitionInfo,
SurfaceControl.Transaction t, SurfaceControl.Transaction finishT)
throws RemoteException {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady(transaction=%d)",
+ t.getId());
mMainExecutor.execute(() -> Transitions.this.onTransitionReady(
iBinder, transitionInfo, t, finishT));
}
@@ -1424,16 +1507,21 @@ public class Transitions implements RemoteCallable<Transitions>,
@Override
public void registerRemote(@NonNull TransitionFilter filter,
@NonNull RemoteTransition remoteTransition) {
- mMainExecutor.execute(() -> {
- mRemoteTransitionHandler.addFiltered(filter, remoteTransition);
- });
+ mMainExecutor.execute(
+ () -> mRemoteTransitionHandler.addFiltered(filter, remoteTransition));
+ }
+
+ @Override
+ public void registerRemoteForTakeover(@NonNull TransitionFilter filter,
+ @NonNull RemoteTransition remoteTransition) {
+ mMainExecutor.execute(() -> mRemoteTransitionHandler.addFilteredForTakeover(
+ filter, remoteTransition));
}
@Override
public void unregisterRemote(@NonNull RemoteTransition remoteTransition) {
- mMainExecutor.execute(() -> {
- mRemoteTransitionHandler.removeFiltered(remoteTransition);
- });
+ mMainExecutor.execute(
+ () -> mRemoteTransitionHandler.removeFiltered(remoteTransition));
}
}
@@ -1462,17 +1550,23 @@ public class Transitions implements RemoteCallable<Transitions>,
public void registerRemote(@NonNull TransitionFilter filter,
@NonNull RemoteTransition remoteTransition) {
executeRemoteCallWithTaskPermission(mTransitions, "registerRemote",
- (transitions) -> {
- transitions.mRemoteTransitionHandler.addFiltered(filter, remoteTransition);
- });
+ (transitions) -> transitions.mRemoteTransitionHandler.addFiltered(
+ filter, remoteTransition));
+ }
+
+ @Override
+ public void registerRemoteForTakeover(@NonNull TransitionFilter filter,
+ @NonNull RemoteTransition remoteTransition) {
+ executeRemoteCallWithTaskPermission(mTransitions, "registerRemoteForTakeover",
+ (transitions) -> transitions.mRemoteTransitionHandler.addFilteredForTakeover(
+ filter, remoteTransition));
}
@Override
public void unregisterRemote(@NonNull RemoteTransition remoteTransition) {
executeRemoteCallWithTaskPermission(mTransitions, "unregisterRemote",
- (transitions) -> {
- transitions.mRemoteTransitionHandler.removeFiltered(remoteTransition);
- });
+ (transitions) ->
+ transitions.mRemoteTransitionHandler.removeFiltered(remoteTransition));
}
@Override
@@ -1557,6 +1651,8 @@ public class Transitions implements RemoteCallable<Transitions>,
pw.println(" (" + Integer.toHexString(System.identityHashCode(handler)) + ")");
}
+ mRemoteTransitionHandler.dump(pw, prefix);
+
pw.println(prefix + "Observers:");
for (TransitionObserver observer : mObservers) {
pw.print(innerPrefix);
@@ -1609,4 +1705,16 @@ public class Transitions implements RemoteCallable<Transitions>,
}
}
}
+
+ private static boolean getShellTransitEnabled() {
+ try {
+ if (AppGlobals.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_AUTOMOTIVE, 0)) {
+ return SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
+ }
+ } catch (RemoteException re) {
+ Log.w(TAG, "Error getting system features");
+ }
+ return true;
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java
index ed4ae05cb2c9..456658c54fd0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java
@@ -16,6 +16,8 @@
package com.android.wm.shell.transition.tracing;
+import static android.tracing.perfetto.DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP;
+
import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellHandlerMapping;
import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellHandlerMappings;
import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellTransition;
@@ -47,7 +49,12 @@ public class PerfettoTransitionTracer implements TransitionTracer {
public PerfettoTransitionTracer() {
Producer.init(InitArguments.DEFAULTS);
- mDataSource.register(DataSourceParams.DEFAULTS);
+ DataSourceParams params =
+ new DataSourceParams.Builder()
+ .setBufferExhaustedPolicy(
+ PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP)
+ .build();
+ mDataSource.register(params);
}
/**
@@ -211,8 +218,6 @@ public class PerfettoTransitionTracer implements TransitionTracer {
}
os.end(mappingsToken);
-
- ctx.flush();
});
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
index c26604a84a61..7c2ba455c0c9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
@@ -293,7 +293,13 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene
@Override
public void onFoldStateChanged(boolean isFolded) {
if (isFolded) {
+ // Reset unfold animation finished flag on folding, so it could be used next time
+ // when we unfold the device as an indication that animation hasn't finished yet
mAnimationFinished = false;
+
+ // If we are currently animating unfold animation we should finish it because
+ // the animation might not start and finish as the device was folded
+ finishTransitionIfNeeded();
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt
index 7a50814f0275..564e716c7378 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/util/KtProtoLog.kt
@@ -31,42 +31,42 @@ class KtProtoLog {
companion object {
/** @see [com.android.internal.protolog.common.ProtoLog.d] */
fun d(group: IProtoLogGroup, messageString: String, vararg args: Any) {
- if (ProtoLog.isEnabled(group)) {
+ if (group.isLogToLogcat) {
Log.d(group.tag, String.format(messageString, *args))
}
}
/** @see [com.android.internal.protolog.common.ProtoLog.v] */
fun v(group: IProtoLogGroup, messageString: String, vararg args: Any) {
- if (ProtoLog.isEnabled(group)) {
+ if (group.isLogToLogcat) {
Log.v(group.tag, String.format(messageString, *args))
}
}
/** @see [com.android.internal.protolog.common.ProtoLog.i] */
fun i(group: IProtoLogGroup, messageString: String, vararg args: Any) {
- if (ProtoLog.isEnabled(group)) {
+ if (group.isLogToLogcat) {
Log.i(group.tag, String.format(messageString, *args))
}
}
/** @see [com.android.internal.protolog.common.ProtoLog.w] */
fun w(group: IProtoLogGroup, messageString: String, vararg args: Any) {
- if (ProtoLog.isEnabled(group)) {
+ if (group.isLogToLogcat) {
Log.w(group.tag, String.format(messageString, *args))
}
}
/** @see [com.android.internal.protolog.common.ProtoLog.e] */
fun e(group: IProtoLogGroup, messageString: String, vararg args: Any) {
- if (ProtoLog.isEnabled(group)) {
+ if (group.isLogToLogcat) {
Log.e(group.tag, String.format(messageString, *args))
}
}
/** @see [com.android.internal.protolog.common.ProtoLog.wtf] */
fun wtf(group: IProtoLogGroup, messageString: String, vararg args: Any) {
- if (ProtoLog.isEnabled(group)) {
+ if (group.isLogToLogcat) {
Log.wtf(group.tag, String.format(messageString, *args))
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
index b2eeea7048bc..95e0d79c212e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java
@@ -19,21 +19,30 @@ package com.android.wm.shell.windowdecor;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.content.pm.PackageManager.FEATURE_PC;
+import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS;
+import static android.view.WindowManager.TRANSIT_CHANGE;
import android.app.ActivityManager.RunningTaskInfo;
+import android.content.ContentResolver;
import android.content.Context;
+import android.graphics.Rect;
import android.os.Handler;
+import android.provider.Settings;
import android.util.SparseArray;
import android.view.Choreographer;
+import android.view.Display;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.View;
+import android.window.DisplayAreaInfo;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import androidx.annotation.Nullable;
import com.android.wm.shell.R;
+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.SyncTransactionQueue;
@@ -51,6 +60,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
private final Handler mMainHandler;
private final Choreographer mMainChoreographer;
private final DisplayController mDisplayController;
+ private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
private final SyncTransactionQueue mSyncQueue;
private final Transitions mTransitions;
private TaskOperations mTaskOperations;
@@ -63,6 +73,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
Choreographer mainChoreographer,
ShellTaskOrganizer taskOrganizer,
DisplayController displayController,
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
SyncTransactionQueue syncQueue,
Transitions transitions) {
mContext = context;
@@ -70,6 +81,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
mMainChoreographer = mainChoreographer;
mTaskOrganizer = taskOrganizer;
mDisplayController = displayController;
+ mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
mSyncQueue = syncQueue;
mTransitions = transitions;
if (!Transitions.ENABLE_SHELL_TRANSITIONS) {
@@ -107,6 +119,21 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
}
@Override
+ public void onTaskVanished(RunningTaskInfo taskInfo) {
+ // A task vanishing doesn't necessarily mean the task was closed, it could also mean its
+ // windowing mode changed. We're only interested in closing tasks so checking whether
+ // its info still exists in the task organizer is one way to disambiguate.
+ final boolean closed = mTaskOrganizer.getRunningTaskInfo(taskInfo.taskId) == null;
+ if (closed) {
+ // Destroying the window decoration is usually handled when a TRANSIT_CLOSE transition
+ // changes happen, but there are certain cases in which closing tasks aren't included
+ // in transitions, such as when a non-visible task is closed. See b/296921167.
+ // Destroy the decoration here in case the lack of transition missed it.
+ destroyWindowDecoration(taskInfo);
+ }
+ }
+
+ @Override
public void onTaskChanging(
RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -156,10 +183,33 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
}
private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
- return taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
- || (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
- && taskInfo.configuration.windowConfiguration.getDisplayWindowingMode()
- == WINDOWING_MODE_FREEFORM);
+ if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
+ return true;
+ }
+ if (taskInfo.getActivityType() != ACTIVITY_TYPE_STANDARD) {
+ return false;
+ }
+ final DisplayAreaInfo rootDisplayAreaInfo =
+ mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId);
+ if (rootDisplayAreaInfo != null) {
+ return rootDisplayAreaInfo.configuration.windowConfiguration.getWindowingMode()
+ == WINDOWING_MODE_FREEFORM;
+ }
+
+ // It is possible that the rootDisplayAreaInfo is null when a task appears soon enough after
+ // a new display shows up, because TDA may appear after task appears in WM shell. Instead of
+ // fixing the synchronization issues, let's use other signals to "guess" the answer. It is
+ // OK in this context because no other captions other than the legacy developer option
+ // freeform and Kingyo/CF PC may use this class. WM shell should have full control over the
+ // condition where captions should show up in all new cases such as desktop mode, for which
+ // we should use different window decor view models. Ultimately Kingyo/CF PC may need to
+ // spin up their own window decor view model when they start to care about multiple
+ // displays.
+ if (isPc()) {
+ return true;
+ }
+ return taskInfo.displayId != Display.DEFAULT_DISPLAY
+ && forcesDesktopModeOnExternalDisplays();
}
private void createWindowDecoration(
@@ -186,7 +236,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
final FluidResizeTaskPositioner taskPositioner =
new FluidResizeTaskPositioner(mTaskOrganizer, mTransitions, windowDecoration,
- mDisplayController, 0 /* disallowedAreaForEndBoundsHeight */);
+ mDisplayController);
final CaptionTouchEventListener touchEventListener =
new CaptionTouchEventListener(taskInfo, taskPositioner);
windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
@@ -205,6 +255,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
private final WindowContainerToken mTaskToken;
private final DragPositioningCallback mDragPositioningCallback;
private final DragDetector mDragDetector;
+ private final int mDisplayId;
private int mDragPointerId = -1;
private boolean mIsDragging;
@@ -216,6 +267,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
mTaskToken = taskInfo.token;
mDragPositioningCallback = dragPositioningCallback;
mDragDetector = new DragDetector(this);
+ mDisplayId = taskInfo.displayId;
}
@Override
@@ -224,12 +276,15 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
if (id == R.id.close_window) {
mTaskOperations.closeTask(mTaskToken);
} else if (id == R.id.back_button) {
- mTaskOperations.injectBackKey();
+ mTaskOperations.injectBackKey(mDisplayId);
} else if (id == R.id.minimize_window) {
mTaskOperations.minimizeTask(mTaskToken);
} else if (id == R.id.maximize_window) {
RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
- mTaskOperations.maximizeTask(taskInfo);
+ final DisplayAreaInfo rootDisplayAreaInfo =
+ mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId);
+ mTaskOperations.maximizeTask(taskInfo,
+ rootDisplayAreaInfo.configuration.windowConfiguration.getWindowingMode());
}
}
@@ -286,8 +341,15 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
mDragPointerId = e.getPointerId(0);
}
final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
- mDragPositioningCallback.onDragPositioningEnd(
+ final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd(
e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
+ DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(newTaskBounds,
+ mWindowDecorByTaskId.get(mTaskId).calculateValidDragArea());
+ if (newTaskBounds != taskInfo.configuration.windowConfiguration.getBounds()) {
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setBounds(taskInfo.token, newTaskBounds);
+ mTransitions.startTransition(TRANSIT_CHANGE, wct, null);
+ }
final boolean wasDragging = mIsDragging;
mIsDragging = false;
return wasDragging;
@@ -296,4 +358,17 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
return true;
}
}
+
+ /**
+ * Returns if this device is a PC.
+ */
+ private boolean isPc() {
+ return mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
+ }
+
+ private boolean forcesDesktopModeOnExternalDisplays() {
+ final ContentResolver resolver = mContext.getContentResolver();
+ return Settings.Global.getInt(resolver,
+ DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0) != 0;
+ }
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 91e9601c6a27..d0ca5b0fdce6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -16,16 +16,23 @@
package com.android.wm.shell.windowdecor;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize;
+
+import android.annotation.NonNull;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.WindowConfiguration;
import android.app.WindowConfiguration.WindowingMode;
import android.content.Context;
import android.content.res.ColorStateList;
+import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.VectorDrawable;
import android.os.Handler;
+import android.util.Size;
import android.view.Choreographer;
import android.view.SurfaceControl;
import android.view.View;
@@ -67,9 +74,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
Handler handler,
Choreographer choreographer,
SyncTransactionQueue syncQueue) {
- super(context, displayController, taskOrganizer, taskInfo, taskSurface,
- taskInfo.getConfiguration());
-
+ super(context, displayController, taskOrganizer, taskInfo, taskSurface);
mHandler = handler;
mChoreographer = choreographer;
mSyncQueue = syncQueue;
@@ -87,13 +92,16 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
}
@Override
+ @NonNull
Rect calculateValidDragArea() {
+ final Context displayContext = mDisplayController.getDisplayContext(mTaskInfo.displayId);
+ if (displayContext == null) return new Rect();
final int leftButtonsWidth = loadDimensionPixelSize(mContext.getResources(),
R.dimen.caption_left_buttons_width);
// On a smaller screen, don't require as much empty space on screen, as offscreen
// drags will be restricted too much.
- final int requiredEmptySpaceId = mDisplayController.getDisplayContext(mTaskInfo.displayId)
+ final int requiredEmptySpaceId = displayContext
.getResources().getConfiguration().smallestScreenWidthDp >= 600
? R.dimen.freeform_required_visible_empty_space_in_header :
R.dimen.small_screen_required_visible_empty_space_in_header;
@@ -190,7 +198,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
mRelayoutParams.mShadowRadiusId = shadowRadiusID;
mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
mRelayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition;
- mRelayoutParams.mAllowCaptionInputFallthrough = false;
relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
// After this line, mTaskInfo is up-to-date and should be used instead of taskInfo
@@ -218,7 +225,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
mHandler,
mChoreographer,
mDisplay.getDisplayId(),
- 0 /* taskCornerRadius */,
mDecorationContainerSurface,
mDragPositioningCallback,
mSurfaceControlBuilderSupplier,
@@ -230,12 +236,10 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
.getScaledTouchSlop();
mDragDetector.setTouchSlop(touchSlop);
- final int resize_handle = mResult.mRootView.getResources()
- .getDimensionPixelSize(R.dimen.freeform_resize_handle);
- final int resize_corner = mResult.mRootView.getResources()
- .getDimensionPixelSize(R.dimen.freeform_resize_corner);
- mDragResizeListener.setGeometry(
- mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop);
+ final Resources res = mResult.mRootView.getResources();
+ mDragResizeListener.setGeometry(new DragResizeWindowGeometry(0 /* taskCornerRadius */,
+ new Size(mResult.mWidth, mResult.mHeight), getResizeEdgeHandleSize(res),
+ getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop);
}
/**
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 fa3d8a61fd04..21b6db29143a 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
@@ -16,6 +16,7 @@
package com.android.wm.shell.windowdecor;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
@@ -25,17 +26,18 @@ import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_HOVER_ENTER;
import static android.view.MotionEvent.ACTION_HOVER_EXIT;
+import static android.view.MotionEvent.ACTION_HOVER_MOVE;
+import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.WindowInsets.Type.statusBars;
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 static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FREEFORM_ANIMATION_DURATION;
-import static com.android.wm.shell.windowdecor.MoveToDesktopAnimator.DRAG_FREEFORM_SCALE;
+import static com.android.wm.shell.compatui.AppCompatUtils.isSingleTopActivityTranslucent;
+import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
@@ -71,6 +73,8 @@ import android.window.WindowContainerTransaction;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.window.flags.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -79,11 +83,14 @@ import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
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.DesktopModeStatus;
+import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition;
+import com.android.wm.shell.desktopmode.DesktopWallpaperActivity;
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter;
+import com.android.wm.shell.shared.DesktopModeStatus;
import com.android.wm.shell.splitscreen.SplitScreen;
import com.android.wm.shell.splitscreen.SplitScreen.StageType;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -96,6 +103,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;
@@ -119,9 +127,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
private final Choreographer mMainChoreographer;
private final DisplayController mDisplayController;
private final SyncTransactionQueue mSyncQueue;
- private final Optional<DesktopTasksController> mDesktopTasksController;
+ private final DesktopTasksController mDesktopTasksController;
private final InputManager mInputManager;
-
private boolean mTransitionDragActive;
private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>();
@@ -129,8 +136,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
private final ExclusionRegionListener mExclusionRegionListener =
new ExclusionRegionListenerImpl();
- private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId =
- new SparseArray<>();
+ private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId;
private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl();
private final InputMonitorFactory mInputMonitorFactory;
private TaskOperations mTaskOperations;
@@ -147,6 +153,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() {
@@ -197,7 +204,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
new DesktopModeWindowDecoration.Factory(),
new InputMonitorFactory(),
SurfaceControl.Transaction::new,
- rootTaskDisplayAreaOrganizer);
+ rootTaskDisplayAreaOrganizer,
+ new SparseArray<>());
}
@VisibleForTesting
@@ -219,7 +227,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory,
InputMonitorFactory inputMonitorFactory,
Supplier<SurfaceControl.Transaction> transactionFactory,
- RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
+ RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
+ SparseArray<DesktopModeWindowDecoration> windowDecorByTaskId) {
mContext = context;
mMainExecutor = shellExecutor;
mMainHandler = mainHandler;
@@ -231,7 +240,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mDisplayInsetsController = displayInsetsController;
mSyncQueue = syncQueue;
mTransitions = transitions;
- mDesktopTasksController = desktopTasksController;
+ mDesktopTasksController = desktopTasksController.get();
mShellCommandHandler = shellCommandHandler;
mWindowManager = windowManager;
mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory;
@@ -239,6 +248,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mTransactionFactory = transactionFactory;
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);
}
@@ -248,8 +260,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mShellCommandHandler.addDumpCallback(this::dump, this);
mDisplayInsetsController.addInsetsChangedListener(mContext.getDisplayId(),
new DesktopModeOnInsetsChangedListener());
- mDesktopTasksController.ifPresent(c -> c.setOnTaskResizeAnimationListener(
- new DeskopModeOnTaskResizeAnimationListener()));
+ mDesktopTasksController.setOnTaskResizeAnimationListener(
+ new DeskopModeOnTaskResizeAnimationListener());
try {
mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener,
mContext.getDisplayId());
@@ -269,11 +281,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mSplitScreenController.registerSplitScreenListener(new SplitScreen.SplitScreenListener() {
@Override
public void onTaskStageChanged(int taskId, @StageType int stage, boolean visible) {
- if (visible) {
+ if (visible && stage != STAGE_TYPE_UNDEFINED) {
DesktopModeWindowDecoration decor = mWindowDecorByTaskId.get(taskId);
- if (decor != null && DesktopModeStatus.isEnabled()
- && decor.mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
- mDesktopTasksController.ifPresent(c -> c.moveToSplit(decor.mTaskInfo));
+ if (decor != null && DesktopModeStatus.canEnterDesktopMode(mContext)) {
+ mDesktopTasksController.moveToSplit(decor.mTaskInfo);
}
}
}
@@ -305,6 +316,22 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
@Override
+ public void onTaskVanished(RunningTaskInfo taskInfo) {
+ // A task vanishing doesn't necessarily mean the task was closed, it could also mean its
+ // windowing mode changed. We're only interested in closing tasks so checking whether
+ // its info still exists in the task organizer is one way to disambiguate.
+ final boolean closed = mTaskOrganizer.getRunningTaskInfo(taskInfo.taskId) == null;
+ ProtoLog.v(WM_SHELL_DESKTOP_MODE, "Task Vanished: #%d closed=%b", taskInfo.taskId, closed);
+ if (closed) {
+ // Destroying the window decoration is usually handled when a TRANSIT_CLOSE transition
+ // changes happen, but there are certain cases in which closing tasks aren't included
+ // in transitions, such as when a non-visible task is closed. See b/296921167.
+ // Destroy the decoration here in case the lack of transition missed it.
+ destroyWindowDecoration(taskInfo);
+ }
+ }
+
+ @Override
public void onTaskChanging(
RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
@@ -340,8 +367,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
@Override
public void destroyWindowDecoration(RunningTaskInfo taskInfo) {
- final DesktopModeWindowDecoration decoration =
- mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId);
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId);
if (decoration == null) return;
decoration.close();
@@ -349,11 +375,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
if (mEventReceiversByDisplay.contains(displayId)) {
removeTaskFromEventReceiver(displayId);
}
+ // Remove the decoration from the cache last because WindowDecoration#close could still
+ // issue CANCEL MotionEvents to touch listeners before its view host is released.
+ // See b/327664694.
+ mWindowDecorByTaskId.remove(taskInfo.taskId);
}
private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
- View.OnGenericMotionListener , DragDetector.MotionEventHandler {
+ View.OnGenericMotionListener, DragDetector.MotionEventHandler {
private static final int CLOSE_MAXIMIZE_MENU_DELAY_MS = 150;
private final int mTaskId;
@@ -361,6 +391,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
private final DragPositioningCallback mDragPositioningCallback;
private final DragDetector mDragDetector;
private final GestureDetector mGestureDetector;
+ private final int mDisplayId;
/**
* Whether to pilfer the next motion event to send cancellations to the windows below.
@@ -382,6 +413,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mDragPositioningCallback = dragPositioningCallback;
mDragDetector = new DragDetector(this);
mGestureDetector = new GestureDetector(mContext, this);
+ mDisplayId = taskInfo.displayId;
mCloseMaximizeWindowRunnable = () -> {
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
if (decoration == null) return;
@@ -402,25 +434,26 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mSplitScreenController.moveTaskToFullscreen(getOtherSplitTask(mTaskId).taskId,
SplitScreenController.EXIT_REASON_DESKTOP_MODE);
} else {
- mTaskOperations.closeTask(mTaskToken);
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ mDesktopTasksController.onDesktopWindowClose(wct, mTaskId);
+ mTaskOperations.closeTask(mTaskToken, wct);
}
} else if (id == R.id.back_button) {
- mTaskOperations.injectBackKey();
+ mTaskOperations.injectBackKey(mDisplayId);
} 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();
}
} else if (id == R.id.desktop_button) {
- if (mDesktopTasksController.isPresent()) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
- // App sometimes draws before the insets from WindowDecoration#relayout have
- // been added, so they must be added here
- mWindowDecorByTaskId.get(mTaskId).addCaptionInset(wct);
- mDesktopTasksController.get().moveToDesktop(mTaskId, wct);
- }
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ // 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,
+ DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON);
decoration.closeHandleMenu();
} else if (id == R.id.fullscreen_button) {
decoration.closeHandleMenu();
@@ -428,42 +461,32 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mSplitScreenController.moveTaskToFullscreen(mTaskId,
SplitScreenController.EXIT_REASON_DESKTOP_MODE);
} else {
- mDesktopTasksController.ifPresent(c ->
- c.moveToFullscreen(mTaskId));
+ mDesktopTasksController.moveToFullscreen(mTaskId,
+ DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON);
}
} else if (id == R.id.split_screen_button) {
decoration.closeHandleMenu();
- mDesktopTasksController.ifPresent(c -> {
- c.requestSplit(decoration.mTaskInfo);
- });
+ mDesktopTasksController.requestSplit(decoration.mTaskInfo);
} else if (id == R.id.collapse_menu_button) {
decoration.closeHandleMenu();
- } else if (id == R.id.select_button) {
- if (DesktopModeStatus.IS_DISPLAY_CHANGE_ENABLED) {
- // TODO(b/278084491): dev option to enable display switching
- // remove when select is implemented
- mDesktopTasksController.ifPresent(c -> c.moveToNextDisplay(mTaskId));
- }
} else if (id == R.id.maximize_window) {
final RunningTaskInfo taskInfo = decoration.mTaskInfo;
decoration.closeHandleMenu();
decoration.closeMaximizeMenu();
- mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo));
+ mDesktopTasksController.toggleDesktopTaskSize(taskInfo);
} else if (id == R.id.maximize_menu_maximize_button) {
final RunningTaskInfo taskInfo = decoration.mTaskInfo;
- mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo));
+ mDesktopTasksController.toggleDesktopTaskSize(taskInfo);
decoration.closeHandleMenu();
decoration.closeMaximizeMenu();
} else if (id == R.id.maximize_menu_snap_left_button) {
final RunningTaskInfo taskInfo = decoration.mTaskInfo;
- mDesktopTasksController.ifPresent(c -> c.snapToHalfScreen(
- taskInfo, SnapPosition.LEFT));
+ mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.LEFT);
decoration.closeHandleMenu();
decoration.closeMaximizeMenu();
} else if (id == R.id.maximize_menu_snap_right_button) {
final RunningTaskInfo taskInfo = decoration.mTaskInfo;
- mDesktopTasksController.ifPresent(c -> c.snapToHalfScreen(
- taskInfo, SnapPosition.RIGHT));
+ mDesktopTasksController.snapToHalfScreen(taskInfo, SnapPosition.RIGHT);
decoration.closeHandleMenu();
decoration.closeMaximizeMenu();
}
@@ -495,6 +518,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
(int) e.getRawX(), (int) e.getRawY());
final boolean isTransparentCaption =
TaskInfoKt.isTransparentCaptionBarAppearance(decoration.mTaskInfo);
+ // MotionEvent's coordinates are relative to view, we want location in window
+ // to offset position relative to caption as a whole.
+ int[] viewLocation = new int[2];
+ v.getLocationInWindow(viewLocation);
+ final boolean isResizeEvent = decoration.shouldResizeListenerHandleEvent(e,
+ new Point(viewLocation[0], viewLocation[1]));
// The caption window may be a spy window when the caption background is
// transparent, which means events will fall through to the app window. Make
// sure to cancel these events if they do not happen in the intersection of the
@@ -502,11 +531,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
// the drag-move or other caption gestures should take priority outside those
// regions.
mShouldPilferCaptionEvents = !(downInCustomizableCaptionRegion
- && downInExclusionRegion && isTransparentCaption);
+ && downInExclusionRegion && isTransparentCaption) && !isResizeEvent;
}
if (!mShouldPilferCaptionEvents) {
- // The event will be handled by a window below.
+ // The event will be handled by a window below or pilfered by resize handler.
return false;
}
// Otherwise pilfer so that windows below receive cancellations for this gesture, and
@@ -553,17 +582,28 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
// Re-hovering over any of the maximize menu views should keep the menu open by
// cancelling any attempts to close the menu.
mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable);
+ if (id != R.id.maximize_window) {
+ decoration.onMaximizeMenuHoverEnter(id, ev);
+ }
}
return true;
+ } else if (ev.getAction() == ACTION_HOVER_MOVE
+ && MaximizeMenu.Companion.isMaximizeMenuView(id)) {
+ decoration.onMaximizeMenuHoverMove(id, ev);
+ mMainHandler.removeCallbacks(mCloseMaximizeWindowRunnable);
} else if (ev.getAction() == ACTION_HOVER_EXIT) {
if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) {
decoration.onMaximizeWindowHoverExit();
- } else if (id == R.id.maximize_window || id == R.id.maximize_menu) {
+ } else if (id == R.id.maximize_window
+ || MaximizeMenu.Companion.isMaximizeMenuView(id)) {
// Close menu if not hovering over maximize menu or maximize button after a
// delay to give user a chance to re-enter view or to move from one maximize
// menu view to another.
mMainHandler.postDelayed(mCloseMaximizeWindowRunnable,
CLOSE_MAXIMIZE_MENU_DELAY_MS);
+ if (id != R.id.maximize_window) {
+ decoration.onMaximizeMenuHoverExit(id, ev);
+ }
}
return true;
}
@@ -572,7 +612,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
private void moveTaskToFront(RunningTaskInfo taskInfo) {
if (!taskInfo.isFocused) {
- mDesktopTasksController.ifPresent(c -> c.moveTaskToFront(taskInfo));
+ mDesktopTasksController.moveTaskToFront(taskInfo);
}
}
@@ -584,7 +624,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
public boolean handleMotionEvent(@Nullable View v, MotionEvent e) {
final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
final RunningTaskInfo taskInfo = decoration.mTaskInfo;
- if (DesktopModeStatus.isEnabled()
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
return false;
}
@@ -606,7 +646,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
// prevent the button's ripple effect from showing.
return !touchingButton;
}
- case MotionEvent.ACTION_MOVE: {
+ case ACTION_MOVE: {
// If a decor's resize drag zone is active, don't also try to reposition it.
if (decoration.isHandlingDragResize()) break;
decoration.closeMaximizeMenu();
@@ -616,10 +656,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningMove(
e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
- mDesktopTasksController.ifPresent(c -> c.onDragPositioningMove(taskInfo,
+ mDesktopTasksController.onDragPositioningMove(taskInfo,
decoration.mTaskSurface,
e.getRawX(dragPointerIdx),
- newTaskBounds));
+ newTaskBounds);
mIsDragging = true;
return true;
}
@@ -641,10 +681,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
(int) (e.getRawY(dragPointerIdx) - e.getY(dragPointerIdx)));
final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd(
e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
- mDesktopTasksController.ifPresent(c -> c.onDragPositioningEnd(taskInfo,
- position,
+ mDesktopTasksController.onDragPositioningEnd(taskInfo, position,
new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)),
- newTaskBounds));
+ newTaskBounds, decoration.calculateValidDragArea());
if (touchingButton && !mHasLongClicked) {
// We need the input event to not be consumed here to end the ripple
// effect on the touched button. We will reset drag state in the ensuing
@@ -672,10 +711,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
&& action != MotionEvent.ACTION_CANCEL)) {
return false;
}
- mDesktopTasksController.ifPresent(c -> {
- final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
- c.toggleDesktopTaskSize(decoration.mTaskInfo);
- });
+ final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId);
+ mDesktopTasksController.toggleDesktopTaskSize(decoration.mTaskInfo);
return true;
}
}
@@ -764,7 +801,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
*/
private void handleReceivedMotionEvent(MotionEvent ev, InputMonitor inputMonitor) {
final DesktopModeWindowDecoration relevantDecor = getRelevantWindowDecor(ev);
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
if (!mInImmersiveMode && (relevantDecor == null
|| relevantDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM
|| mTransitionDragActive)) {
@@ -773,7 +810,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
handleEventOutsideCaption(ev, relevantDecor);
// Prevent status bar from reacting to a caption drag.
- if (DesktopModeStatus.isEnabled()) {
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
if (mTransitionDragActive) {
inputMonitor.pilferPointers();
}
@@ -793,7 +830,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
if (relevantDecor == null || relevantDecor.checkTouchEventInCaption(ev)) {
return;
}
-
+ relevantDecor.updateHoverAndPressStatus(ev);
final int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
if (!mTransitionDragActive) {
@@ -810,74 +847,87 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
*/
private void handleCaptionThroughStatusBar(MotionEvent ev,
DesktopModeWindowDecoration relevantDecor) {
+ if (relevantDecor == null) {
+ if (ev.getActionMasked() == ACTION_UP) {
+ mMoveToDesktopAnimator = null;
+ mTransitionDragActive = false;
+ }
+ return;
+ }
switch (ev.getActionMasked()) {
+ case MotionEvent.ACTION_HOVER_EXIT:
+ case MotionEvent.ACTION_HOVER_MOVE:
+ case MotionEvent.ACTION_HOVER_ENTER: {
+ relevantDecor.updateHoverAndPressStatus(ev);
+ break;
+ }
case MotionEvent.ACTION_DOWN: {
// Begin drag through status bar if applicable.
- if (relevantDecor != null) {
- mDragToDesktopAnimationStartBounds.set(
- relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds());
- boolean dragFromStatusBarAllowed = false;
- if (DesktopModeStatus.isEnabled()) {
- // In proto2 any full screen or multi-window task can be dragged to
- // freeform.
- final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode();
- dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN
- || windowingMode == WINDOWING_MODE_MULTI_WINDOW;
- }
+ relevantDecor.checkTouchEvent(ev);
+ relevantDecor.updateHoverAndPressStatus(ev);
+ mDragToDesktopAnimationStartBounds.set(
+ relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds());
+ boolean dragFromStatusBarAllowed = false;
+ if (DesktopModeStatus.canEnterDesktopMode(mContext)) {
+ // In proto2 any full screen or multi-window task can be dragged to
+ // freeform.
+ final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode();
+ dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN
+ || windowingMode == WINDOWING_MODE_MULTI_WINDOW;
+ }
- if (dragFromStatusBarAllowed
- && relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)) {
- mTransitionDragActive = true;
- }
+ if (dragFromStatusBarAllowed
+ && relevantDecor.checkTouchEventInFocusedCaptionHandle(ev)) {
+ mTransitionDragActive = true;
}
break;
}
case MotionEvent.ACTION_UP: {
- if (relevantDecor == null) {
- mMoveToDesktopAnimator = null;
- mTransitionDragActive = false;
- return;
- }
if (mTransitionDragActive) {
+ mDesktopTasksController.updateVisualIndicator(relevantDecor.mTaskInfo,
+ relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY());
mTransitionDragActive = false;
- final int statusBarHeight = getStatusBarHeight(
- relevantDecor.mTaskInfo.displayId);
- if (ev.getRawY() > 2 * statusBarHeight) {
- if (DesktopModeStatus.isEnabled()) {
- animateToDesktop(relevantDecor, ev);
- }
- mMoveToDesktopAnimator = null;
- return;
- } else if (mMoveToDesktopAnimator != null) {
- mDesktopTasksController.ifPresent(
- c -> c.cancelDragToDesktop(relevantDecor.mTaskInfo));
+ if (mMoveToDesktopAnimator != null) {
+ // Though this isn't a hover event, we need to update handle's hover state
+ // as it likely will change.
+ relevantDecor.updateHoverAndPressStatus(ev);
+ mDesktopTasksController.onDragPositioningEndThroughStatusBar(
+ new PointF(ev.getRawX(), ev.getRawY()), relevantDecor.mTaskInfo);
mMoveToDesktopAnimator = null;
return;
+ } else {
+ // In cases where we create an indicator but do not start the
+ // move-to-desktop animation, we need to dismiss it.
+ mDesktopTasksController.releaseVisualIndicator();
}
}
- relevantDecor.checkClickEvent(ev);
+ relevantDecor.checkTouchEvent(ev);
break;
}
- case MotionEvent.ACTION_MOVE: {
+ case ACTION_MOVE: {
if (relevantDecor == null) {
return;
}
if (mTransitionDragActive) {
- mDesktopTasksController.ifPresent(
- c -> c.updateVisualIndicator(
+ // Do not create an indicator at all if we're not past transition height.
+ DisplayLayout layout = mDisplayController
+ .getDisplayLayout(relevantDecor.mTaskInfo.displayId);
+ if (ev.getRawY() < 2 * layout.stableInsets().top
+ && mMoveToDesktopAnimator == null) {
+ return;
+ }
+ final DesktopModeVisualIndicator.IndicatorType indicatorType =
+ mDesktopTasksController.updateVisualIndicator(
relevantDecor.mTaskInfo,
- relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY()));
- final int statusBarHeight = getStatusBarHeight(
- relevantDecor.mTaskInfo.displayId);
- if (ev.getRawY() > statusBarHeight) {
+ relevantDecor.mTaskSurface, ev.getRawX(), ev.getRawY());
+ if (indicatorType != TO_FULLSCREEN_INDICATOR) {
if (mMoveToDesktopAnimator == null) {
mMoveToDesktopAnimator = new MoveToDesktopAnimator(
mContext, mDragToDesktopAnimationStartBounds,
relevantDecor.mTaskInfo, relevantDecor.mTaskSurface);
- mDesktopTasksController.ifPresent(
- c -> c.startDragToDesktop(relevantDecor.mTaskInfo,
- mMoveToDesktopAnimator));
+ mDesktopTasksController.startDragToDesktop(relevantDecor.mTaskInfo,
+ mMoveToDesktopAnimator);
}
}
if (mMoveToDesktopAnimator != null) {
@@ -894,72 +944,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
}
- /**
- * Gets bounds of a scaled window centered relative to the screen bounds
- * @param scale the amount to scale to relative to the Screen Bounds
- */
- private Rect calculateFreeformBounds(int displayId, float scale) {
- // TODO(b/319819547): Account for app constraints so apps do not become letterboxed
- final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(displayId);
- final int screenWidth = displayLayout.width();
- final int screenHeight = displayLayout.height();
-
- final float adjustmentPercentage = (1f - scale) / 2;
- return new Rect((int) (screenWidth * adjustmentPercentage),
- (int) (screenHeight * adjustmentPercentage),
- (int) (screenWidth * (adjustmentPercentage + scale)),
- (int) (screenHeight * (adjustmentPercentage + scale)));
- }
-
- /**
- * Blocks relayout until transition is finished and transitions to Desktop
- */
- private void animateToDesktop(DesktopModeWindowDecoration relevantDecor,
- MotionEvent ev) {
- centerAndMoveToDesktopWithAnimation(relevantDecor, ev);
- }
-
- /**
- * Animates a window to the center, grows to freeform size, and transitions to Desktop Mode.
- * @param relevantDecor the window decor of the task to be animated
- * @param ev the motion event that triggers the animation
- */
- private void centerAndMoveToDesktopWithAnimation(DesktopModeWindowDecoration relevantDecor,
- MotionEvent ev) {
- ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
- animator.setDuration(FREEFORM_ANIMATION_DURATION);
- final SurfaceControl sc = relevantDecor.mTaskSurface;
- final Rect endBounds = calculateFreeformBounds(ev.getDisplayId(), DRAG_FREEFORM_SCALE);
- final Transaction t = mTransactionFactory.get();
- final float diffX = endBounds.centerX() - ev.getRawX();
- final float diffY = endBounds.top - ev.getRawY();
- final float startingX = ev.getRawX() - DRAG_FREEFORM_SCALE
- * mDragToDesktopAnimationStartBounds.width() / 2;
-
- animator.addUpdateListener(animation -> {
- final float animatorValue = (float) animation.getAnimatedValue();
- final float x = startingX + diffX * animatorValue;
- final float y = ev.getRawY() + diffY * animatorValue;
- t.setPosition(sc, x, y);
- t.apply();
- });
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mDesktopTasksController.ifPresent(
- c -> {
- c.onDragPositioningEndThroughStatusBar(relevantDecor.mTaskInfo,
- calculateFreeformBounds(ev.getDisplayId(),
- DesktopTasksController
- .DESKTOP_MODE_INITIAL_BOUNDS_SCALE));
- });
- }
- });
- animator.start();
- }
-
@Nullable
private DesktopModeWindowDecoration getRelevantWindowDecor(MotionEvent ev) {
+ // If we are mid-transition, dragged task's decor is always relevant.
+ final int draggedTaskId = mDesktopTasksController.getDraggingTaskId();
+ if (draggedTaskId != INVALID_TASK_ID) {
+ return mWindowDecorByTaskId.get(draggedTaskId);
+ }
final DesktopModeWindowDecoration focusedDecor = getFocusedDecor();
if (focusedDecor == null) {
return null;
@@ -1048,12 +1039,19 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
&& taskInfo.isFocused) {
return false;
}
- return DesktopModeStatus.isEnabled()
+ // 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
&& taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
- && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop()
- && mDisplayController.getDisplayContext(taskInfo.displayId)
- .getResources().getConfiguration().smallestScreenWidthDp >= 600;
+ && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop();
}
private void createWindowDecoration(
@@ -1078,21 +1076,18 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mSyncQueue,
mRootTaskDisplayAreaOrganizer);
mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration);
- windowDecoration.createResizeVeil();
final DragPositioningCallback dragPositioningCallback;
- final int transitionAreaHeight = mContext.getResources().getDimensionPixelSize(
- R.dimen.desktop_mode_transition_area_height);
if (!DesktopModeStatus.isVeiledResizeEnabled()) {
dragPositioningCallback = new FluidResizeTaskPositioner(
mTaskOrganizer, mTransitions, windowDecoration, mDisplayController,
- mDragStartListener, mTransactionFactory, transitionAreaHeight);
+ mDragStartListener, mTransactionFactory);
windowDecoration.setTaskDragResizer(
(FluidResizeTaskPositioner) dragPositioningCallback);
} else {
dragPositioningCallback = new VeiledResizeTaskPositioner(
mTaskOrganizer, windowDecoration, mDisplayController,
- mDragStartListener, mTransitions, transitionAreaHeight);
+ mDragStartListener, mTransitions);
windowDecoration.setTaskDragResizer(
(VeiledResizeTaskPositioner) dragPositioningCallback);
}
@@ -1122,10 +1117,19 @@ 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");
- pw.println(innerPrefix + "DesktopModeStatus=" + DesktopModeStatus.isEnabled());
+ pw.println(innerPrefix + "DesktopModeStatus="
+ + DesktopModeStatus.canEnterDesktopMode(mContext));
pw.println(innerPrefix + "mTransitionDragActive=" + mTransitionDragActive);
pw.println(innerPrefix + "mEventReceiversByDisplay=" + mEventReceiversByDisplay);
pw.println(innerPrefix + "mWindowDecorByTaskId=" + mWindowDecorByTaskId);
@@ -1181,12 +1185,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
@Override
public void onExclusionRegionChanged(int taskId, Region region) {
- mDesktopTasksController.ifPresent(d -> d.onExclusionRegionChanged(taskId, region));
+ mDesktopTasksController.onExclusionRegionChanged(taskId, region);
}
@Override
public void onExclusionRegionDismissed(int taskId) {
- mDesktopTasksController.ifPresent(d -> d.removeExclusionRegionForTask(taskId));
+ mDesktopTasksController.removeExclusionRegionForTask(taskId);
}
}
@@ -1225,7 +1229,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
final boolean inImmersiveMode = !source.isVisible();
// Calls WindowDecoration#relayout if decoration visibility needs to be updated
if (inImmersiveMode != mInImmersiveMode) {
- decor.relayout(decor.mTaskInfo);
+ if (Flags.enableDesktopWindowingImmersiveHandleHiding()) {
+ decor.relayout(decor.mTaskInfo);
+ }
mInImmersiveMode = inImmersiveMode;
}
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 39803e2afd34..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
@@ -19,12 +19,20 @@ package com.android.wm.shell.windowdecor;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.windowingModeToString;
+import static android.view.MotionEvent.ACTION_CANCEL;
+import static android.view.MotionEvent.ACTION_DOWN;
+import static android.view.MotionEvent.ACTION_UP;
import static com.android.launcher3.icons.BaseIconFactory.MODE_DEFAULT;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize;
+import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize;
+import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.WindowConfiguration.WindowingMode;
import android.content.Context;
+import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
@@ -36,11 +44,15 @@ import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.os.Handler;
+import android.os.Trace;
+import android.util.Log;
+import android.util.Size;
import android.view.Choreographer;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewConfiguration;
+import android.view.WindowManager;
import android.widget.ImageButton;
import android.window.WindowContainerTransaction;
@@ -48,18 +60,19 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.IconProvider;
+import com.android.window.flags.Flags;
import com.android.wm.shell.R;
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.SyncTransactionQueue;
-import com.android.wm.shell.desktopmode.DesktopModeStatus;
-import com.android.wm.shell.desktopmode.DesktopTasksController;
+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;
@@ -78,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;
@@ -86,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;
@@ -97,9 +112,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
private MaximizeMenu mMaximizeMenu;
private ResizeVeil mResizeVeil;
-
- private Drawable mAppIconDrawable;
private Bitmap mAppIconBitmap;
+ private Bitmap mResizeVeilBitmap;
+
private CharSequence mAppName;
private ExclusionRegionListener mExclusionRegionListener;
@@ -112,12 +127,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
- Configuration windowDecorConfig,
Handler handler,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
- this (context, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorConfig,
+ this (context, displayController, taskOrganizer, taskInfo, taskSurface,
handler, choreographer, syncQueue, rootTaskDisplayAreaOrganizer,
SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
WindowContainerTransaction::new, SurfaceControl::new,
@@ -130,7 +144,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
ShellTaskOrganizer taskOrganizer,
ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
- Configuration windowDecorConfig,
Handler handler,
Choreographer choreographer,
SyncTransactionQueue syncQueue,
@@ -140,17 +153,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
Supplier<SurfaceControl> surfaceControlSupplier,
SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
- super(context, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorConfig,
+ super(context, displayController, taskOrganizer, taskInfo, taskSurface,
surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
windowContainerTransactionSupplier, surfaceControlSupplier,
surfaceControlViewHostFactory);
-
mHandler = handler;
mChoreographer = choreographer;
mSyncQueue = syncQueue;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
-
- loadAppInfo();
}
void setCaptionListeners(
@@ -186,16 +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);
}
@@ -207,54 +289,41 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
final WindowContainerTransaction wct = new WindowContainerTransaction();
- 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
+ Trace.beginSection("DesktopModeWindowDecoration#relayout-applyWCT");
mTaskOrganizer.applyTransaction(wct);
+ Trace.endSection();
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#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) {
- 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);
+ Trace.endSection();
if (!mTaskInfo.isFocused) {
closeHandleMenu();
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();
@@ -265,43 +334,79 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) {
closeDragResizeListener();
+ Trace.beginSection("DesktopModeWindowDecoration#relayout-DragResizeInputListener");
mDragResizeListener = new DragResizeInputListener(
mContext,
mHandler,
mChoreographer,
mDisplay.getDisplayId(),
- mRelayoutParams.mCornerRadius,
mDecorationContainerSurface,
mDragPositioningCallback,
mSurfaceControlBuilderSupplier,
mSurfaceControlTransactionSupplier,
mDisplayController);
+ Trace.endSection();
}
final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext())
.getScaledTouchSlop();
mDragDetector.setTouchSlop(touchSlop);
- final int resize_handle = mResult.mRootView.getResources()
- .getDimensionPixelSize(R.dimen.freeform_resize_handle);
- final int resize_corner = mResult.mRootView.getResources()
- .getDimensionPixelSize(R.dimen.freeform_resize_corner);
-
// If either task geometry or position have changed, update this task's
// exclusion region listener
+ final Resources res = mResult.mRootView.getResources();
if (mDragResizeListener.setGeometry(
- mResult.mWidth, mResult.mHeight, resize_handle, resize_corner, touchSlop)
+ new DragResizeWindowGeometry(mRelayoutParams.mCornerRadius,
+ new Size(mResult.mWidth, mResult.mHeight), getResizeEdgeHandleSize(res),
+ getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop)
|| !mTaskInfo.positionInParent.equals(mPositionInParent)) {
updateExclusionRegion();
}
+ }
+
+ 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);
+ }
+ }
- if (isMaximizeMenuActive()) {
- 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;
+ });
}
+ throw new IllegalArgumentException("Unexpected layout resource id");
}
@VisibleForTesting
@@ -312,17 +417,22 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
boolean applyStartTransactionOnDraw,
boolean shouldSetTaskPositionAndCrop) {
final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode());
+ final boolean isAppHeader =
+ 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;
relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode());
relayoutParams.mCaptionWidthId = getCaptionWidthId(relayoutParams.mLayoutResId);
- if (captionLayoutId == R.layout.desktop_mode_app_controls_window_decor
- && TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
- // App is requesting to customize the caption bar. Allow input to fall through to the
- // windows below so that the app can respond to input events on their custom content.
- relayoutParams.mAllowCaptionInputFallthrough = true;
+ if (isAppHeader) {
+ if (TaskInfoKt.isTransparentCaptionBarAppearance(taskInfo)) {
+ // If the app is requesting to customize the caption bar, allow input to fall
+ // through to the windows below so that the app can respond to input events on
+ // their custom content.
+ relayoutParams.mInputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY;
+ }
// Report occluding elements as bounding rects to the insets system so that apps can
// draw in the empty space in the center:
// First, the "app chip" section of the caption bar (+ some extra margins).
@@ -337,6 +447,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
controlsElement.mWidthResId = R.dimen.desktop_mode_customizable_caption_margin_end;
controlsElement.mAlignment = RelayoutParams.OccludingCaptionElement.Alignment.END;
relayoutParams.mOccludingCaptionElements.add(controlsElement);
+ } else if (isAppHandle) {
+ // The focused decor (fullscreen/split) does not need to handle input because input in
+ // the App Handle is handled by the InputMonitor in DesktopModeWindowDecorViewModel.
+ relayoutParams.mInputFeatures
+ |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
}
if (DesktopModeStatus.useWindowShadow(/* isFocusedWindow= */ taskInfo.isFocused)) {
relayoutParams.mShadowRadiusId = taskInfo.isFocused
@@ -345,19 +460,25 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
relayoutParams.mSetTaskPositionAndCrop = shouldSetTaskPositionAndCrop;
- // The configuration used to lay out the window decoration. The system context's config is
- // used when the task density has been overridden to a custom density so that the resources
- // and views of the decoration aren't affected and match the rest of the System UI, if not
- // then just use the task's configuration. A copy is made instead of using the original
- // reference so that the configuration isn't mutated on config changes and diff checks can
- // be made in WindowDecoration#relayout using the pre/post-relayout configuration.
- // See b/301119301.
+
+ // The configuration used to layout the window decoration. A copy is made instead of using
+ // the original reference so that the configuration isn't mutated on config changes and
+ // diff checks can be made in WindowDecoration#relayout using the pre/post-relayout
+ // configuration. See b/301119301.
// TODO(b/301119301): consider moving the config data needed for diffs to relayout params
// instead of using a whole Configuration as a parameter.
final Configuration windowDecorConfig = new Configuration();
- windowDecorConfig.setTo(DesktopTasksController.isDesktopDensityOverrideSet()
- ? context.getResources().getConfiguration() // Use system context.
- : taskInfo.configuration); // Use task configuration.
+ if (Flags.enableAppHeaderWithTaskDensity() && isAppHeader) {
+ // 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.useDesktopOverrideDensity()) {
+ // The task has had its density overridden, but keep using the system's density to
+ // layout the header.
+ windowDecorConfig.setTo(context.getResources().getConfiguration());
+ } else {
+ windowDecorConfig.setTo(taskInfo.configuration);
+ }
relayoutParams.mWindowDecorConfig = windowDecorConfig;
if (DesktopModeStatus.useRoundedCorners()) {
@@ -371,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;
@@ -399,7 +520,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
final int menuHeight = loadDimensionPixelSize(
resources, R.dimen.desktop_mode_maximize_menu_height);
- float menuLeft = (mPositionInParent.x + maximizeButtonLocation[0]);
+ float menuLeft = (mPositionInParent.x + maximizeButtonLocation[0] - ((float) (menuWidth
+ - maximizeWindowButton.getWidth()) / 2));
float menuTop = (mPositionInParent.y + captionHeight);
final float menuRight = menuLeft + menuWidth;
final float menuBottom = menuTop + menuHeight;
@@ -419,21 +541,50 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
return mHandleMenu != null;
}
+ boolean shouldResizeListenerHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) {
+ return mDragResizeListener != null && mDragResizeListener.shouldHandleEvent(e, offset);
+ }
+
boolean isHandlingDragResize() {
return mDragResizeListener != null && mDragResizeListener.isHandlingDragResize();
}
- private void loadAppInfo() {
- PackageManager pm = mContext.getApplicationContext().getPackageManager();
- final IconProvider provider = new IconProvider(mContext);
- mAppIconDrawable = provider.getIcon(mTaskInfo.topActivityInfo);
- final Resources resources = mContext.getResources();
- final BaseIconFactory factory = new BaseIconFactory(mContext,
- resources.getDisplayMetrics().densityDpi,
- resources.getDimensionPixelSize(R.dimen.desktop_mode_caption_icon_radius));
- mAppIconBitmap = factory.createScaledBitmap(mAppIconDrawable, MODE_DEFAULT);
- final ApplicationInfo applicationInfo = mTaskInfo.topActivityInfo.applicationInfo;
- mAppName = pm.getApplicationLabel(applicationInfo);
+ private void loadAppInfoIfNeeded() {
+ // TODO(b/337370277): move this to another thread.
+ try {
+ Trace.beginSection("DesktopModeWindowDecoration#loadAppInfoIfNeeded");
+ if (mAppIconBitmap != null && mAppName != null) {
+ return;
+ }
+ final ActivityInfo activityInfo = mTaskInfo.topActivityInfo;
+ if (activityInfo == null) {
+ Log.e(TAG, "Top activity info not found in task");
+ return;
+ }
+ PackageManager pm = mContext.getApplicationContext().getPackageManager();
+ final IconProvider provider = new IconProvider(mContext);
+ final Drawable appIconDrawable = provider.getIcon(activityInfo);
+ final BaseIconFactory headerIconFactory = createIconFactory(mContext,
+ R.dimen.desktop_mode_caption_icon_radius);
+ mAppIconBitmap = headerIconFactory.createScaledBitmap(appIconDrawable, MODE_DEFAULT);
+
+ final BaseIconFactory resizeVeilIconFactory = createIconFactory(mContext,
+ R.dimen.desktop_mode_resize_veil_icon_size);
+ mResizeVeilBitmap = resizeVeilIconFactory
+ .createScaledBitmap(appIconDrawable, MODE_DEFAULT);
+
+ final ApplicationInfo applicationInfo = activityInfo.applicationInfo;
+ mAppName = pm.getApplicationLabel(applicationInfo);
+ } finally {
+ Trace.endSection();
+ }
+ }
+
+ private BaseIconFactory createIconFactory(Context context, int dimensions) {
+ final Resources resources = context.getResources();
+ final int densityDpi = resources.getDisplayMetrics().densityDpi;
+ final int iconSize = resources.getDimensionPixelSize(dimensions);
+ return new BaseIconFactory(context, densityDpi, iconSize);
}
private void closeDragResizeListener() {
@@ -448,23 +599,27 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
* Create the resize veil for this task. Note the veil's visibility is View.GONE by default
* until a resize event calls showResizeVeil below.
*/
- void createResizeVeil() {
- mResizeVeil = new ResizeVeil(mContext, mAppIconDrawable, mTaskInfo,
- mSurfaceControlBuilderSupplier, mDisplay, mSurfaceControlTransactionSupplier);
+ private void createResizeVeilIfNeeded() {
+ if (mResizeVeil != null) return;
+ loadAppInfoIfNeeded();
+ mResizeVeil = new ResizeVeil(mContext, mDisplayController, mResizeVeilBitmap,
+ mTaskSurface, mSurfaceControlTransactionSupplier, mTaskInfo);
}
/**
* Show the resize veil.
*/
public void showResizeVeil(Rect taskBounds) {
- mResizeVeil.showVeil(mTaskSurface, taskBounds);
+ createResizeVeilIfNeeded();
+ mResizeVeil.showVeil(mTaskSurface, taskBounds, mTaskInfo);
}
/**
* Show the resize veil.
*/
public void showResizeVeil(SurfaceControl.Transaction tx, Rect taskBounds) {
- mResizeVeil.showVeil(tx, mTaskSurface, taskBounds, false /* fadeIn */);
+ createResizeVeilIfNeeded();
+ mResizeVeil.showVeil(tx, mTaskSurface, taskBounds, mTaskInfo, false /* fadeIn */);
}
/**
@@ -498,8 +653,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
* Determine valid drag area for this task based on elements in the app chip.
*/
@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;
@@ -581,15 +737,18 @@ 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)
.setAppName(mAppName)
.setOnClickListener(mOnCaptionButtonClickListener)
.setOnTouchListener(mOnCaptionTouchListener)
.setLayoutId(mRelayoutParams.mLayoutResId)
- .setWindowingButtonsVisible(DesktopModeStatus.isEnabled())
+ .setWindowingButtonsVisible(DesktopModeStatus.canEnterDesktopMode(mContext))
.setCaptionHeight(mResult.mCaptionHeight)
+ .setDisplayController(mDisplayController)
+ .setSplitScreenController(splitScreenController)
.build();
mWindowDecorViewHolder.onHandleMenuOpened();
mHandleMenu.show();
@@ -606,10 +765,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
@Override
- void releaseViews() {
+ void releaseViews(WindowContainerTransaction wct) {
closeHandleMenu();
closeMaximizeMenu();
- super.releaseViews();
+ super.releaseViews(wct);
}
/**
@@ -677,7 +836,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
*/
boolean checkTouchEventInFocusedCaptionHandle(MotionEvent ev) {
if (isHandleMenuActive() || !(mWindowDecorViewHolder
- instanceof DesktopModeFocusedWindowDecorationViewHolder)) {
+ instanceof AppHandleViewHolder)) {
return false;
}
@@ -706,27 +865,54 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
/**
- * Check a passed MotionEvent if a click has occurred on any button on this caption
+ * Check a passed MotionEvent if it has occurred on any button related to this decor.
* Note this should only be called when a regular onClick is not possible
* (i.e. the button was clicked through status bar layer)
*
* @param ev the MotionEvent to compare
*/
- void checkClickEvent(MotionEvent ev) {
+ void checkTouchEvent(MotionEvent ev) {
if (mResult.mRootView == null) return;
- if (!isHandleMenuActive()) {
- // Click if point in caption handle view
- final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
- final View handle = caption.findViewById(R.id.caption_handle);
- if (checkTouchEventInFocusedCaptionHandle(ev)) {
- mOnCaptionButtonClickListener.onClick(handle);
- }
- } else {
- mHandleMenu.checkClickEvent(ev);
+ final View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption);
+ final View handle = caption.findViewById(R.id.caption_handle);
+ final boolean inHandle = !isHandleMenuActive()
+ && checkTouchEventInFocusedCaptionHandle(ev);
+ final int action = ev.getActionMasked();
+ if (action == ACTION_UP && inHandle) {
+ handle.performClick();
+ }
+ if (isHandleMenuActive()) {
+ mHandleMenu.checkMotionEvent(ev);
closeHandleMenuIfNeeded(ev);
}
}
+ /**
+ * Updates hover and pressed status of views in this decoration. Should only be called
+ * when status cannot be updated normally (i.e. the button is hovered through status
+ * bar layer).
+ * @param ev the MotionEvent to compare against.
+ */
+ void updateHoverAndPressStatus(MotionEvent ev) {
+ if (mResult.mRootView == null) return;
+ final View handle = mResult.mRootView.findViewById(R.id.caption_handle);
+ final boolean inHandle = !isHandleMenuActive()
+ && checkTouchEventInFocusedCaptionHandle(ev);
+ final int action = ev.getActionMasked();
+ // The comparison against ACTION_UP is needed for the cancel drag to desktop case.
+ handle.setHovered(inHandle && action != ACTION_UP);
+ // 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() && !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;
@@ -738,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() {
@@ -764,7 +951,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
*/
private Region getGlobalExclusionRegion() {
Region exclusionRegion;
- if (mTaskInfo.isResizeable) {
+ if (mDragResizeListener != null && mTaskInfo.isResizeable) {
exclusionRegion = mDragResizeListener.getCornersRegion();
} else {
exclusionRegion = new Region();
@@ -775,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);
@@ -796,21 +987,39 @@ 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();
}
+ /** Called when there is a {@Link ACTION_HOVER_ENTER} on a view in the maximize menu. */
+ void onMaximizeMenuHoverEnter(int id, MotionEvent ev) {
+ mMaximizeMenu.onMaximizeMenuHoverEnter(id, ev);
+ }
+
+ /** Called when there is a {@Link ACTION_HOVER_MOVE} on a view in the maximize menu. */
+ void onMaximizeMenuHoverMove(int id, MotionEvent ev) {
+ mMaximizeMenu.onMaximizeMenuHoverMove(id, ev);
+ }
+
+ /** Called when there is a {@Link ACTION_HOVER_EXIT} on a view in the maximize menu. */
+ void onMaximizeMenuHoverExit(int id, MotionEvent ev) {
+ mMaximizeMenu.onMaximizeMenuHoverExit(id, ev);
+ }
+
+
@Override
public String toString() {
return "{"
@@ -833,17 +1042,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
Choreographer choreographer,
SyncTransactionQueue syncQueue,
RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) {
- final Configuration windowDecorConfig =
- DesktopTasksController.isDesktopDensityOverrideSet()
- ? context.getResources().getConfiguration() // Use system context
- : taskInfo.configuration; // Use task configuration
return new DesktopModeWindowDecoration(
context,
displayController,
taskOrganizer,
taskInfo,
taskSurface,
- windowDecorConfig,
handler,
choreographer,
syncQueue,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
index 8ce2d6d6d092..421ffd929fb2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallback.java
@@ -23,6 +23,9 @@ import android.graphics.Rect;
* Callback called when receiving drag-resize or drag-move related input events.
*/
public interface DragPositioningCallback {
+ /**
+ * Indicates the direction of resizing. May be combined together to indicate a diagonal drag.
+ */
@IntDef(flag = true, value = {
CTRL_TYPE_UNDEFINED, CTRL_TYPE_LEFT, CTRL_TYPE_RIGHT, CTRL_TYPE_TOP, CTRL_TYPE_BOTTOM
})
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 5afbd54088d1..fe1c9c3cce66 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,16 @@ 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 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 +39,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 +56,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,
@@ -131,7 +136,7 @@ public class DragPositioningCallbackUtility {
t.setPosition(decoration.mTaskSurface, repositionTaskBounds.left, repositionTaskBounds.top);
}
- private static void updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart,
+ static void updateTaskBounds(Rect repositionTaskBounds, Rect taskBoundsAtDragStart,
PointF repositionStartPoint, float x, float y) {
final float deltaX = x - repositionStartPoint.x;
final float deltaY = y - repositionStartPoint.y;
@@ -140,65 +145,69 @@ public class DragPositioningCallbackUtility {
}
/**
- * Calculates the new position of the top edge of the task and returns true if it is below the
- * disallowed area.
+ * If task bounds are outside of provided drag area, snap the bounds to be just inside the
+ * drag area.
*
- * @param disallowedAreaForEndBoundsHeight the height of the area that where the task positioner
- * should not finalize the bounds using WCT#setBounds
- * @param taskBoundsAtDragStart the bounds of the task on the first drag input event
- * @param repositionStartPoint initial input coordinate
- * @param y the y position of the motion event
- * @return true if the top of the task is below the disallowed area
+ * @param repositionTaskBounds bounds determined by task positioner
+ * @param validDragArea the area that task must be positioned inside
+ * @return whether bounds were modified
*/
- static boolean isBelowDisallowedArea(int disallowedAreaForEndBoundsHeight,
- Rect taskBoundsAtDragStart, PointF repositionStartPoint, float y) {
- final float deltaY = y - repositionStartPoint.y;
- final float topPosition = taskBoundsAtDragStart.top + deltaY;
- return topPosition > disallowedAreaForEndBoundsHeight;
- }
-
- /**
- * Updates repositionTaskBounds to the final bounds of the task after the drag is finished. If
- * the bounds are outside of the valid drag area, the task is shifted back onto the edge of the
- * valid drag area.
- */
- static void onDragEnd(Rect repositionTaskBounds, Rect taskBoundsAtDragStart,
- PointF repositionStartPoint, float x, float y, Rect validDragArea) {
- updateTaskBounds(repositionTaskBounds, taskBoundsAtDragStart, repositionStartPoint,
- x, y);
- snapTaskBoundsIfNecessary(repositionTaskBounds, validDragArea);
- }
-
- private static void snapTaskBoundsIfNecessary(Rect repositionTaskBounds, Rect validDragArea) {
+ public static boolean snapTaskBoundsIfNecessary(Rect repositionTaskBounds, Rect validDragArea) {
// If we were never supplied a valid drag area, do not restrict movement.
// Otherwise, we restrict deltas to keep task position inside the Rect.
- if (validDragArea.width() == 0) return;
+ if (validDragArea.width() == 0) return false;
+ boolean result = false;
if (repositionTaskBounds.left < validDragArea.left) {
repositionTaskBounds.offset(validDragArea.left - repositionTaskBounds.left, 0);
+ result = true;
} else if (repositionTaskBounds.left > validDragArea.right) {
repositionTaskBounds.offset(validDragArea.right - repositionTaskBounds.left, 0);
+ result = true;
}
if (repositionTaskBounds.top < validDragArea.top) {
repositionTaskBounds.offset(0, validDragArea.top - repositionTaskBounds.top);
+ result = true;
} else if (repositionTaskBounds.top > validDragArea.bottom) {
repositionTaskBounds.offset(0, validDragArea.bottom - repositionTaskBounds.top);
+ result = true;
}
+ return result;
}
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)
@@ -206,9 +215,15 @@ public class DragPositioningCallbackUtility {
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 e83e5d1ef5a5..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;
@@ -24,13 +23,17 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERL
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_CONSUMER;
-import static com.android.input.flags.Flags.enablePointerChoreographer;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE;
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM;
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;
+import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.input.InputManager;
@@ -38,6 +41,7 @@ import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
+import android.util.Size;
import android.view.Choreographer;
import android.view.IWindowSession;
import android.view.InputChannel;
@@ -51,9 +55,11 @@ import android.view.ViewConfiguration;
import android.view.WindowManagerGlobal;
import android.window.InputTransferToken;
+import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
+import java.util.function.Consumer;
import java.util.function.Supplier;
/**
@@ -65,40 +71,20 @@ import java.util.function.Supplier;
class DragResizeInputListener implements AutoCloseable {
private static final String TAG = "DragResizeInputListener";
private final IWindowSession mWindowSession = WindowManagerGlobal.getWindowSession();
- private final Context mContext;
- private final Handler mHandler;
- private final Choreographer mChoreographer;
- private final InputManager mInputManager;
private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier;
private final int mDisplayId;
private final IBinder mClientToken;
- private final InputTransferToken mInputTransferToken;
private final SurfaceControl mDecorationSurface;
private final InputChannel mInputChannel;
private final TaskResizeInputEventReceiver mInputEventReceiver;
- private final DragPositioningCallback mCallback;
private final SurfaceControl mInputSinkSurface;
private final IBinder mSinkClientToken;
private final InputChannel mSinkInputChannel;
private final DisplayController mDisplayController;
-
- private int mTaskWidth;
- private int mTaskHeight;
- private int mResizeHandleThickness;
- private int mCornerSize;
- private int mTaskCornerRadius;
-
- private Rect mLeftTopCornerBounds;
- private Rect mRightTopCornerBounds;
- private Rect mLeftBottomCornerBounds;
- private Rect mRightBottomCornerBounds;
-
- private int mDragPointerId = -1;
- private DragDetector mDragDetector;
private final Region mTouchRegion = new Region();
DragResizeInputListener(
@@ -106,23 +92,17 @@ class DragResizeInputListener implements AutoCloseable {
Handler handler,
Choreographer choreographer,
int displayId,
- int taskCornerRadius,
SurfaceControl decorationSurface,
DragPositioningCallback callback,
Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
DisplayController displayController) {
- mInputManager = context.getSystemService(InputManager.class);
- mContext = context;
- mHandler = handler;
- mChoreographer = choreographer;
mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
mDisplayId = displayId;
- mTaskCornerRadius = taskCornerRadius;
mDecorationSurface = decorationSurface;
mDisplayController = displayController;
mClientToken = new Binder();
- mInputTransferToken = new InputTransferToken();
+ final InputTransferToken inputTransferToken = new InputTransferToken();
mInputChannel = new InputChannel();
try {
mWindowSession.grantInputChannel(
@@ -135,23 +115,25 @@ class DragResizeInputListener implements AutoCloseable {
INPUT_FEATURE_SPY,
TYPE_APPLICATION,
null /* windowToken */,
- mInputTransferToken,
+ inputTransferToken,
TAG + " of " + decorationSurface.toString(),
mInputChannel);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
- mInputEventReceiver = new TaskResizeInputEventReceiver(
- mInputChannel, mHandler, mChoreographer);
- mCallback = callback;
- mDragDetector = new DragDetector(mInputEventReceiver);
- mDragDetector.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop());
+ mInputEventReceiver = new TaskResizeInputEventReceiver(context, mInputChannel, callback,
+ handler, choreographer, () -> {
+ final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId);
+ return new Size(layout.width(), layout.height());
+ }, this::updateSinkInputChannel);
+ mInputEventReceiver.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop());
mInputSinkSurface = surfaceControlBuilderSupplier.get()
.setName("TaskInputSink of " + decorationSurface)
.setContainerLayer()
.setParent(mDecorationSurface)
+ .setCallsite("DragResizeInputListener.constructor")
.build();
mSurfaceControlTransactionSupplier.get()
.setLayer(mInputSinkSurface, WindowDecoration.INPUT_SINK_Z_ORDER)
@@ -170,7 +152,7 @@ class DragResizeInputListener implements AutoCloseable {
INPUT_FEATURE_NO_INPUT_CHANNEL,
TYPE_INPUT_CONSUMER,
null /* windowToken */,
- mInputTransferToken,
+ inputTransferToken,
"TaskInputSink of " + decorationSurface,
mSinkInputChannel);
} catch (RemoteException e) {
@@ -181,86 +163,26 @@ class DragResizeInputListener implements AutoCloseable {
/**
* Updates the geometry (the touch region) of this drag resize handler.
*
- * @param taskWidth The width of the task.
- * @param taskHeight The height of the task.
- * @param resizeHandleThickness The thickness of the resize handle in pixels.
- * @param cornerSize The size of the resize handle centered in each corner.
- * @param touchSlop The distance in pixels user has to drag with touch for it to register as
- * a resize action.
+ * @param incomingGeometry The geometry update to apply for this task's drag resize regions.
+ * @param touchSlop The distance in pixels user has to drag with touch for it to register
+ * as a resize action.
* @return whether the geometry has changed or not
*/
- boolean setGeometry(int taskWidth, int taskHeight, int resizeHandleThickness, int cornerSize,
- int touchSlop) {
- if (mTaskWidth == taskWidth && mTaskHeight == taskHeight
- && mResizeHandleThickness == resizeHandleThickness
- && mCornerSize == cornerSize) {
+ boolean setGeometry(@NonNull DragResizeWindowGeometry incomingGeometry, int touchSlop) {
+ DragResizeWindowGeometry geometry = mInputEventReceiver.getGeometry();
+ if (incomingGeometry.equals(geometry)) {
+ // Geometry hasn't changed size so skip all updates.
return false;
+ } else {
+ geometry = incomingGeometry;
}
-
- mTaskWidth = taskWidth;
- mTaskHeight = taskHeight;
- mResizeHandleThickness = resizeHandleThickness;
- mCornerSize = cornerSize;
- mDragDetector.setTouchSlop(touchSlop);
+ mInputEventReceiver.setTouchSlop(touchSlop);
mTouchRegion.setEmpty();
- final Rect topInputBounds = new Rect(
- -mResizeHandleThickness,
- -mResizeHandleThickness,
- mTaskWidth + mResizeHandleThickness,
- 0);
- mTouchRegion.union(topInputBounds);
-
- final Rect leftInputBounds = new Rect(
- -mResizeHandleThickness,
- 0,
- 0,
- mTaskHeight);
- mTouchRegion.union(leftInputBounds);
-
- final Rect rightInputBounds = new Rect(
- mTaskWidth,
- 0,
- mTaskWidth + mResizeHandleThickness,
- mTaskHeight);
- mTouchRegion.union(rightInputBounds);
-
- final Rect bottomInputBounds = new Rect(
- -mResizeHandleThickness,
- mTaskHeight,
- mTaskWidth + mResizeHandleThickness,
- mTaskHeight + mResizeHandleThickness);
- mTouchRegion.union(bottomInputBounds);
-
- // Set up touch areas in each corner.
- int cornerRadius = mCornerSize / 2;
- mLeftTopCornerBounds = new Rect(
- -cornerRadius,
- -cornerRadius,
- cornerRadius,
- cornerRadius);
- mTouchRegion.union(mLeftTopCornerBounds);
-
- mRightTopCornerBounds = new Rect(
- mTaskWidth - cornerRadius,
- -cornerRadius,
- mTaskWidth + cornerRadius,
- cornerRadius);
- mTouchRegion.union(mRightTopCornerBounds);
-
- mLeftBottomCornerBounds = new Rect(
- -cornerRadius,
- mTaskHeight - cornerRadius,
- cornerRadius,
- mTaskHeight + cornerRadius);
- mTouchRegion.union(mLeftBottomCornerBounds);
-
- mRightBottomCornerBounds = new Rect(
- mTaskWidth - cornerRadius,
- mTaskHeight - cornerRadius,
- mTaskWidth + cornerRadius,
- mTaskHeight + cornerRadius);
- mTouchRegion.union(mRightBottomCornerBounds);
+ // Apply the geometry to the touch region.
+ geometry.union(mTouchRegion);
+ mInputEventReceiver.setGeometry(geometry);
+ mInputEventReceiver.setTouchRegion(mTouchRegion);
try {
mWindowSession.updateInputChannel(
@@ -275,8 +197,9 @@ class DragResizeInputListener implements AutoCloseable {
e.rethrowFromSystemServer();
}
+ final Size taskSize = geometry.getTaskSize();
mSurfaceControlTransactionSupplier.get()
- .setWindowCrop(mInputSinkSurface, mTaskWidth, mTaskHeight)
+ .setWindowCrop(mInputSinkSurface, taskSize.getWidth(), taskSize.getHeight())
.apply();
// The touch region of the TaskInputSink should be the touch region of this
// DragResizeInputHandler minus the task bounds. Pilfering events isn't enough to prevent
@@ -289,21 +212,16 @@ class DragResizeInputListener implements AutoCloseable {
// issue. However, were there touchscreen-only a region out of the task bounds, mouse
// gestures will become no-op in that region, even though the mouse gestures may appear to
// be performed on the input window behind the resize handle.
- mTouchRegion.op(0, 0, mTaskWidth, mTaskHeight, Region.Op.DIFFERENCE);
+ mTouchRegion.op(0, 0, taskSize.getWidth(), taskSize.getHeight(), Region.Op.DIFFERENCE);
updateSinkInputChannel(mTouchRegion);
return true;
}
/**
- * Generate a Region that encapsulates all 4 corner handles
+ * Generate a Region that encapsulates all 4 corner handles and window edges.
*/
- Region getCornersRegion() {
- Region region = new Region();
- region.union(mLeftTopCornerBounds);
- region.union(mLeftBottomCornerBounds);
- region.union(mRightTopCornerBounds);
- region.union(mRightBottomCornerBounds);
- return region;
+ @NonNull Region getCornersRegion() {
+ return mInputEventReceiver.getCornersRegion();
}
private void updateSinkInputChannel(Region region) {
@@ -321,6 +239,10 @@ class DragResizeInputListener implements AutoCloseable {
}
}
+ boolean shouldHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) {
+ return mInputEventReceiver.shouldHandleEvent(e, offset);
+ }
+
boolean isHandlingDragResize() {
return mInputEventReceiver.isHandlingEvents();
}
@@ -346,19 +268,40 @@ class DragResizeInputListener implements AutoCloseable {
.apply();
}
- private class TaskResizeInputEventReceiver extends InputEventReceiver
- implements DragDetector.MotionEventHandler {
- private final Choreographer mChoreographer;
- private final Runnable mConsumeBatchEventRunnable;
+ private static class TaskResizeInputEventReceiver extends InputEventReceiver implements
+ DragDetector.MotionEventHandler {
+ @NonNull private final Context mContext;
+ private final InputManager mInputManager;
+ @NonNull private final InputChannel mInputChannel;
+ @NonNull private final DragPositioningCallback mCallback;
+ @NonNull private final Choreographer mChoreographer;
+ @NonNull private final Runnable mConsumeBatchEventRunnable;
+ @NonNull private final DragDetector mDragDetector;
+ @NonNull private final Supplier<Size> mDisplayLayoutSizeSupplier;
+ @NonNull private final Consumer<Region> mTouchRegionConsumer;
+ private final Rect mTmpRect = new Rect();
private boolean mConsumeBatchEventScheduled;
+ private DragResizeWindowGeometry mDragResizeWindowGeometry;
+ private Region mTouchRegion;
private boolean mShouldHandleEvents;
private int mLastCursorType = PointerIcon.TYPE_DEFAULT;
private Rect mDragStartTaskBounds;
- private final Rect mTmpRect = new Rect();
-
- private TaskResizeInputEventReceiver(
- InputChannel inputChannel, Handler handler, Choreographer choreographer) {
+ // 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,
+ @NonNull InputChannel inputChannel,
+ @NonNull DragPositioningCallback callback, @NonNull Handler handler,
+ @NonNull Choreographer choreographer,
+ @NonNull Supplier<Size> displayLayoutSizeSupplier,
+ @NonNull Consumer<Region> touchRegionConsumer) {
super(inputChannel, handler.getLooper());
+ mContext = context;
+ mInputManager = context.getSystemService(InputManager.class);
+ mInputChannel = inputChannel;
+ mCallback = callback;
mChoreographer = choreographer;
mConsumeBatchEventRunnable = () -> {
@@ -371,6 +314,48 @@ class DragResizeInputListener implements AutoCloseable {
scheduleConsumeBatchEvent();
}
};
+
+ mDragDetector = new DragDetector(this);
+ mDisplayLayoutSizeSupplier = displayLayoutSizeSupplier;
+ mTouchRegionConsumer = touchRegionConsumer;
+ }
+
+ /**
+ * Returns the geometry of the areas to drag resize.
+ */
+ DragResizeWindowGeometry getGeometry() {
+ return mDragResizeWindowGeometry;
+ }
+
+ /**
+ * Updates the geometry of the areas to drag resize.
+ */
+ void setGeometry(@NonNull DragResizeWindowGeometry dragResizeWindowGeometry) {
+ mDragResizeWindowGeometry = dragResizeWindowGeometry;
+ }
+
+ /**
+ * Sets how much slop to allow for touches.
+ */
+ void setTouchSlop(int touchSlop) {
+ mDragDetector.setTouchSlop(touchSlop);
+ }
+
+ /**
+ * Updates the region accepting input for drag resizing the task.
+ */
+ void setTouchRegion(@NonNull Region touchRegion) {
+ mTouchRegion = touchRegion;
+ }
+
+ /**
+ * Returns the union of all regions that can be touched for drag resizing; the corners and
+ * window edges.
+ */
+ @NonNull Region getCornersRegion() {
+ Region region = new Region();
+ mDragResizeWindowGeometry.union(region);
+ return region;
}
@Override
@@ -408,27 +393,31 @@ 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 = (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
- float x = e.getX(0);
- float y = e.getY(0);
- if (isTouch) {
- mShouldHandleEvents = isInCornerBounds(x, y);
- } else {
- mShouldHandleEvents = isInResizeHandleBounds(x, y);
- }
+ 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 = 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,
rawX, rawY);
// Increase the input sink region to cover the whole screen; this is to
// prevent input and focus from going to other tasks during a drag resize.
updateInputSinkRegionForDrag(mDragStartTaskBounds);
result = true;
+ } else {
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE,
+ "%s: Handling action down, but ignore event", TAG);
}
break;
}
@@ -437,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;
@@ -447,15 +443,21 @@ class DragResizeInputListener implements AutoCloseable {
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
- mInputManager.pilferPointers(mInputChannel.getToken());
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
// sink region. Otherwise, we should revert it here.
if (taskBounds.equals(mDragStartTaskBounds)) {
- updateSinkInputChannel(mTouchRegion);
+ mTouchRegionConsumer.accept(mTouchRegion);
}
}
mShouldHandleEvents = false;
@@ -480,125 +482,23 @@ class DragResizeInputListener implements AutoCloseable {
private void updateInputSinkRegionForDrag(Rect taskBounds) {
mTmpRect.set(taskBounds);
- final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId);
- final Region dragTouchRegion = new Region(-taskBounds.left,
- -taskBounds.top,
- -taskBounds.left + layout.width(),
- -taskBounds.top + layout.height());
+ final Size displayLayoutSize = mDisplayLayoutSizeSupplier.get();
+ final Region dragTouchRegion = new Region(-taskBounds.left, -taskBounds.top,
+ -taskBounds.left + displayLayoutSize.getWidth(),
+ -taskBounds.top + displayLayoutSize.getHeight());
// Remove the localized task bounds from the touch region.
mTmpRect.offsetTo(0, 0);
dragTouchRegion.op(mTmpRect, Region.Op.DIFFERENCE);
- updateSinkInputChannel(dragTouchRegion);
- }
-
- private boolean isInCornerBounds(float xf, float yf) {
- return calculateCornersCtrlType(xf, yf) != 0;
- }
-
- private boolean isInResizeHandleBounds(float x, float y) {
- return calculateResizeHandlesCtrlType(x, y) != 0;
- }
-
- @DragPositioningCallback.CtrlType
- private int calculateCtrlType(boolean isTouch, float x, float y) {
- if (isTouch) {
- return calculateCornersCtrlType(x, y);
- }
- return calculateResizeHandlesCtrlType(x, y);
- }
-
- @DragPositioningCallback.CtrlType
- private int calculateResizeHandlesCtrlType(float x, float y) {
- int ctrlType = 0;
- // mTaskCornerRadius is only used in comparing with corner regions. Comparisons with
- // sides will use the bounds specified in setGeometry and not go into task bounds.
- if (x < mTaskCornerRadius) {
- ctrlType |= CTRL_TYPE_LEFT;
- }
- if (x > mTaskWidth - mTaskCornerRadius) {
- ctrlType |= CTRL_TYPE_RIGHT;
- }
- if (y < mTaskCornerRadius) {
- ctrlType |= CTRL_TYPE_TOP;
- }
- if (y > mTaskHeight - mTaskCornerRadius) {
- ctrlType |= CTRL_TYPE_BOTTOM;
- }
- // Check distances from the center if it's in one of four corners.
- if ((ctrlType & (CTRL_TYPE_LEFT | CTRL_TYPE_RIGHT)) != 0
- && (ctrlType & (CTRL_TYPE_TOP | CTRL_TYPE_BOTTOM)) != 0) {
- return checkDistanceFromCenter(ctrlType, x, y);
- }
- // Otherwise, we should make sure we don't resize tasks inside task bounds.
- return (x < 0 || y < 0 || x >= mTaskWidth || y >= mTaskHeight) ? ctrlType : 0;
- }
-
- // If corner input is not within appropriate distance of corner radius, do not use it.
- // If input is not on a corner or is within valid distance, return ctrlType.
- @DragPositioningCallback.CtrlType
- private int checkDistanceFromCenter(@DragPositioningCallback.CtrlType int ctrlType,
- float x, float y) {
- int centerX;
- int centerY;
-
- // Determine center of rounded corner circle; this is simply the corner if radius is 0.
- switch (ctrlType) {
- case CTRL_TYPE_LEFT | CTRL_TYPE_TOP: {
- centerX = mTaskCornerRadius;
- centerY = mTaskCornerRadius;
- break;
- }
- case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM: {
- centerX = mTaskCornerRadius;
- centerY = mTaskHeight - mTaskCornerRadius;
- break;
- }
- case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP: {
- centerX = mTaskWidth - mTaskCornerRadius;
- centerY = mTaskCornerRadius;
- break;
- }
- case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM: {
- centerX = mTaskWidth - mTaskCornerRadius;
- centerY = mTaskHeight - mTaskCornerRadius;
- break;
- }
- default: {
- throw new IllegalArgumentException("ctrlType should be complex, but it's 0x"
- + Integer.toHexString(ctrlType));
- }
- }
- double distanceFromCenter = Math.hypot(x - centerX, y - centerY);
-
- if (distanceFromCenter < mTaskCornerRadius + mResizeHandleThickness
- && distanceFromCenter >= mTaskCornerRadius) {
- return ctrlType;
- }
- return 0;
- }
-
- @DragPositioningCallback.CtrlType
- private int calculateCornersCtrlType(float x, float y) {
- int xi = (int) x;
- int yi = (int) y;
- if (mLeftTopCornerBounds.contains(xi, yi)) {
- return CTRL_TYPE_LEFT | CTRL_TYPE_TOP;
- }
- if (mLeftBottomCornerBounds.contains(xi, yi)) {
- return CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM;
- }
- if (mRightTopCornerBounds.contains(xi, yi)) {
- return CTRL_TYPE_RIGHT | CTRL_TYPE_TOP;
- }
- if (mRightBottomCornerBounds.contains(xi, yi)) {
- return CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM;
- }
- return 0;
+ mTouchRegionConsumer.accept(dragTouchRegion);
}
private void updateCursorType(int displayId, int deviceId, int pointerId, float x,
float y) {
- @DragPositioningCallback.CtrlType int ctrlType = calculateResizeHandlesCtrlType(x, 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(/* isTouchscreen= */
+ false, /* isEdgeResizePermitted= */ true, x, y);
int cursorType = PointerIcon.TYPE_DEFAULT;
switch (ctrlType) {
@@ -629,14 +529,16 @@ class DragResizeInputListener implements AutoCloseable {
// where views in the task can receive input events because we can't set touch regions
// of input sinks to have rounded corners.
if (mLastCursorType != cursorType || cursorType != PointerIcon.TYPE_DEFAULT) {
- if (enablePointerChoreographer()) {
- mInputManager.setPointerIcon(PointerIcon.getSystemIcon(mContext, cursorType),
- displayId, deviceId, pointerId, mInputChannel.getToken());
- } else {
- mInputManager.setPointerIconType(cursorType);
- }
+ ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: update pointer icon from %d to %d",
+ TAG, mLastCursorType, cursorType);
+ mInputManager.setPointerIcon(PointerIcon.getSystemIcon(mContext, cursorType),
+ displayId, deviceId, pointerId, mInputChannel.getToken());
mLastCursorType = cursorType;
}
}
+
+ private boolean shouldHandleEvent(MotionEvent e, Point offset) {
+ return mDragResizeWindowGeometry.shouldHandleEvent(e, offset);
+ }
}
}
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
new file mode 100644
index 000000000000..b5d1d4a76342
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
@@ -0,0 +1,529 @@
+/*
+ * 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 static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+
+import static com.android.window.flags.Flags.enableWindowingEdgeDragResize;
+import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM;
+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.DragPositioningCallback.CTRL_TYPE_UNDEFINED;
+
+import android.annotation.NonNull;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.util.Size;
+import android.view.MotionEvent;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.wm.shell.R;
+
+import java.util.Objects;
+
+/**
+ * Geometry for a drag resize region for a particular window.
+ */
+final class DragResizeWindowGeometry {
+ // TODO(b/337264971) clean up when no longer needed
+ @VisibleForTesting static final boolean DEBUG = true;
+ // The additional width to apply to edge resize bounds just for logging when a touch is
+ // close.
+ @VisibleForTesting static final int EDGE_DEBUG_BUFFER = 15;
+ private final int mTaskCornerRadius;
+ private final Size mTaskSize;
+ // The size of the handle applied to the edges of the window, for the user to drag resize.
+ private final int mResizeHandleThickness;
+ // The task corners to permit drag resizing with a course input, such as touch.
+
+ private final @NonNull TaskCorners mLargeTaskCorners;
+ // The task corners to permit drag resizing with a fine input, such as stylus or cursor.
+ private final @NonNull TaskCorners mFineTaskCorners;
+ // The bounds for each edge drag region, which can resize the task in one direction.
+ private final @NonNull TaskEdges mTaskEdges;
+ // Extra-large edge bounds for logging to help debug when an edge resize is ignored.
+ private final @Nullable TaskEdges mDebugTaskEdges;
+
+ DragResizeWindowGeometry(int taskCornerRadius, @NonNull Size taskSize,
+ int resizeHandleThickness, int fineCornerSize, int largeCornerSize) {
+ mTaskCornerRadius = taskCornerRadius;
+ mTaskSize = taskSize;
+ mResizeHandleThickness = resizeHandleThickness;
+
+ mLargeTaskCorners = new TaskCorners(mTaskSize, largeCornerSize);
+ mFineTaskCorners = new TaskCorners(mTaskSize, fineCornerSize);
+
+ // Save touch areas for each edge.
+ mTaskEdges = new TaskEdges(mTaskSize, mResizeHandleThickness);
+ if (DEBUG) {
+ mDebugTaskEdges = new TaskEdges(mTaskSize, mResizeHandleThickness + EDGE_DEBUG_BUFFER);
+ } else {
+ mDebugTaskEdges = null;
+ }
+ }
+
+ /**
+ * Returns the resource value to use for the resize handle on the edge of the window.
+ */
+ static int getResizeEdgeHandleSize(@NonNull Resources res) {
+ return enableWindowingEdgeDragResize()
+ ? res.getDimensionPixelSize(R.dimen.desktop_mode_edge_handle)
+ : res.getDimensionPixelSize(R.dimen.freeform_resize_handle);
+ }
+
+ /**
+ * Returns the resource value to use for course input, such as touch, that benefits from a large
+ * square on each of the window's corners.
+ */
+ static int getLargeResizeCornerSize(@NonNull Resources res) {
+ return res.getDimensionPixelSize(R.dimen.desktop_mode_corner_resize_large);
+ }
+
+ /**
+ * Returns the resource value to use for fine input, such as stylus, that can use a smaller
+ * square on each of the window's corners.
+ */
+ static int getFineResizeCornerSize(@NonNull Resources res) {
+ return res.getDimensionPixelSize(R.dimen.freeform_resize_corner);
+ }
+
+ /**
+ * Returns the size of the task this geometry is calculated for.
+ */
+ @NonNull Size getTaskSize() {
+ // Safe to return directly since size is immutable.
+ return mTaskSize;
+ }
+
+ /**
+ * Returns the union of all regions that can be touched for drag resizing; the corners window
+ * and window edges.
+ */
+ void union(@NonNull Region region) {
+ // Apply the edge resize regions.
+ if (inDebugMode()) {
+ // Use the larger edge sizes if we are debugging, to be able to log if we ignored a
+ // touch due to the size of the edge region.
+ mDebugTaskEdges.union(region);
+ } else {
+ mTaskEdges.union(region);
+ }
+
+ if (enableWindowingEdgeDragResize()) {
+ // Apply the corners as well for the larger corners, to ensure we capture all possible
+ // touches.
+ mLargeTaskCorners.union(region);
+ } else {
+ // Only apply fine corners for the legacy approach.
+ mFineTaskCorners.union(region);
+ }
+ }
+
+ /**
+ * Returns if this MotionEvent should be handled, based on its source and position.
+ */
+ boolean shouldHandleEvent(@NonNull MotionEvent e, @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 = isEventFromTouchscreen(e)
+ ? isInCornerBounds(mLargeTaskCorners, x, y)
+ : isInCornerBounds(mFineTaskCorners, x, y);
+ // 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 isEventFromTouchscreen(e)
+ ? isInCornerBounds(mFineTaskCorners, x, y)
+ : isInEdgeResizeBounds(x, y);
+ }
+ }
+
+ 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;
+ }
+
+ private boolean isInEdgeResizeBounds(float x, float y) {
+ return calculateEdgeResizeCtrlType(x, y) != 0;
+ }
+
+ /**
+ * 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 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 = 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 && isEdgeResizePermitted) {
+ ctrlType = calculateEdgeResizeCtrlType(x, y);
+ }
+ return ctrlType;
+ } else {
+ // Legacy uses only fine corners for touch, and edges only for non-touch input.
+ return isTouchscreen
+ ? mFineTaskCorners.calculateCornersCtrlType(x, y)
+ : calculateEdgeResizeCtrlType(x, y);
+ }
+ }
+
+ @DragPositioningCallback.CtrlType
+ private int calculateEdgeResizeCtrlType(float x, float y) {
+ if (inDebugMode() && (mDebugTaskEdges.contains((int) x, (int) y)
+ && !mTaskEdges.contains((int) x, (int) y))) {
+ return CTRL_TYPE_UNDEFINED;
+ }
+ int ctrlType = CTRL_TYPE_UNDEFINED;
+ // mTaskCornerRadius is only used in comparing with corner regions. Comparisons with
+ // sides will use the bounds specified in setGeometry and not go into task bounds.
+ if (x < mTaskCornerRadius) {
+ ctrlType |= CTRL_TYPE_LEFT;
+ }
+ if (x > mTaskSize.getWidth() - mTaskCornerRadius) {
+ ctrlType |= CTRL_TYPE_RIGHT;
+ }
+ if (y < mTaskCornerRadius) {
+ ctrlType |= CTRL_TYPE_TOP;
+ }
+ if (y > mTaskSize.getHeight() - mTaskCornerRadius) {
+ ctrlType |= CTRL_TYPE_BOTTOM;
+ }
+ // If the touch is within one of the four corners, check if it is within the bounds of the
+ // // handle.
+ if ((ctrlType & (CTRL_TYPE_LEFT | CTRL_TYPE_RIGHT)) != 0
+ && (ctrlType & (CTRL_TYPE_TOP | CTRL_TYPE_BOTTOM)) != 0) {
+ return checkDistanceFromCenter(ctrlType, x, y);
+ }
+ // Otherwise, we should make sure we don't resize tasks inside task bounds.
+ return (x < 0 || y < 0 || x >= mTaskSize.getWidth() || y >= mTaskSize.getHeight())
+ ? ctrlType : CTRL_TYPE_UNDEFINED;
+ }
+
+ /**
+ * Return {@code ctrlType} if the corner input is outside the (potentially rounded) corner of
+ * the task, and within the thickness of the resize handle. Otherwise, return 0.
+ */
+ @DragPositioningCallback.CtrlType
+ private int checkDistanceFromCenter(@DragPositioningCallback.CtrlType int ctrlType, float x,
+ float y) {
+ final Point cornerRadiusCenter = calculateCenterForCornerRadius(ctrlType);
+ double distanceFromCenter = Math.hypot(x - cornerRadiusCenter.x, y - cornerRadiusCenter.y);
+
+ if (distanceFromCenter < mTaskCornerRadius + mResizeHandleThickness
+ && distanceFromCenter >= mTaskCornerRadius) {
+ return ctrlType;
+ }
+ return CTRL_TYPE_UNDEFINED;
+ }
+
+ /**
+ * Returns center of rounded corner circle; this is simply the corner if radius is 0.
+ */
+ private Point calculateCenterForCornerRadius(@DragPositioningCallback.CtrlType int ctrlType) {
+ int centerX;
+ int centerY;
+
+ switch (ctrlType) {
+ case CTRL_TYPE_LEFT | CTRL_TYPE_TOP: {
+ centerX = mTaskCornerRadius;
+ centerY = mTaskCornerRadius;
+ break;
+ }
+ case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM: {
+ centerX = mTaskCornerRadius;
+ centerY = mTaskSize.getHeight() - mTaskCornerRadius;
+ break;
+ }
+ case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP: {
+ centerX = mTaskSize.getWidth() - mTaskCornerRadius;
+ centerY = mTaskCornerRadius;
+ break;
+ }
+ case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM: {
+ centerX = mTaskSize.getWidth() - mTaskCornerRadius;
+ centerY = mTaskSize.getHeight() - mTaskCornerRadius;
+ break;
+ }
+ default: {
+ throw new IllegalArgumentException(
+ "ctrlType should be complex, but it's 0x" + Integer.toHexString(ctrlType));
+ }
+ }
+ return new Point(centerX, centerY);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) return false;
+ if (this == obj) return true;
+ if (!(obj instanceof DragResizeWindowGeometry other)) return false;
+
+ return this.mTaskCornerRadius == other.mTaskCornerRadius
+ && this.mTaskSize.equals(other.mTaskSize)
+ && this.mResizeHandleThickness == other.mResizeHandleThickness
+ && this.mFineTaskCorners.equals(other.mFineTaskCorners)
+ && this.mLargeTaskCorners.equals(other.mLargeTaskCorners)
+ && (inDebugMode()
+ ? this.mDebugTaskEdges.equals(other.mDebugTaskEdges)
+ : this.mTaskEdges.equals(other.mTaskEdges));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mTaskCornerRadius,
+ mTaskSize,
+ mResizeHandleThickness,
+ mFineTaskCorners,
+ mLargeTaskCorners,
+ (inDebugMode() ? mDebugTaskEdges : mTaskEdges));
+ }
+
+ private boolean inDebugMode() {
+ return DEBUG && mDebugTaskEdges != null;
+ }
+
+ /**
+ * Representation of the drag resize regions at the corner of the window.
+ */
+ private static class TaskCorners {
+ // The size of the square applied to the corners of the window, for the user to drag
+ // resize.
+ private final int mCornerSize;
+ // The square for each corner.
+ private final @NonNull Rect mLeftTopCornerBounds;
+ private final @NonNull Rect mRightTopCornerBounds;
+ private final @NonNull Rect mLeftBottomCornerBounds;
+ private final @NonNull Rect mRightBottomCornerBounds;
+
+ TaskCorners(@NonNull Size taskSize, int cornerSize) {
+ mCornerSize = cornerSize;
+ final int cornerRadius = cornerSize / 2;
+ mLeftTopCornerBounds = new Rect(
+ -cornerRadius,
+ -cornerRadius,
+ cornerRadius,
+ cornerRadius);
+
+ mRightTopCornerBounds = new Rect(
+ taskSize.getWidth() - cornerRadius,
+ -cornerRadius,
+ taskSize.getWidth() + cornerRadius,
+ cornerRadius);
+
+ mLeftBottomCornerBounds = new Rect(
+ -cornerRadius,
+ taskSize.getHeight() - cornerRadius,
+ cornerRadius,
+ taskSize.getHeight() + cornerRadius);
+
+ mRightBottomCornerBounds = new Rect(
+ taskSize.getWidth() - cornerRadius,
+ taskSize.getHeight() - cornerRadius,
+ taskSize.getWidth() + cornerRadius,
+ taskSize.getHeight() + cornerRadius);
+ }
+
+ /**
+ * Updates the region to include all four corners.
+ */
+ void union(Region region) {
+ region.union(mLeftTopCornerBounds);
+ region.union(mRightTopCornerBounds);
+ region.union(mLeftBottomCornerBounds);
+ region.union(mRightBottomCornerBounds);
+ }
+
+ /**
+ * Returns the control type based on the position of the {@code MotionEvent}'s coordinates.
+ */
+ @DragPositioningCallback.CtrlType
+ int calculateCornersCtrlType(float x, float y) {
+ int xi = (int) x;
+ int yi = (int) y;
+ if (mLeftTopCornerBounds.contains(xi, yi)) {
+ return CTRL_TYPE_LEFT | CTRL_TYPE_TOP;
+ }
+ if (mLeftBottomCornerBounds.contains(xi, yi)) {
+ return CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM;
+ }
+ if (mRightTopCornerBounds.contains(xi, yi)) {
+ return CTRL_TYPE_RIGHT | CTRL_TYPE_TOP;
+ }
+ if (mRightBottomCornerBounds.contains(xi, yi)) {
+ return CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM;
+ }
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "TaskCorners of size " + mCornerSize + " for the"
+ + " top left " + mLeftTopCornerBounds
+ + " top right " + mRightTopCornerBounds
+ + " bottom left " + mLeftBottomCornerBounds
+ + " bottom right " + mRightBottomCornerBounds;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) return false;
+ if (this == obj) return true;
+ if (!(obj instanceof TaskCorners other)) return false;
+
+ return this.mCornerSize == other.mCornerSize
+ && this.mLeftTopCornerBounds.equals(other.mLeftTopCornerBounds)
+ && this.mRightTopCornerBounds.equals(other.mRightTopCornerBounds)
+ && this.mLeftBottomCornerBounds.equals(other.mLeftBottomCornerBounds)
+ && this.mRightBottomCornerBounds.equals(other.mRightBottomCornerBounds);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mCornerSize,
+ mLeftTopCornerBounds,
+ mRightTopCornerBounds,
+ mLeftBottomCornerBounds,
+ mRightBottomCornerBounds);
+ }
+ }
+
+ /**
+ * Representation of the drag resize regions at the edges of the window.
+ */
+ private static class TaskEdges {
+ private final @NonNull Rect mTopEdgeBounds;
+ private final @NonNull Rect mLeftEdgeBounds;
+ private final @NonNull Rect mRightEdgeBounds;
+ private final @NonNull Rect mBottomEdgeBounds;
+ private final @NonNull Region mRegion;
+
+ private TaskEdges(@NonNull Size taskSize, int resizeHandleThickness) {
+ // Save touch areas for each edge.
+ mTopEdgeBounds = new Rect(
+ -resizeHandleThickness,
+ -resizeHandleThickness,
+ taskSize.getWidth() + resizeHandleThickness,
+ 0);
+ mLeftEdgeBounds = new Rect(
+ -resizeHandleThickness,
+ 0,
+ 0,
+ taskSize.getHeight());
+ mRightEdgeBounds = new Rect(
+ taskSize.getWidth(),
+ 0,
+ taskSize.getWidth() + resizeHandleThickness,
+ taskSize.getHeight());
+ mBottomEdgeBounds = new Rect(
+ -resizeHandleThickness,
+ taskSize.getHeight(),
+ taskSize.getWidth() + resizeHandleThickness,
+ taskSize.getHeight() + resizeHandleThickness);
+
+ mRegion = new Region();
+ mRegion.union(mTopEdgeBounds);
+ mRegion.union(mLeftEdgeBounds);
+ mRegion.union(mRightEdgeBounds);
+ mRegion.union(mBottomEdgeBounds);
+ }
+
+ /**
+ * Returns {@code true} if the edges contain the given point.
+ */
+ private boolean contains(int x, int y) {
+ return mRegion.contains(x, y);
+ }
+
+ /**
+ * Updates the region to include all four corners.
+ */
+ private void union(Region region) {
+ region.union(mTopEdgeBounds);
+ region.union(mLeftEdgeBounds);
+ region.union(mRightEdgeBounds);
+ region.union(mBottomEdgeBounds);
+ }
+
+ @Override
+ public String toString() {
+ return "TaskEdges for the"
+ + " top " + mTopEdgeBounds
+ + " left " + mLeftEdgeBounds
+ + " right " + mRightEdgeBounds
+ + " bottom " + mBottomEdgeBounds;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) return false;
+ if (this == obj) return true;
+ if (!(obj instanceof TaskEdges other)) return false;
+
+ return this.mTopEdgeBounds.equals(other.mTopEdgeBounds)
+ && this.mLeftEdgeBounds.equals(other.mLeftEdgeBounds)
+ && this.mRightEdgeBounds.equals(other.mRightEdgeBounds)
+ && this.mBottomEdgeBounds.equals(other.mBottomEdgeBounds);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mTopEdgeBounds,
+ mLeftEdgeBounds,
+ mRightEdgeBounds,
+ mBottomEdgeBounds);
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
index 6bfc7cdcb33e..76096b0c59f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java
@@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor;
import static android.view.WindowManager.TRANSIT_CHANGE;
+import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.IBinder;
@@ -60,9 +61,6 @@ class FluidResizeTaskPositioner implements DragPositioningCallback,
private final Rect mTaskBoundsAtDragStart = new Rect();
private final PointF mRepositionStartPoint = new PointF();
private final Rect mRepositionTaskBounds = new Rect();
- // If a task move (not resize) finishes with the positions y less than this value, do not
- // finalize the bounds there using WCT#setBounds
- private final int mDisallowedAreaForEndBoundsHeight;
private boolean mHasDragResized;
private boolean mIsResizingOrAnimatingResize;
private int mCtrlType;
@@ -70,11 +68,9 @@ class FluidResizeTaskPositioner implements DragPositioningCallback,
@Surface.Rotation private int mRotation;
FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, Transitions transitions,
- WindowDecoration windowDecoration, DisplayController displayController,
- int disallowedAreaForEndBoundsHeight) {
+ WindowDecoration windowDecoration, DisplayController displayController) {
this(taskOrganizer, transitions, windowDecoration, displayController,
- dragStartListener -> {}, SurfaceControl.Transaction::new,
- disallowedAreaForEndBoundsHeight);
+ dragStartListener -> {}, SurfaceControl.Transaction::new);
}
FluidResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
@@ -82,15 +78,13 @@ class FluidResizeTaskPositioner implements DragPositioningCallback,
WindowDecoration windowDecoration,
DisplayController displayController,
DragPositioningCallbackUtility.DragStartListener dragStartListener,
- Supplier<SurfaceControl.Transaction> supplier,
- int disallowedAreaForEndBoundsHeight) {
+ Supplier<SurfaceControl.Transaction> supplier) {
mTaskOrganizer = taskOrganizer;
mTransitions = transitions;
mWindowDecoration = windowDecoration;
mDisplayController = displayController;
mDragStartListener = dragStartListener;
mTransactionSupplier = supplier;
- mDisallowedAreaForEndBoundsHeight = disallowedAreaForEndBoundsHeight;
}
@Override
@@ -157,14 +151,10 @@ class FluidResizeTaskPositioner implements DragPositioningCallback,
wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
}
mDragResizeEndTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
- } else if (mCtrlType == CTRL_TYPE_UNDEFINED
- && DragPositioningCallbackUtility.isBelowDisallowedArea(
- mDisallowedAreaForEndBoundsHeight, mTaskBoundsAtDragStart, mRepositionStartPoint,
- y)) {
+ } else if (mCtrlType == CTRL_TYPE_UNDEFINED) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
- DragPositioningCallbackUtility.onDragEnd(mRepositionTaskBounds,
- mTaskBoundsAtDragStart, mRepositionStartPoint, x, y,
- mWindowDecoration.calculateValidDragArea());
+ DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds,
+ mTaskBoundsAtDragStart, mRepositionStartPoint, x, y);
wct.setBounds(mWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
}
@@ -189,10 +179,11 @@ class FluidResizeTaskPositioner implements DragPositioningCallback,
for (TransitionInfo.Change change: info.getChanges()) {
final SurfaceControl sc = change.getLeash();
final Rect endBounds = change.getEndAbsBounds();
+ final Point endPosition = change.getEndRelOffset();
startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
- .setPosition(sc, endBounds.left, endBounds.top);
+ .setPosition(sc, endPosition.x, endPosition.y);
finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
- .setPosition(sc, endBounds.left, endBounds.top);
+ .setPosition(sc, endPosition.x, endPosition.y);
}
startTransaction.apply();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt
new file mode 100644
index 000000000000..b21c3f522eab
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.animation.ValueAnimator
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.ImageButton
+
+/**
+ * [ImageButton] for the handle at the top of fullscreen apps. Has custom hover
+ * and press handling to grow the handle on hover enter and shrink the handle on
+ * hover exit and press.
+ */
+class HandleImageButton (context: Context?, attrs: AttributeSet?) :
+ ImageButton(context, attrs) {
+ private val handleAnimator = ValueAnimator()
+
+ override fun onHoverChanged(hovered: Boolean) {
+ super.onHoverChanged(hovered)
+ if (hovered) {
+ animateHandle(HANDLE_HOVER_ANIM_DURATION, HANDLE_HOVER_ENTER_SCALE)
+ } else {
+ if (!isPressed) {
+ animateHandle(HANDLE_HOVER_ANIM_DURATION, HANDLE_DEFAULT_SCALE)
+ }
+ }
+ }
+
+ override fun setPressed(pressed: Boolean) {
+ if (isPressed != pressed) {
+ super.setPressed(pressed)
+ if (pressed) {
+ animateHandle(HANDLE_PRESS_ANIM_DURATION, HANDLE_PRESS_DOWN_SCALE)
+ } else {
+ animateHandle(HANDLE_PRESS_ANIM_DURATION, HANDLE_DEFAULT_SCALE)
+ }
+ }
+ }
+
+ private fun animateHandle(duration: Long, endScale: Float) {
+ if (handleAnimator.isRunning) {
+ handleAnimator.cancel()
+ }
+ handleAnimator.duration = duration
+ handleAnimator.setFloatValues(scaleX, endScale)
+ handleAnimator.addUpdateListener { animator ->
+ scaleX = animator.animatedValue as Float
+ }
+ handleAnimator.start()
+ }
+
+ companion object {
+ /** The duration of animations related to hover state. **/
+ private const val HANDLE_HOVER_ANIM_DURATION = 300L
+ /** The duration of animations related to pressed state. **/
+ private const val HANDLE_PRESS_ANIM_DURATION = 200L
+ /** Ending scale for hover enter. **/
+ private const val HANDLE_HOVER_ENTER_SCALE = 1.2f
+ /** Ending scale for press down. **/
+ private const val HANDLE_PRESS_DOWN_SCALE = 0.85f
+ /** Default scale for handle. **/
+ private const val HANDLE_DEFAULT_SCALE = 1f
+ }
+}
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 b37dd0d6fd2d..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
@@ -20,6 +20,11 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.view.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;
@@ -31,17 +36,26 @@ 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;
-import android.widget.Button;
import android.widget.ImageButton;
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.
@@ -53,16 +67,27 @@ import com.android.wm.shell.R;
*/
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;
@@ -72,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;
@@ -93,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);
}
@@ -127,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) {
@@ -140,10 +176,12 @@ class HandleMenu {
* Set up interactive elements of handle menu's app info pill.
*/
private void setupAppInfoPill(View handleMenu) {
- final ImageButton collapseBtn = handleMenu.findViewById(R.id.collapse_menu_button);
+ final HandleMenuImageButton collapseBtn =
+ handleMenu.findViewById(R.id.collapse_menu_button);
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);
}
@@ -172,9 +210,9 @@ class HandleMenu {
final ColorStateList activeColorStateList = iconColors[1];
final int windowingMode = mTaskInfo.getWindowingMode();
fullscreenBtn.setImageTintList(windowingMode == WINDOWING_MODE_FULLSCREEN
- ? activeColorStateList : inActiveColorStateList);
+ ? activeColorStateList : inActiveColorStateList);
splitscreenBtn.setImageTintList(windowingMode == WINDOWING_MODE_MULTI_WINDOW
- ? activeColorStateList : inActiveColorStateList);
+ ? activeColorStateList : inActiveColorStateList);
floatingBtn.setImageTintList(windowingMode == WINDOWING_MODE_PINNED
? activeColorStateList : inActiveColorStateList);
desktopBtn.setImageTintList(windowingMode == WINDOWING_MODE_FREEFORM
@@ -185,11 +223,9 @@ class HandleMenu {
* Set up interactive elements & height of handle menu's more actions pill
*/
private void setupMoreActionsPill(View handleMenu) {
- final Button selectBtn = handleMenu.findViewById(R.id.select_button);
- selectBtn.setOnClickListener(mOnClickListener);
- final Button screenshotBtn = handleMenu.findViewById(R.id.screenshot_button);
- // TODO: Remove once implemented.
- screenshotBtn.setVisibility(View.GONE);
+ if (!SHOULD_SHOW_MORE_ACTIONS_PILL) {
+ handleMenu.findViewById(R.id.more_actions_pill).setVisibility(View.GONE);
+ }
}
/**
@@ -214,55 +250,105 @@ 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);
}
}
/**
- * Check a passed MotionEvent if a click has occurred on any button on this caption
- * Note this should only be called when a regular onClick is not possible
+ * Check a passed MotionEvent if a click or hover has occurred on any button on this caption
+ * Note this should only be called when a regular onClick/onHover is not possible
* (i.e. the button was clicked through status bar layer)
*
* @param ev the MotionEvent to compare against.
*/
- void checkClickEvent(MotionEvent ev) {
- final View handleMenu = mHandleMenuWindow.mWindowViewHost.getView();
- final ImageButton collapse = handleMenu.findViewById(R.id.collapse_menu_button);
- // Translate the input point from display coordinates to the same space as the collapse
- // button, meaning its parent (app info pill view).
- final PointF inputPoint = new PointF(ev.getX() - mHandleMenuPosition.x,
- ev.getY() - mHandleMenuPosition.y);
- if (pointInView(collapse, inputPoint.x, inputPoint.y)) {
- mOnClickListener.onClick(collapse);
+ void checkMotionEvent(MotionEvent ev) {
+ // 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);
+ final int action = ev.getActionMasked();
+ collapse.setHovered(inputInCollapseButton && action != ACTION_UP);
+ collapse.setPressed(inputInCollapseButton && action == ACTION_DOWN);
+ if (action == ACTION_UP && inputInCollapseButton) {
+ collapse.performClick();
}
}
+ 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,
+ ev.getY() - mHandleMenuPosition.y);
+ }
+
/**
* A valid menu input is one of the following:
* An input that happens in the menu views.
@@ -272,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) {
@@ -287,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() {
@@ -305,12 +414,15 @@ class HandleMenu {
* Determines handle menu height based on if windowing pill should be shown.
*/
private int getHandleMenuHeight(Resources resources) {
- int menuHeight = loadDimensionPixelSize(resources,
- R.dimen.desktop_mode_handle_menu_height);
+ int menuHeight = loadDimensionPixelSize(resources, R.dimen.desktop_mode_handle_menu_height);
if (!mShouldShowWindowingPill) {
menuHeight -= loadDimensionPixelSize(resources,
R.dimen.desktop_mode_handle_menu_windowing_pill_height);
}
+ if (!SHOULD_SHOW_MORE_ACTIONS_PILL) {
+ menuHeight -= loadDimensionPixelSize(resources,
+ R.dimen.desktop_mode_handle_menu_more_actions_pill_height);
+ }
return menuHeight;
}
@@ -323,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) {
@@ -335,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;
@@ -343,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;
}
@@ -384,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
new file mode 100644
index 000000000000..18757ef6ff40
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.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
+import android.widget.ImageButton
+
+/**
+ * A custom [ImageButton] for buttons inside handle menu that intentionally doesn't handle hovers.
+ * 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) {
+ lateinit var taskInfo: RunningTaskInfo
+
+ override fun onHoverEvent(motionEvent: MotionEvent): Boolean {
+ if (Flags.enableAdditionalWindowsAboveStatusBar() || taskInfo.isFreeform) {
+ return super.onHoverEvent(motionEvent)
+ } else {
+ return false
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
index b2f8cfdbfb7a..4f049015af99 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt
@@ -20,6 +20,8 @@ import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.drawable.RippleDrawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
@@ -29,6 +31,7 @@ import android.widget.ProgressBar
import androidx.core.animation.doOnEnd
import androidx.core.animation.doOnStart
import androidx.core.content.ContextCompat
+import com.android.window.flags.Flags
import com.android.wm.shell.R
private const val OPEN_MAXIMIZE_MENU_DELAY_ON_HOVER_MS = 350
@@ -85,22 +88,51 @@ class MaximizeButtonView(
}
fun cancelHoverAnimation() {
- hoverProgressAnimatorSet.removeAllListeners()
+ hoverProgressAnimatorSet.childAnimations.forEach { it.removeAllListeners() }
hoverProgressAnimatorSet.cancel()
progressBar.visibility = View.INVISIBLE
}
- fun setAnimationTints(darkMode: Boolean) {
- if (darkMode) {
- progressBar.progressTintList = ColorStateList.valueOf(
+ /**
+ * Set the color tints of the maximize button views.
+ *
+ * @param darkMode whether the app's theme is in dark mode.
+ * @param iconForegroundColor the color tint to use for the maximize icon to match the rest of
+ * the App Header icons
+ * @param baseForegroundColor the base foreground color tint used by the App Header, used to style
+ * views within this button using the same base color but with different opacities.
+ */
+ fun setAnimationTints(
+ darkMode: Boolean,
+ iconForegroundColor: ColorStateList? = null,
+ baseForegroundColor: Int? = null,
+ rippleDrawable: RippleDrawable? = null
+ ) {
+ if (Flags.enableThemedAppHeaders()) {
+ requireNotNull(iconForegroundColor) { "Icon foreground color must be non-null" }
+ requireNotNull(baseForegroundColor) { "Base foreground color must be non-null" }
+ requireNotNull(rippleDrawable) { "Ripple drawable must be non-null" }
+ maximizeWindow.imageTintList = iconForegroundColor
+ maximizeWindow.background = rippleDrawable
+ progressBar.progressTintList = ColorStateList.valueOf(baseForegroundColor)
+ .withAlpha(OPACITY_15)
+ progressBar.progressBackgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT)
+ } else {
+ if (darkMode) {
+ progressBar.progressTintList = ColorStateList.valueOf(
resources.getColor(R.color.desktop_mode_maximize_menu_progress_dark))
- maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context,
+ maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context,
R.color.desktop_mode_caption_button_color_selector_dark))
- } else {
- progressBar.progressTintList = ColorStateList.valueOf(
+ } else {
+ progressBar.progressTintList = ColorStateList.valueOf(
resources.getColor(R.color.desktop_mode_maximize_menu_progress_light))
- maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context,
+ maximizeWindow.background?.setTintList(ContextCompat.getColorStateList(context,
R.color.desktop_mode_caption_button_color_selector_light))
+ }
}
}
+
+ companion object {
+ private const val OPACITY_15 = 38
+ }
}
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 b82f7ca47ef3..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
@@ -16,29 +16,55 @@
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
+import android.view.View.SCALE_Y
+import android.view.View.TRANSLATION_Y
+import android.view.View.TRANSLATION_Z
import android.view.WindowManager
import android.view.WindowlessWindowManager
import android.widget.Button
+import android.widget.TextView
import android.window.TaskConstants
+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
@@ -58,17 +84,16 @@ 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 shadowRadius = loadDimensionPixelSize(
- R.dimen.desktop_mode_maximize_menu_shadow_radius
- ).toFloat()
private val cornerRadius = loadDimensionPixelSize(
R.dimen.desktop_mode_maximize_menu_corner_radius
).toFloat()
private val menuWidth = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_width)
private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height)
+ private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding)
/** Position the menu relative to the caption's position. */
fun positionMenu(position: PointF, t: Transaction) {
@@ -80,22 +105,20 @@ class MaximizeMenu(
fun show() {
if (maximizeMenu != null) return
createMaximizeMenu()
- setupMaximizeMenu()
+ maximizeMenuView?.animateOpenMenu()
}
/** Closes the maximize window and releases its view. */
fun close() {
+ 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
@@ -119,16 +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)
- .setWindowCrop(leash, menuWidth, menuHeight)
- .setShadowRadius(leash, shadowRadius)
.setCornerRadius(leash, cornerRadius)
.show(leash)
- maximizeMenu = AdditionalWindow(leash, viewHost, transactionSupplier)
+ maximizeMenu =
+ AdditionalViewHostViewContainer(leash, viewHost, transactionSupplier)
syncQueue.runInSync { transaction ->
transaction.merge(t)
@@ -144,31 +176,6 @@ class MaximizeMenu(
}
}
- private fun setupMaximizeMenu() {
- val maximizeMenuView = maximizeMenu?.mWindowViewHost?.view ?: return
-
- maximizeMenuView.setOnGenericMotionListener(onGenericMotionListener)
- maximizeMenuView.setOnTouchListener(onTouchListener)
-
- val maximizeButton = maximizeMenuView.requireViewById<Button>(
- R.id.maximize_menu_maximize_button
- )
- maximizeButton.setOnClickListener(onClickListener)
- maximizeButton.setOnGenericMotionListener(onGenericMotionListener)
-
- val snapRightButton = maximizeMenuView.requireViewById<Button>(
- R.id.maximize_menu_snap_right_button
- )
- snapRightButton.setOnClickListener(onClickListener)
- snapRightButton.setOnGenericMotionListener(onGenericMotionListener)
-
- val snapLeftButton = maximizeMenuView.requireViewById<Button>(
- R.id.maximize_menu_snap_left_button
- )
- snapLeftButton.setOnClickListener(onClickListener)
- snapLeftButton.setOnGenericMotionListener(onGenericMotionListener)
- }
-
/**
* A valid menu input is one of the following:
* An input that happens in the menu views.
@@ -187,14 +194,457 @@ 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 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.
+ maximizeMenuView?.updateSplitSnapSelection(MaximizeMenuView.SnapToHalfSelection.NONE)
+ }
+ }
+
+ private fun setSnapButtonsColorOnHover(viewId: Int, ev: MotionEvent) {
+ 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
+ }
}
companion object {
+ // Open menu animation constants
+ private const val ALPHA_ANIMATION_DURATION_MS = 50L
+ private const val MAX_DRAWABLE_ALPHA_VALUE = 255
+ private const val STARTING_MENU_HEIGHT_SCALE = 0.8f
+ private const val MENU_HEIGHT_ANIMATION_DURATION_MS = 300L
+ private const val ELEVATION_ANIMATION_DURATION_MS = 50L
+ private const val CONTROLS_ALPHA_ANIMATION_DELAY_MS = 33L
+ private const val MENU_Z_TRANSLATION = 1f
fun isMaximizeMenuView(@IdRes viewId: Int): Boolean {
- return viewId == R.id.maximize_menu || viewId == R.id.maximize_menu_maximize_button ||
+ return viewId == R.id.maximize_menu ||
+ viewId == R.id.maximize_menu_maximize_button ||
viewId == R.id.maximize_menu_snap_left_button ||
- viewId == R.id.maximize_menu_snap_right_button
+ viewId == R.id.maximize_menu_snap_right_button ||
+ viewId == R.id.maximize_menu_snap_menu_layout ||
+ viewId == R.id.maximize_menu_snap_menu_layout
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
index af055230b629..974166700203 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt
@@ -7,7 +7,7 @@ import android.graphics.PointF
import android.graphics.Rect
import android.view.MotionEvent
import android.view.SurfaceControl
-import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.wm.shell.R
/**
* Creates an animator to shrink and position task after a user drags a fullscreen task from
@@ -31,17 +31,24 @@ class MoveToDesktopAnimator @JvmOverloads constructor(
private val animatedTaskWidth
get() = dragToDesktopAnimator.animatedValue as Float * startBounds.width()
+ val scale: Float
+ get() = dragToDesktopAnimator.animatedValue as Float
+ private val mostRecentInput = PointF()
private val dragToDesktopAnimator: ValueAnimator = ValueAnimator.ofFloat(1f,
DRAG_FREEFORM_SCALE)
.setDuration(ANIMATION_DURATION.toLong())
.apply {
val t = SurfaceControl.Transaction()
- val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
- addUpdateListener { animation ->
- val animatorValue = animation.animatedValue as Float
- t.setScale(taskSurface, animatorValue, animatorValue)
- .setCornerRadius(taskSurface, cornerRadius)
- .apply()
+ val cornerRadius = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_dragged_task_radius).toFloat()
+ addUpdateListener {
+ setTaskPosition(mostRecentInput.x, mostRecentInput.y)
+ t.setScale(taskSurface, scale, scale)
+ .setCornerRadius(taskSurface, cornerRadius)
+ .setScale(taskSurface, scale, scale)
+ .setCornerRadius(taskSurface, cornerRadius)
+ .setPosition(taskSurface, position.x, position.y)
+ .apply()
}
}
@@ -77,22 +84,31 @@ class MoveToDesktopAnimator @JvmOverloads constructor(
// allow dragging beyond its stage across any region of the display. Because of that, the
// rawX/Y are more true to where the gesture is on screen and where the surface should be
// positioned.
- position.x = ev.rawX - animatedTaskWidth / 2
- position.y = ev.rawY
+ mostRecentInput.set(ev.rawX, ev.rawY)
- if (!allowSurfaceChangesOnMove) {
+ // If animator is running, allow it to set scale and position at the same time.
+ if (!allowSurfaceChangesOnMove || dragToDesktopAnimator.isRunning) {
return
}
-
+ setTaskPosition(ev.rawX, ev.rawY)
val t = transactionFactory()
t.setPosition(taskSurface, position.x, position.y)
t.apply()
}
/**
- * Ends the animation, setting the scale and position to the final animation value
+ * Calculates the top left corner of task from input coordinates.
+ * Top left will be needed for the resulting surface control transaction.
+ */
+ private fun setTaskPosition(x: Float, y: Float) {
+ position.x = x - animatedTaskWidth / 2
+ position.y = y
+ }
+
+ /**
+ * Cancels the animation, intended to be used when another animator will take over.
*/
- fun endAnimator() {
- dragToDesktopAnimator.end()
+ fun cancelAnimator() {
+ dragToDesktopAnimator.cancel()
}
} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
deleted file mode 100644
index b0d3b5090ef0..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.windowdecor;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.annotation.ColorRes;
-import android.app.ActivityManager.RunningTaskInfo;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.view.Display;
-import android.view.LayoutInflater;
-import android.view.SurfaceControl;
-import android.view.SurfaceControlViewHost;
-import android.view.View;
-import android.view.WindowManager;
-import android.view.WindowlessWindowManager;
-import android.widget.ImageView;
-import android.window.TaskConstants;
-
-import com.android.wm.shell.R;
-
-import java.util.function.Supplier;
-
-/**
- * Creates and updates a veil that covers task contents on resize.
- */
-public class ResizeVeil {
- private static final int RESIZE_ALPHA_DURATION = 100;
- private final Context mContext;
- private final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier;
- private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier;
- private final Drawable mAppIcon;
- private ImageView mIconView;
- private SurfaceControl mParentSurface;
- private SurfaceControl mVeilSurface;
- private final RunningTaskInfo mTaskInfo;
- private SurfaceControlViewHost mViewHost;
- private final Display mDisplay;
- private ValueAnimator mVeilAnimator;
-
- public ResizeVeil(Context context, Drawable appIcon, RunningTaskInfo taskInfo,
- Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Display display,
- Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier) {
- mContext = context;
- mAppIcon = appIcon;
- mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier;
- mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
- mTaskInfo = taskInfo;
- mDisplay = display;
- setupResizeVeil();
- }
-
- /**
- * Create the veil in its default invisible state.
- */
- private void setupResizeVeil() {
- SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
- final SurfaceControl.Builder builder = mSurfaceControlBuilderSupplier.get();
- mVeilSurface = builder
- .setName("Resize veil of Task= " + mTaskInfo.taskId)
- .setContainerLayer()
- .build();
- View v = LayoutInflater.from(mContext)
- .inflate(R.layout.desktop_mode_resize_veil, null);
-
- t.setPosition(mVeilSurface, 0, 0)
- .setLayer(mVeilSurface, TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL)
- .apply();
- Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds();
- final WindowManager.LayoutParams lp =
- new WindowManager.LayoutParams(taskBounds.width(),
- taskBounds.height(),
- WindowManager.LayoutParams.TYPE_APPLICATION,
- WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
- PixelFormat.TRANSPARENT);
- lp.setTitle("Resize veil of Task=" + mTaskInfo.taskId);
- lp.setTrustedOverlay();
- WindowlessWindowManager windowManager = new WindowlessWindowManager(mTaskInfo.configuration,
- mVeilSurface, null /* hostInputToken */);
- mViewHost = new SurfaceControlViewHost(mContext, mDisplay, windowManager, "ResizeVeil");
- mViewHost.setView(v, lp);
-
- mIconView = mViewHost.getView().findViewById(R.id.veil_application_icon);
- mIconView.setImageDrawable(mAppIcon);
- }
-
- /**
- * Shows the veil surface/view.
- *
- * @param t the transaction to apply in sync with the veil draw
- * @param parentSurface the surface that the veil should be a child of
- * @param taskBounds the bounds of the task that owns the veil
- * @param fadeIn if true, the veil will fade-in with an animation, if false, it will be shown
- * immediately
- */
- public void showVeil(SurfaceControl.Transaction t, SurfaceControl parentSurface,
- Rect taskBounds, boolean fadeIn) {
- // Parent surface can change, ensure it is up to date.
- if (!parentSurface.equals(mParentSurface)) {
- t.reparent(mVeilSurface, parentSurface);
- mParentSurface = parentSurface;
- }
-
- int backgroundColorId = getBackgroundColorId();
- mViewHost.getView().setBackgroundColor(mContext.getColor(backgroundColorId));
-
- relayout(taskBounds, t);
- if (fadeIn) {
- cancelAnimation();
- mVeilAnimator = new ValueAnimator();
- mVeilAnimator.setFloatValues(0f, 1f);
- mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION);
- mVeilAnimator.addUpdateListener(animation -> {
- t.setAlpha(mVeilSurface, mVeilAnimator.getAnimatedFraction());
- t.apply();
- });
- mVeilAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- t.setAlpha(mVeilSurface, 1);
- t.apply();
- }
- });
-
- final ValueAnimator iconAnimator = new ValueAnimator();
- iconAnimator.setFloatValues(0f, 1f);
- iconAnimator.setDuration(RESIZE_ALPHA_DURATION);
- iconAnimator.addUpdateListener(animation -> {
- mIconView.setAlpha(animation.getAnimatedFraction());
- });
-
- t.show(mVeilSurface)
- .addTransactionCommittedListener(
- mContext.getMainExecutor(), () -> {
- mVeilAnimator.start();
- iconAnimator.start();
- })
- .setAlpha(mVeilSurface, 0);
- } else {
- // Show the veil immediately at full opacity.
- t.show(mVeilSurface).setAlpha(mVeilSurface, 1);
- }
- mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t);
- }
-
- /**
- * Animate veil's alpha to 1, fading it in.
- */
- public void showVeil(SurfaceControl parentSurface, Rect taskBounds) {
- SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
- showVeil(t, parentSurface, taskBounds, true /* fadeIn */);
- }
-
- /**
- * Update veil bounds to match bounds changes.
- * @param newBounds bounds to update veil to.
- */
- private void relayout(Rect newBounds, SurfaceControl.Transaction t) {
- mViewHost.relayout(newBounds.width(), newBounds.height());
- t.setWindowCrop(mVeilSurface, newBounds.width(), newBounds.height());
- t.setPosition(mParentSurface, newBounds.left, newBounds.top);
- t.setWindowCrop(mParentSurface, newBounds.width(), newBounds.height());
- }
-
- /**
- * Calls relayout to update task and veil bounds.
- * @param newBounds bounds to update veil to.
- */
- public void updateResizeVeil(Rect newBounds) {
- SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
- updateResizeVeil(t, newBounds);
- }
-
- /**
- * Calls relayout to update task and veil bounds.
- * Finishes veil fade in if animation is currently running; this is to prevent empty space
- * being visible behind the transparent veil during a fast resize.
- *
- * @param t a transaction to be applied in sync with the veil draw.
- * @param newBounds bounds to update veil to.
- */
- public void updateResizeVeil(SurfaceControl.Transaction t, Rect newBounds) {
- if (mVeilAnimator != null && mVeilAnimator.isStarted()) {
- mVeilAnimator.removeAllUpdateListeners();
- mVeilAnimator.end();
- }
- relayout(newBounds, t);
- mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t);
- }
-
- /**
- * Animate veil's alpha to 0, fading it out.
- */
- public void hideVeil() {
- cancelAnimation();
- mVeilAnimator = new ValueAnimator();
- mVeilAnimator.setFloatValues(1, 0);
- mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION);
- mVeilAnimator.addUpdateListener(animation -> {
- SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
- t.setAlpha(mVeilSurface, 1 - mVeilAnimator.getAnimatedFraction());
- t.apply();
- });
- mVeilAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
- t.hide(mVeilSurface);
- t.apply();
- }
- });
- mVeilAnimator.start();
- }
-
- @ColorRes
- private int getBackgroundColorId() {
- Configuration configuration = mContext.getResources().getConfiguration();
- if ((configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK)
- == Configuration.UI_MODE_NIGHT_YES) {
- return R.color.desktop_mode_resize_veil_dark;
- } else {
- return R.color.desktop_mode_resize_veil_light;
- }
- }
-
- private void cancelAnimation() {
- if (mVeilAnimator != null) {
- mVeilAnimator.removeAllUpdateListeners();
- mVeilAnimator.cancel();
- }
- }
-
- /**
- * Dispose of veil when it is no longer needed, likely on close of its container decor.
- */
- void dispose() {
- cancelAnimation();
- mVeilAnimator = null;
-
- if (mViewHost != null) {
- mViewHost.release();
- mViewHost = null;
- }
- if (mVeilSurface != null) {
- final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
- t.remove(mVeilSurface);
- mVeilSurface = null;
- t.apply();
- }
- }
-}
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
new file mode 100644
index 000000000000..cd2dac806a7f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.kt
@@ -0,0 +1,417 @@
+/*
+ * 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.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.PixelFormat
+import android.graphics.PointF
+import android.graphics.Rect
+import android.os.Trace
+import android.view.Display
+import android.view.LayoutInflater
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import android.view.SurfaceSession
+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
+
+/**
+ * Creates and updates a veil that covers task contents on resize.
+ */
+class ResizeVeil @JvmOverloads constructor(
+ private val context: Context,
+ private val displayController: DisplayController,
+ private val appIcon: Bitmap,
+ private var parentSurface: SurfaceControl,
+ private val surfaceControlTransactionSupplier: Supplier<SurfaceControl.Transaction>,
+ private val surfaceControlBuilderFactory: SurfaceControlBuilderFactory =
+ object : SurfaceControlBuilderFactory {},
+ private val surfaceControlViewHostFactory: 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
+
+ /** A container surface to host the veil background and icon child surfaces. */
+ private var veilSurface: SurfaceControl? = null
+ /** A color surface for the veil background. */
+ private var backgroundSurface: SurfaceControl? = null
+ /** A surface that hosts a windowless window with the app icon. */
+ private var iconSurface: SurfaceControl? = null
+ private var viewHost: SurfaceControlViewHost? = null
+ private var display: Display? = null
+ private var veilAnimator: ValueAnimator? = null
+
+ /**
+ * Whether the resize veil is currently visible.
+ *
+ * Note: when animating a [ResizeVeil.hideVeil], the veil is considered visible as soon
+ * as the animation starts.
+ */
+ private var isVisible = false
+
+ private val onDisplaysChangedListener: OnDisplaysChangedListener =
+ object : OnDisplaysChangedListener {
+ override fun onDisplayAdded(displayId: Int) {
+ if (taskInfo.displayId != displayId) {
+ return
+ }
+ displayController.removeDisplayWindowListener(this)
+ setupResizeVeil(taskInfo)
+ }
+ }
+
+ /**
+ * Whether the resize veil is ready to be shown.
+ */
+ private val isReady: Boolean
+ get() = viewHost != null
+
+ init {
+ setupResizeVeil(taskInfo)
+ }
+
+ /**
+ * Create the veil in its default invisible state.
+ */
+ private fun setupResizeVeil(taskInfo: RunningTaskInfo) {
+ if (!obtainDisplayOrRegisterListener(taskInfo.displayId)) {
+ // Display may not be available yet, skip this until then.
+ return
+ }
+ Trace.beginSection("ResizeVeil#setupResizeVeil")
+ veilSurface = surfaceControlBuilderFactory
+ .create("Resize veil of Task=" + taskInfo.taskId)
+ .setContainerLayer()
+ .setHidden(true)
+ .setParent(parentSurface)
+ .setCallsite("ResizeVeil#setupResizeVeil")
+ .build()
+ backgroundSurface = surfaceControlBuilderFactory
+ .create("Resize veil background of Task=" + taskInfo.taskId, surfaceSession)
+ .setColorLayer()
+ .setHidden(true)
+ .setParent(veilSurface)
+ .setCallsite("ResizeVeil#setupResizeVeil")
+ .build()
+ iconSurface = surfaceControlBuilderFactory
+ .create("Resize veil icon of Task=" + taskInfo.taskId)
+ .setContainerLayer()
+ .setHidden(true)
+ .setParent(veilSurface)
+ .setCallsite("ResizeVeil#setupResizeVeil")
+ .build()
+ iconSize = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_resize_veil_icon_size)
+ val root = LayoutInflater.from(context)
+ .inflate(R.layout.desktop_mode_resize_veil, null /* root */)
+ iconView = root.requireViewById(R.id.veil_application_icon)
+ iconView.setImageBitmap(appIcon)
+ val lp = WindowManager.LayoutParams(
+ iconSize,
+ iconSize,
+ WindowManager.LayoutParams.TYPE_APPLICATION,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
+ PixelFormat.TRANSPARENT)
+ lp.title = "Resize veil icon window of Task=" + taskInfo.taskId
+ lp.setTrustedOverlay()
+ val wwm = WindowlessWindowManager(taskInfo.configuration,
+ iconSurface, null /* hostInputToken */)
+ viewHost = surfaceControlViewHostFactory.create(context, display, wwm, "ResizeVeil")
+ viewHost?.setView(root, lp)
+ Trace.endSection()
+ }
+
+ private fun obtainDisplayOrRegisterListener(displayId: Int): Boolean {
+ display = displayController.getDisplay(displayId)
+ if (display == null) {
+ displayController.addDisplayWindowListener(onDisplaysChangedListener)
+ return false
+ }
+ return true
+ }
+
+ /**
+ * Shows the veil surface/view.
+ *
+ * @param t the transaction to apply in sync with the veil draw
+ * @param parent the surface that the veil should be a child of
+ * @param taskBounds the bounds of the task that owns the veil
+ * @param fadeIn if true, the veil will fade-in with an animation, if false, it will be shown
+ * immediately
+ */
+ fun showVeil(
+ t: SurfaceControl.Transaction,
+ parent: SurfaceControl,
+ taskBounds: Rect,
+ taskInfo: RunningTaskInfo,
+ fadeIn: Boolean,
+ ) {
+ if (!isReady || isVisible) {
+ t.apply()
+ return
+ }
+ isVisible = true
+ val background = backgroundSurface
+ val icon = iconSurface
+ val veil = veilSurface
+ if (background == null || icon == null || veil == null) return
+
+ // Parent surface can change, ensure it is up to date.
+ if (parent != parentSurface) {
+ t.reparent(veil, parent)
+ 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(backgroundColor.toArgb()).components)
+ relayout(taskBounds, t)
+ if (fadeIn) {
+ cancelAnimation()
+ val veilAnimT = surfaceControlTransactionSupplier.get()
+ val iconAnimT = surfaceControlTransactionSupplier.get()
+ veilAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = RESIZE_ALPHA_DURATION
+ addUpdateListener {
+ veilAnimT.setAlpha(background, animatedValue as Float)
+ .apply()
+ }
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ veilAnimT.show(background)
+ .setAlpha(background, 0f)
+ .apply()
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ veilAnimT.setAlpha(background, 1f).apply()
+ }
+ })
+ }
+ val iconAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = RESIZE_ALPHA_DURATION
+ addUpdateListener {
+ iconAnimT.setAlpha(icon, animatedValue as Float)
+ .apply()
+ }
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ iconAnimT.show(icon)
+ .setAlpha(icon, 0f)
+ .apply()
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ iconAnimT.setAlpha(icon, 1f).apply()
+ }
+ })
+ }
+
+ // Let the animators show it with the correct alpha value once the animation starts.
+ t.hide(icon)
+ .hide(background)
+ .apply()
+ veilAnimator?.start()
+ iconAnimator.start()
+ } else {
+ // Show the veil immediately.
+ t.show(icon)
+ .show(background)
+ .setAlpha(icon, 1f)
+ .setAlpha(background, 1f)
+ .apply()
+ }
+ }
+
+ /**
+ * Animate veil's alpha to 1, fading it in.
+ */
+ fun showVeil(parentSurface: SurfaceControl, taskBounds: Rect, taskInfo: RunningTaskInfo) {
+ if (!isReady || isVisible) {
+ return
+ }
+ val t = surfaceControlTransactionSupplier.get()
+ showVeil(t, parentSurface, taskBounds, taskInfo, true /* fadeIn */)
+ }
+
+ /**
+ * Update veil bounds to match bounds changes.
+ * @param newBounds bounds to update veil to.
+ */
+ private fun relayout(newBounds: Rect, t: SurfaceControl.Transaction) {
+ val iconPosition = calculateAppIconPosition(newBounds)
+ val veil = veilSurface
+ val icon = iconSurface
+ if (veil == null || icon == null) return
+ t.setWindowCrop(veil, newBounds.width(), newBounds.height())
+ .setPosition(icon, iconPosition.x, iconPosition.y)
+ .setPosition(parentSurface, newBounds.left.toFloat(), newBounds.top.toFloat())
+ .setWindowCrop(parentSurface, newBounds.width(), newBounds.height())
+ }
+
+ /**
+ * Calls relayout to update task and veil bounds.
+ * @param newBounds bounds to update veil to.
+ */
+ fun updateResizeVeil(newBounds: Rect) {
+ if (!isVisible) {
+ return
+ }
+ val t = surfaceControlTransactionSupplier.get()
+ updateResizeVeil(t, newBounds)
+ }
+
+ /**
+ * Calls relayout to update task and veil bounds.
+ * Finishes veil fade in if animation is currently running; this is to prevent empty space
+ * being visible behind the transparent veil during a fast resize.
+ *
+ * @param t a transaction to be applied in sync with the veil draw.
+ * @param newBounds bounds to update veil to.
+ */
+ fun updateResizeVeil(t: SurfaceControl.Transaction, newBounds: Rect) {
+ if (!isVisible) {
+ t.apply()
+ return
+ }
+ veilAnimator?.let { animator ->
+ if (animator.isStarted) {
+ animator.removeAllUpdateListeners()
+ animator.end()
+ }
+ }
+ relayout(newBounds, t)
+ t.apply()
+ }
+
+ /**
+ * Animate veil's alpha to 0, fading it out.
+ */
+ fun hideVeil() {
+ if (!isVisible) {
+ return
+ }
+ cancelAnimation()
+ val background = backgroundSurface
+ val icon = iconSurface
+ if (background == null || icon == null) return
+
+ veilAnimator = ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = RESIZE_ALPHA_DURATION
+ addUpdateListener {
+ surfaceControlTransactionSupplier.get()
+ .setAlpha(background, animatedValue as Float)
+ .setAlpha(icon, animatedValue as Float)
+ .apply()
+ }
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ surfaceControlTransactionSupplier.get()
+ .hide(background)
+ .hide(icon)
+ .apply()
+ }
+ })
+ }
+ veilAnimator?.start()
+ isVisible = false
+ }
+
+ private fun calculateAppIconPosition(parentBounds: Rect): PointF {
+ return PointF(parentBounds.width().toFloat() / 2 - iconSize.toFloat() / 2,
+ parentBounds.height().toFloat() / 2 - iconSize.toFloat() / 2)
+ }
+
+ private fun cancelAnimation() {
+ veilAnimator?.removeAllUpdateListeners()
+ veilAnimator?.cancel()
+ }
+
+ /**
+ * Dispose of veil when it is no longer needed, likely on close of its container decor.
+ */
+ fun dispose() {
+ cancelAnimation()
+ veilAnimator = null
+ isVisible = false
+
+ viewHost?.release()
+ viewHost = null
+
+ val t: SurfaceControl.Transaction = surfaceControlTransactionSupplier.get()
+ backgroundSurface?.let { background -> t.remove(background) }
+ backgroundSurface = null
+ iconSurface?.let { icon -> t.remove(icon) }
+ iconSurface = null
+ veilSurface?.let { veil -> t.remove(veil) }
+ veilSurface = null
+ t.apply()
+ displayController.removeDisplayWindowListener(onDisplaysChangedListener)
+ }
+
+ interface SurfaceControlBuilderFactory {
+ fun create(name: String): SurfaceControl.Builder {
+ return SurfaceControl.Builder().setName(name)
+ }
+
+ fun create(name: String, surfaceSession: SurfaceSession): SurfaceControl.Builder {
+ return SurfaceControl.Builder(surfaceSession).setName(name)
+ }
+ }
+
+ companion object {
+ private const val TAG = "ResizeVeil"
+ private const val RESIZE_ALPHA_DURATION = 100L
+ private const val VEIL_CONTAINER_LAYER = TaskConstants.TASK_CHILD_LAYER_RESIZE_VEIL
+
+ /** The background is a child of the veil container layer and goes at the bottom. */
+ private const val VEIL_BACKGROUND_LAYER = 0
+
+ /** The icon is a child of the veil container layer and goes in front of the background. */
+ private const val VEIL_ICON_LAYER = 1
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java
index d0fcd8651481..ad238c35dd83 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java
@@ -52,19 +52,19 @@ class TaskOperations {
mSyncQueue = syncQueue;
}
- void injectBackKey() {
- sendBackEvent(KeyEvent.ACTION_DOWN);
- sendBackEvent(KeyEvent.ACTION_UP);
+ void injectBackKey(int displayId) {
+ sendBackEvent(KeyEvent.ACTION_DOWN, displayId);
+ sendBackEvent(KeyEvent.ACTION_UP, displayId);
}
- private void sendBackEvent(int action) {
+ private void sendBackEvent(int action, int displayId) {
final long when = SystemClock.uptimeMillis();
final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK,
0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD,
0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
InputDevice.SOURCE_KEYBOARD);
- ev.setDisplayId(mContext.getDisplay().getDisplayId());
+ ev.setDisplayId(displayId);
if (!mContext.getSystemService(InputManager.class)
.injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
Log.e(TAG, "Inject input event fail");
@@ -72,7 +72,10 @@ class TaskOperations {
}
void closeTask(WindowContainerToken taskToken) {
- WindowContainerTransaction wct = new WindowContainerTransaction();
+ closeTask(taskToken, new WindowContainerTransaction());
+ }
+
+ void closeTask(WindowContainerToken taskToken, WindowContainerTransaction wct) {
wct.removeTask(taskToken);
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
mTransitionStarter.startRemoveTransition(wct);
@@ -91,14 +94,12 @@ class TaskOperations {
}
}
- void maximizeTask(RunningTaskInfo taskInfo) {
+ void maximizeTask(RunningTaskInfo taskInfo, int containerWindowingMode) {
WindowContainerTransaction wct = new WindowContainerTransaction();
int targetWindowingMode = taskInfo.getWindowingMode() != WINDOWING_MODE_FULLSCREEN
? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_FREEFORM;
- int displayWindowingMode =
- taskInfo.configuration.windowConfiguration.getDisplayWindowingMode();
wct.setWindowingMode(taskInfo.token,
- targetWindowingMode == displayWindowingMode
+ targetWindowingMode == containerWindowingMode
? WINDOWING_MODE_UNDEFINED : targetWindowingMode);
if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) {
wct.setBounds(taskInfo.token, null);
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 5c69d5542227..5fce5d228d71 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,7 @@ package com.android.wm.shell.windowdecor;
import static android.view.WindowManager.TRANSIT_CHANGE;
+import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.IBinder;
@@ -54,9 +55,6 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback,
private final Rect mTaskBoundsAtDragStart = new Rect();
private final PointF mRepositionStartPoint = new PointF();
private final Rect mRepositionTaskBounds = new Rect();
- // If a task move (not resize) finishes with the positions y less than this value, do not
- // finalize the bounds there using WCT#setBounds
- private final int mDisallowedAreaForEndBoundsHeight;
private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
private int mCtrlType;
private boolean mIsResizingOrAnimatingResize;
@@ -66,25 +64,22 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback,
DesktopModeWindowDecoration windowDecoration,
DisplayController displayController,
DragPositioningCallbackUtility.DragStartListener dragStartListener,
- Transitions transitions,
- int disallowedAreaForEndBoundsHeight) {
+ Transitions transitions) {
this(taskOrganizer, windowDecoration, displayController, dragStartListener,
- SurfaceControl.Transaction::new, transitions, disallowedAreaForEndBoundsHeight);
+ SurfaceControl.Transaction::new, transitions);
}
public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer,
DesktopModeWindowDecoration windowDecoration,
DisplayController displayController,
DragPositioningCallbackUtility.DragStartListener dragStartListener,
- Supplier<SurfaceControl.Transaction> supplier, Transitions transitions,
- int disallowedAreaForEndBoundsHeight) {
+ Supplier<SurfaceControl.Transaction> supplier, Transitions transitions) {
mDesktopWindowDecoration = windowDecoration;
mTaskOrganizer = taskOrganizer;
mDisplayController = displayController;
mDragStartListener = dragStartListener;
mTransactionSupplier = supplier;
mTransitions = transitions;
- mDisallowedAreaForEndBoundsHeight = disallowedAreaForEndBoundsHeight;
}
@Override
@@ -151,13 +146,10 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback,
// won't be called.
resetVeilIfVisible();
}
- } else if (DragPositioningCallbackUtility.isBelowDisallowedArea(
- mDisallowedAreaForEndBoundsHeight, mTaskBoundsAtDragStart, mRepositionStartPoint,
- y)) {
+ } else {
final WindowContainerTransaction wct = new WindowContainerTransaction();
- DragPositioningCallbackUtility.onDragEnd(mRepositionTaskBounds,
- mTaskBoundsAtDragStart, mRepositionStartPoint, x, y,
- mDesktopWindowDecoration.calculateValidDragArea());
+ DragPositioningCallbackUtility.updateTaskBounds(mRepositionTaskBounds,
+ mTaskBoundsAtDragStart, mRepositionStartPoint, x, y);
wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds);
mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
}
@@ -188,10 +180,11 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback,
for (TransitionInfo.Change change: info.getChanges()) {
final SurfaceControl sc = change.getLeash();
final Rect endBounds = change.getEndAbsBounds();
+ final Point endPosition = change.getEndRelOffset();
startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
- .setPosition(sc, endBounds.left, endBounds.top);
+ .setPosition(sc, endPosition.x, endPosition.y);
finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height())
- .setPosition(sc, endBounds.left, endBounds.top);
+ .setPosition(sc, endPosition.x, endPosition.y);
}
startTransaction.apply();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
index 01a6012ea314..1563259f4a1a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecorViewModel.java
@@ -67,6 +67,14 @@ public interface WindowDecorViewModel {
void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo);
/**
+ * Notifies a task has vanished, which can mean that the task changed windowing mode or was
+ * removed.
+ *
+ * @param taskInfo the task info of the task
+ */
+ void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo);
+
+ /**
* Notifies a transition is about to start about the given task to give the window decoration a
* chance to prepare for this transition. Unlike {@link #onTaskInfoChanged}, this method creates
* a window decoration if one does not exist but is required.
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 32c2d1e9b257..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
@@ -18,7 +18,11 @@ package com.android.wm.shell.windowdecor;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+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;
@@ -33,6 +37,7 @@ import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Binder;
+import android.os.Trace;
import android.view.Display;
import android.view.InsetsSource;
import android.view.InsetsState;
@@ -40,21 +45,24 @@ import android.view.LayoutInflater;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.View;
-import android.view.ViewRootImpl;
-import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
import android.window.SurfaceSyncGroup;
import android.window.TaskConstants;
+import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.desktopmode.DesktopModeStatus;
+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;
import java.util.List;
+import java.util.Objects;
import java.util.function.Supplier;
/**
@@ -131,8 +139,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
TaskDragResizer mTaskDragResizer;
private boolean mIsCaptionVisible;
+ /** The most recent set of insets applied to this window decoration. */
+ private WindowDecorationInsets mWindowDecorationInsets;
private final Binder mOwner = new Binder();
- private final Rect mCaptionInsetsRect = new Rect();
private final float[] mTmpColor = new float[3];
WindowDecoration(
@@ -140,9 +149,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
DisplayController displayController,
ShellTaskOrganizer taskOrganizer,
RunningTaskInfo taskInfo,
- SurfaceControl taskSurface,
- Configuration windowDecorConfig) {
- this(context, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorConfig,
+ SurfaceControl taskSurface) {
+ this(context, displayController, taskOrganizer, taskInfo, taskSurface,
SurfaceControl.Builder::new, SurfaceControl.Transaction::new,
WindowContainerTransaction::new, SurfaceControl::new,
new SurfaceControlViewHostFactory() {});
@@ -154,7 +162,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
ShellTaskOrganizer taskOrganizer,
RunningTaskInfo taskInfo,
@NonNull SurfaceControl taskSurface,
- Configuration windowDecorConfig,
Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
@@ -171,8 +178,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
mSurfaceControlViewHostFactory = surfaceControlViewHostFactory;
mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId);
- mWindowDecorConfig = windowDecorConfig;
- mDecorWindowContext = mContext.createConfigurationContext(mWindowDecorConfig);
}
/**
@@ -194,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;
}
@@ -203,20 +216,48 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
mLayoutResId = params.mLayoutResId;
if (!mTaskInfo.isVisible) {
- releaseViews();
+ releaseViews(wct);
finishT.hide(mTaskSurface);
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.densityDpi;
- final int oldNightMode = mWindowDecorConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
+ final int oldDensityDpi = mWindowDecorConfig != null
+ ? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED;
+ final int oldNightMode = mWindowDecorConfig != null
+ ? (mWindowDecorConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK)
+ : Configuration.UI_MODE_NIGHT_UNDEFINED;
mWindowDecorConfig = params.mWindowDecorConfig != null ? params.mWindowDecorConfig
: mTaskInfo.getConfiguration();
final int newDensityDpi = mWindowDecorConfig.densityDpi;
@@ -225,8 +266,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
|| mDisplay == null
|| mDisplay.getDisplayId() != mTaskInfo.displayId
|| oldLayoutResId != mLayoutResId
- || oldNightMode != newNightMode) {
- releaseViews();
+ || oldNightMode != newNightMode
+ || mDecorWindowContext == null) {
+ releaseViews(wct);
if (!obtainDisplayOrRegisterListener()) {
outResult.mRootView = null;
@@ -244,24 +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.updateDecorationContainerSurface")
.build();
startT.setTrustedOverlay(mDecorationContainerSurface, true)
@@ -271,103 +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.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);
+ }
- if (ViewRootImpl.CAPTION_ON_SHELL) {
- 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.
- mCaptionInsetsRect.set(taskBounds);
- mCaptionInsetsRect.bottom = mCaptionInsetsRect.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.mAllowCaptionInputFallthrough) {
- outResult.mCustomizableCaptionRegion.set(mCaptionInsetsRect);
- }
- 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, mCaptionInsetsRect);
- // Subtract the regions used by the caption elements, the rest is
- // customizable.
- if (params.mAllowCaptionInputFallthrough) {
- outResult.mCustomizableCaptionRegion.op(boundingRects[i],
- Region.Op.DIFFERENCE);
- }
- }
- }
- // Add this caption as an inset source.
- wct.addInsetsSource(mTaskInfo.token,
- mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect,
- boundingRects);
- wct.addInsetsSource(mTaskInfo.token,
- mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures(),
- mCaptionInsetsRect, null /* boundingRects */);
- } else {
- wct.removeInsetsSource(mTaskInfo.token, mOwner, 0 /* index */,
- WindowInsets.Type.captionBar());
- wct.removeInsetsSource(mTaskInfo.token, mOwner, 0 /* index */,
- WindowInsets.Type.mandatorySystemGestures());
+ private void updateCaptionInsets(RelayoutParams params, WindowContainerTransaction wct,
+ RelayoutResult<T> outResult, Rect taskBounds) {
+ if (!mIsCaptionVisible) {
+ if (mWindowDecorationInsets != null) {
+ mWindowDecorationInsets.remove(wct);
+ mWindowDecorationInsets = null;
}
+ return;
+ }
+ // 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 {
- startT.hide(mCaptionContainerSurface);
+ // 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);
+ }
+ }
}
- // Task surface itself
- float shadowRadius;
- final Point taskPosition = mTaskInfo.positionInParent;
- if (isFullscreen) {
- // Shadow is not needed for fullscreen tasks
- shadowRadius = 0;
- } else {
- shadowRadius = loadDimension(resources, params.mShadowRadiusId);
+ 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
@@ -382,7 +415,20 @@ 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
// of windowless window layers and BLASTBufferQueue#update() doesn't support offset.
@@ -390,33 +436,38 @@ 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();
- if (params.mAllowCaptionInputFallthrough) {
- lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_SPY;
- } else {
- lp.inputFeatures &= ~WindowManager.LayoutParams.INPUT_FEATURE_SPY;
- }
+ lp.inputFeatures = params.mInputFeatures;
if (mViewHost == null) {
+ Trace.beginSection("CaptionViewHostLayout-new");
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();
}
+ Trace.endSection(); // CaptionViewHostLayout
}
private Rect calculateBoundingRect(@NonNull OccludingCaptionElement element,
@@ -487,7 +538,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
return true;
}
- void releaseViews() {
+ void releaseViews(WindowContainerTransaction wct) {
if (mViewHost != null) {
mViewHost.release();
mViewHost = null;
@@ -513,19 +564,21 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
t.apply();
}
- final WindowContainerTransaction wct = mWindowContainerTransactionSupplier.get();
- wct.removeInsetsSource(mTaskInfo.token,
- mOwner, 0 /* index */, WindowInsets.Type.captionBar());
- wct.removeInsetsSource(mTaskInfo.token,
- mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures());
- mTaskOrganizer.applyTransaction(wct);
+ if (mWindowDecorationInsets != null) {
+ mWindowDecorationInsets.remove(wct);
+ mWindowDecorationInsets = null;
+ }
}
@Override
public void close() {
+ Trace.beginSection("WindowDecoration#close");
mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener);
- releaseViews();
+ final WindowContainerTransaction wct = mWindowContainerTransactionSupplier.get();
+ releaseViews(wct);
+ mTaskOrganizer.applyTransaction(wct);
mTaskSurface.release();
+ Trace.endSection();
}
static int loadDimensionPixelSize(Resources resources, int resourceId) {
@@ -559,15 +612,17 @@ 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)
.setContainerLayer()
.setParent(mDecorationContainerSurface)
+ .setCallsite("WindowDecoration.addWindow")
.build();
View v = LayoutInflater.from(mDecorWindowContext).inflate(layoutId, null);
@@ -575,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,
@@ -585,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);
}
@@ -594,15 +649,18 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
*/
public void addCaptionInset(WindowContainerTransaction wct) {
final int captionHeightId = getCaptionHeightId(mTaskInfo.getWindowingMode());
- if (!ViewRootImpl.CAPTION_ON_SHELL || captionHeightId == Resources.ID_NULL
- || !mIsCaptionVisible) {
+ if (captionHeightId == Resources.ID_NULL || !mIsCaptionVisible) {
return;
}
final int captionHeight = loadDimensionPixelSize(mContext.getResources(), captionHeightId);
final Rect captionInsets = new Rect(0, 0, 0, captionHeight);
- wct.addInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, WindowInsets.Type.captionBar(),
- captionInsets, null /* boundingRects */);
+ final WindowDecorationInsets newInsets = new WindowDecorationInsets(mTaskInfo.token,
+ mOwner, captionInsets, null /* boundingRets */);
+ if (!newInsets.equals(mWindowDecorationInsets)) {
+ mWindowDecorationInsets = newInsets;
+ mWindowDecorationInsets.addOrUpdate(wct);
+ }
}
static class RelayoutParams {
@@ -611,7 +669,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
int mCaptionHeightId;
int mCaptionWidthId;
final List<OccludingCaptionElement> mOccludingCaptionElements = new ArrayList<>();
- boolean mAllowCaptionInputFallthrough;
+ int mInputFeatures;
int mShadowRadiusId;
int mCornerRadius;
@@ -626,7 +684,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
mCaptionHeightId = Resources.ID_NULL;
mCaptionWidthId = Resources.ID_NULL;
mOccludingCaptionElements.clear();
- mAllowCaptionInputFallthrough = false;
+ mInputFeatures = 0;
mShadowRadiusId = Resources.ID_NULL;
mCornerRadius = 0;
@@ -636,6 +694,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
mWindowDecorConfig = null;
}
+ boolean hasInputFeatureSpy() {
+ return (mInputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_SPY) != 0;
+ }
+
/**
* Describes elements within the caption bar that could occlude app content, and should be
* sent as bounding rectangles to the insets system.
@@ -670,46 +732,55 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
}
}
- interface SurfaceControlViewHostFactory {
+ @VisibleForTesting
+ public interface SurfaceControlViewHostFactory {
default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) {
return new SurfaceControlViewHost(c, d, wmm, "WindowDecoration");
}
+ default SurfaceControlViewHost create(Context c, Display d,
+ WindowlessWindowManager wmm, String callsite) {
+ return new SurfaceControlViewHost(c, d, wmm, callsite);
+ }
}
- /**
- * Subclass for additional windows associated with this WindowDecoration
- */
- static class AdditionalWindow {
- SurfaceControl mWindowSurface;
- SurfaceControlViewHost mWindowViewHost;
- Supplier<SurfaceControl.Transaction> mTransactionSupplier;
+ private static class WindowDecorationInsets {
+ private static final int INDEX = 0;
+ private final WindowContainerToken mToken;
+ private final Binder mOwner;
+ private final Rect mFrame;
+ private final Rect[] mBoundingRects;
+
+ private WindowDecorationInsets(WindowContainerToken token, Binder owner, Rect frame,
+ Rect[] boundingRects) {
+ mToken = token;
+ mOwner = owner;
+ mFrame = frame;
+ mBoundingRects = boundingRects;
+ }
+
+ void addOrUpdate(WindowContainerTransaction wct) {
+ wct.addInsetsSource(mToken, mOwner, INDEX, captionBar(), mFrame, mBoundingRects);
+ wct.addInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures(), mFrame,
+ mBoundingRects);
+ }
- AdditionalWindow(SurfaceControl surfaceControl,
- SurfaceControlViewHost surfaceControlViewHost,
- Supplier<SurfaceControl.Transaction> transactionSupplier) {
- mWindowSurface = surfaceControl;
- mWindowViewHost = surfaceControlViewHost;
- mTransactionSupplier = transactionSupplier;
+ void remove(WindowContainerTransaction wct) {
+ wct.removeInsetsSource(mToken, mOwner, INDEX, captionBar());
+ wct.removeInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures());
}
- void releaseView() {
- WindowlessWindowManager windowManager = mWindowViewHost.getWindowlessWM();
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof WindowDecoration.WindowDecorationInsets that)) return false;
+ return Objects.equals(mToken, that.mToken) && Objects.equals(mOwner,
+ that.mOwner) && Objects.equals(mFrame, that.mFrame)
+ && Objects.deepEquals(mBoundingRects, that.mBoundingRects);
+ }
- 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();
- }
+ @Override
+ public int hashCode() {
+ return Objects.hash(mToken, mOwner, mFrame, Arrays.hashCode(mBoundingRects));
}
}
}
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/extension/TaskInfo.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
index 5dd96aceaec7..7ade9876d28a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
@@ -17,17 +17,21 @@
package com.android.wm.shell.windowdecor.extension
import android.app.TaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS
import android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND
val TaskInfo.isTransparentCaptionBarAppearance: Boolean
get() {
- val appearance = taskDescription?.statusBarAppearance ?: 0
+ val appearance = taskDescription?.topOpaqueSystemBarsAppearance ?: 0
return (appearance and APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND) != 0
}
val TaskInfo.isLightCaptionBarAppearance: Boolean
get() {
- val appearance = taskDescription?.statusBarAppearance ?: 0
+ val appearance = taskDescription?.topOpaqueSystemBarsAppearance ?: 0
return (appearance and APPEARANCE_LIGHT_CAPTION_BARS) != 0
}
+
+val TaskInfo.isFullscreen: Boolean
+ get() = windowingMode == WINDOWING_MODE_FULLSCREEN
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 6dcae2776847..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
@@ -65,7 +80,7 @@ internal class DesktopModeFocusedWindowDecorationViewHolder(
taskInfo.windowingMode == WINDOWING_MODE_FREEFORM) {
Color.valueOf(taskDescription.statusBarColor).luminance() < 0.5
} else {
- taskDescription.statusBarAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0
+ taskDescription.systemBarsAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0
}
} ?: false
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
new file mode 100644
index 000000000000..46127b177bc3
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -0,0 +1,500 @@
+/*
+ * 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.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.LayerDrawable
+import android.graphics.drawable.RippleDrawable
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.RoundRectShape
+import android.view.View
+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.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.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
+
+/**
+ * A desktop mode window decoration used when the window is floating (i.e. freeform). It hosts
+ * finer controls such as a close window button and an "app info" section to pull up additional
+ * controls.
+ */
+internal class AppHeaderViewHolder(
+ rootView: View,
+ onCaptionTouchListener: View.OnTouchListener,
+ onCaptionButtonClickListener: View.OnClickListener,
+ onLongClickListener: OnLongClickListener,
+ onCaptionGenericMotionListener: View.OnGenericMotionListener,
+ appName: CharSequence,
+ appIconBitmap: Bitmap,
+ onMaximizeHoverAnimationFinishedListener: () -> Unit
+) : 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.
+ **/
+ private val headerButtonsRippleRadius = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_header_buttons_ripple_radius)
+
+ /**
+ * The app chip, maximize and close button's height extends to the top & bottom edges of the
+ * header, and their width may be larger than their height. This is by design to increase the
+ * clickable and hover-able bounds of the view as much as possible. However, to prevent the
+ * ripple drawable from being as large as the views (and asymmetrical), insets are applied to
+ * the background ripple drawable itself to give the appearance of a smaller button
+ * (with padding between itself and the header edges / sibling buttons) but without affecting
+ * its touchable region.
+ */
+ private val appChipDrawableInsets = DrawableInsets(
+ vertical = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_header_app_chip_ripple_inset_vertical)
+ )
+ private val maximizeDrawableInsets = DrawableInsets(
+ vertical = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_header_maximize_ripple_inset_vertical),
+ horizontal = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_header_maximize_ripple_inset_horizontal)
+ )
+ private val closeDrawableInsets = DrawableInsets(
+ vertical = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_header_close_ripple_inset_vertical),
+ horizontal = context.resources
+ .getDimensionPixelSize(R.dimen.desktop_mode_header_close_ripple_inset_horizontal)
+ )
+
+ private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
+ private val captionHandle: View = rootView.requireViewById(R.id.caption_handle)
+ private val openMenuButton: View = rootView.requireViewById(R.id.open_menu_button)
+ private val closeWindowButton: ImageButton = rootView.requireViewById(R.id.close_window)
+ private val expandMenuButton: ImageButton = rootView.requireViewById(R.id.expand_menu_button)
+ private val maximizeButtonView: MaximizeButtonView =
+ rootView.requireViewById(R.id.maximize_button_view)
+ private val maximizeWindowButton: ImageButton = rootView.requireViewById(R.id.maximize_window)
+ private val appNameTextView: TextView = rootView.requireViewById(R.id.application_name)
+ private val appIconImageView: ImageView = rootView.requireViewById(R.id.application_icon)
+ val appNameTextWidth: Int
+ get() = appNameTextView.width
+
+ init {
+ captionView.setOnTouchListener(onCaptionTouchListener)
+ captionHandle.setOnTouchListener(onCaptionTouchListener)
+ openMenuButton.setOnClickListener(onCaptionButtonClickListener)
+ openMenuButton.setOnTouchListener(onCaptionTouchListener)
+ closeWindowButton.setOnClickListener(onCaptionButtonClickListener)
+ maximizeWindowButton.setOnClickListener(onCaptionButtonClickListener)
+ maximizeWindowButton.setOnTouchListener(onCaptionTouchListener)
+ maximizeWindowButton.setOnGenericMotionListener(onCaptionGenericMotionListener)
+ maximizeWindowButton.onLongClickListener = onLongClickListener
+ closeWindowButton.setOnTouchListener(onCaptionTouchListener)
+ appNameTextView.text = appName
+ appIconImageView.setImageBitmap(appIconBitmap)
+ maximizeButtonView.onHoverAnimationFinishedListener =
+ onMaximizeHoverAnimationFinishedListener
+ }
+
+ override fun bindData(taskInfo: RunningTaskInfo) {
+ if (Flags.enableThemedAppHeaders()) {
+ bindDataWithThemedHeaders(taskInfo)
+ } else {
+ bindDataLegacy(taskInfo)
+ }
+ }
+
+ private fun bindDataLegacy(taskInfo: RunningTaskInfo) {
+ captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo))
+ val color = getAppNameAndButtonColor(taskInfo)
+ val alpha = Color.alpha(color)
+ closeWindowButton.imageTintList = ColorStateList.valueOf(color)
+ maximizeWindowButton.imageTintList = ColorStateList.valueOf(color)
+ expandMenuButton.imageTintList = ColorStateList.valueOf(color)
+ appNameTextView.isVisible = !taskInfo.isTransparentCaptionBarAppearance
+ appNameTextView.setTextColor(color)
+ appIconImageView.imageAlpha = alpha
+ maximizeWindowButton.imageAlpha = alpha
+ closeWindowButton.imageAlpha = alpha
+ expandMenuButton.imageAlpha = alpha
+ context.withStyledAttributes(
+ set = null,
+ attrs = intArrayOf(
+ android.R.attr.selectableItemBackground,
+ android.R.attr.selectableItemBackgroundBorderless
+ ),
+ defStyleAttr = 0,
+ defStyleRes = 0
+ ) {
+ openMenuButton.background = getDrawable(0)
+ maximizeWindowButton.background = getDrawable(1)
+ closeWindowButton.background = getDrawable(1)
+ }
+ maximizeButtonView.setAnimationTints(isDarkMode())
+ }
+
+ private fun bindDataWithThemedHeaders(taskInfo: RunningTaskInfo) {
+ val header = fillHeaderInfo(taskInfo)
+ val headerStyle = getHeaderStyle(header)
+
+ // Caption Background
+ when (headerStyle.background) {
+ is HeaderStyle.Background.Opaque -> {
+ captionView.setBackgroundColor(headerStyle.background.color)
+ }
+ HeaderStyle.Background.Transparent -> {
+ captionView.setBackgroundColor(Color.TRANSPARENT)
+ }
+ }
+
+ // Caption Foreground
+ val foregroundColor = headerStyle.foreground.color
+ val foregroundAlpha = headerStyle.foreground.opacity
+ val colorStateList = ColorStateList.valueOf(foregroundColor).withAlpha(foregroundAlpha)
+ // App chip.
+ openMenuButton.apply {
+ background = createRippleDrawable(
+ color = foregroundColor,
+ cornerRadius = headerButtonsRippleRadius,
+ drawableInsets = appChipDrawableInsets,
+ )
+ expandMenuButton.imageTintList = colorStateList
+ appNameTextView.apply {
+ isVisible = header.type == Header.Type.DEFAULT
+ setTextColor(colorStateList)
+ }
+ appIconImageView.imageAlpha = foregroundAlpha
+ }
+ // Maximize button.
+ maximizeButtonView.setAnimationTints(
+ darkMode = header.appTheme == Theme.DARK,
+ iconForegroundColor = colorStateList,
+ baseForegroundColor = foregroundColor,
+ rippleDrawable = createRippleDrawable(
+ color = foregroundColor,
+ cornerRadius = headerButtonsRippleRadius,
+ drawableInsets = maximizeDrawableInsets
+ )
+ )
+ // Close button.
+ closeWindowButton.apply {
+ imageTintList = colorStateList
+ background = createRippleDrawable(
+ color = foregroundColor,
+ cornerRadius = headerButtonsRippleRadius,
+ drawableInsets = closeDrawableInsets
+ )
+ }
+ }
+
+ override fun onHandleMenuOpened() {}
+
+ override fun onHandleMenuClosed() {}
+
+ fun setAnimatingTaskResize(animatingTaskResize: Boolean) {
+ // If animating a task resize, cancel any running hover animations
+ if (animatingTaskResize) {
+ maximizeButtonView.cancelHoverAnimation()
+ }
+ maximizeButtonView.hoverDisabled = animatingTaskResize
+ }
+
+ fun onMaximizeWindowHoverExit() {
+ maximizeButtonView.cancelHoverAnimation()
+ }
+
+ fun onMaximizeWindowHoverEnter() {
+ maximizeButtonView.startHoverAnimation()
+ }
+
+ private fun getHeaderStyle(header: Header): HeaderStyle {
+ return HeaderStyle(
+ background = getHeaderBackground(header),
+ foreground = getHeaderForeground(header)
+ )
+ }
+
+ private fun getHeaderBackground(header: Header): HeaderStyle.Background {
+ return when (header.type) {
+ Header.Type.DEFAULT -> {
+ 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())
+ }
+ }
+ }
+ }
+ Header.Type.CUSTOM -> HeaderStyle.Background.Transparent
+ }
+ }
+
+ private fun getHeaderForeground(header: Header): HeaderStyle.Foreground {
+ return when (header.type) {
+ Header.Type.DEFAULT -> {
+ 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
+ )
+ }
+ }
+ }
+ }
+ Header.Type.CUSTOM -> when {
+ header.isAppearanceCaptionLight && header.isFocused -> {
+ HeaderStyle.Foreground(
+ color = lightColors.onSecondaryContainer.toArgb(),
+ opacity = OPACITY_100
+ )
+ }
+ header.isAppearanceCaptionLight && !header.isFocused -> {
+ HeaderStyle.Foreground(
+ color = lightColors.onSecondaryContainer.toArgb(),
+ opacity = OPACITY_65
+ )
+ }
+ !header.isAppearanceCaptionLight && header.isFocused -> {
+ HeaderStyle.Foreground(
+ color = darkColors.onSurface.toArgb(),
+ opacity = OPACITY_100
+ )
+ }
+ !header.isAppearanceCaptionLight && !header.isFocused -> {
+ HeaderStyle.Foreground(
+ color = darkColors.onSurface.toArgb(),
+ opacity = OPACITY_55
+ )
+ }
+ else -> error("No other combination expected header=$header")
+ }
+ }
+ }
+
+ private fun fillHeaderInfo(taskInfo: RunningTaskInfo): Header {
+ return Header(
+ type = if (taskInfo.isTransparentCaptionBarAppearance) {
+ Header.Type.CUSTOM
+ } else {
+ Header.Type.DEFAULT
+ },
+ appTheme = decorThemeUtil.getAppTheme(taskInfo),
+ isFocused = taskInfo.isFocused,
+ isAppearanceCaptionLight = taskInfo.isLightCaptionBarAppearance
+ )
+ }
+
+ @ColorInt
+ private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
+ return Color.argb(
+ alpha,
+ Color.red(color),
+ Color.green(color),
+ Color.blue(color)
+ )
+ }
+
+ private fun createRippleDrawable(
+ @ColorInt color: Int,
+ cornerRadius: Int,
+ drawableInsets: DrawableInsets,
+ ): RippleDrawable {
+ return RippleDrawable(
+ ColorStateList(
+ arrayOf(
+ intArrayOf(android.R.attr.state_hovered),
+ intArrayOf(android.R.attr.state_pressed),
+ intArrayOf(),
+ ),
+ intArrayOf(
+ replaceColorAlpha(color, OPACITY_11),
+ replaceColorAlpha(color, OPACITY_15),
+ Color.TRANSPARENT
+ )
+ ),
+ null /* content */,
+ LayerDrawable(arrayOf(
+ ShapeDrawable().apply {
+ shape = RoundRectShape(
+ FloatArray(8) { cornerRadius.toFloat() },
+ null /* inset */,
+ null /* innerRadii */
+ )
+ paint.color = Color.WHITE
+ }
+ )).apply {
+ require(numberOfLayers == 1) { "Must only contain one layer" }
+ setLayerInset(0 /* index */,
+ drawableInsets.l, drawableInsets.t, drawableInsets.r, drawableInsets.b)
+ }
+ )
+ }
+
+ private data class DrawableInsets(val l: Int, val t: Int, val r: Int, val b: Int) {
+ constructor(vertical: Int = 0, horizontal: Int = 0) :
+ this(horizontal, vertical, horizontal, vertical)
+ }
+
+ private data class Header(
+ val type: Type,
+ val appTheme: Theme,
+ val isFocused: Boolean,
+ val isAppearanceCaptionLight: Boolean,
+ ) {
+ enum class Type { DEFAULT, CUSTOM }
+ }
+
+ private data class HeaderStyle(
+ val background: Background,
+ val foreground: Foreground
+ ) {
+ data class Foreground(
+ @ColorInt val color: Int,
+ val opacity: Int
+ )
+
+ sealed class Background {
+ data object Transparent : Background()
+ data class Opaque(@ColorInt val color: Int) : Background()
+ }
+ }
+
+ @ColorInt
+ private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo): Int {
+ if (taskInfo.isTransparentCaptionBarAppearance) {
+ return Color.TRANSPARENT
+ }
+ val materialColorAttr: Int =
+ if (isDarkMode()) {
+ if (!taskInfo.isFocused) {
+ materialColorSurfaceContainerHigh
+ } else {
+ materialColorSurfaceDim
+ }
+ } else {
+ if (!taskInfo.isFocused) {
+ materialColorSurfaceContainerLow
+ } else {
+ materialColorSecondaryContainer
+ }
+ }
+ context.withStyledAttributes(null, intArrayOf(materialColorAttr), 0, 0) {
+ return getColor(0, 0)
+ }
+ return 0
+ }
+
+ @ColorInt
+ private fun getAppNameAndButtonColor(taskInfo: RunningTaskInfo): Int {
+ val materialColorAttr = when {
+ taskInfo.isTransparentCaptionBarAppearance &&
+ taskInfo.isLightCaptionBarAppearance -> materialColorOnSecondaryContainer
+ taskInfo.isTransparentCaptionBarAppearance &&
+ !taskInfo.isLightCaptionBarAppearance -> materialColorOnSurface
+ isDarkMode() -> materialColorOnSurface
+ else -> materialColorOnSecondaryContainer
+ }
+ val appDetailsOpacity = when {
+ isDarkMode() && !taskInfo.isFocused -> DARK_THEME_UNFOCUSED_OPACITY
+ !isDarkMode() && !taskInfo.isFocused -> LIGHT_THEME_UNFOCUSED_OPACITY
+ else -> FOCUSED_OPACITY
+ }
+ context.withStyledAttributes(null, intArrayOf(materialColorAttr), 0, 0) {
+ val color = getColor(0, 0)
+ return if (appDetailsOpacity == FOCUSED_OPACITY) {
+ color
+ } else {
+ Color.argb(
+ appDetailsOpacity,
+ Color.red(color),
+ Color.green(color),
+ Color.blue(color)
+ )
+ }
+ }
+ return 0
+ }
+
+ private fun isDarkMode(): Boolean {
+ return context.resources.configuration.uiMode and
+ Configuration.UI_MODE_NIGHT_MASK ==
+ Configuration.UI_MODE_NIGHT_YES
+ }
+
+ companion object {
+ private const val TAG = "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
+ }
+}
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/DesktopModeAppControlsWindowDecorationViewHolder.kt
deleted file mode 100644
index 58bbb030da01..000000000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt
+++ /dev/null
@@ -1,178 +0,0 @@
-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.graphics.Bitmap
-import android.graphics.Color
-import android.view.View
-import android.view.View.OnLongClickListener
-import android.widget.ImageButton
-import android.widget.ImageView
-import android.widget.TextView
-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.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.wm.shell.R
-import com.android.wm.shell.windowdecor.MaximizeButtonView
-import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance
-import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance
-
-/**
- * A desktop mode window decoration used when the window is floating (i.e. freeform). It hosts
- * finer controls such as a close window button and an "app info" section to pull up additional
- * controls.
- */
-internal class DesktopModeAppControlsWindowDecorationViewHolder(
- rootView: View,
- onCaptionTouchListener: View.OnTouchListener,
- onCaptionButtonClickListener: View.OnClickListener,
- onLongClickListener: OnLongClickListener,
- onCaptionGenericMotionListener: View.OnGenericMotionListener,
- appName: CharSequence,
- appIconBitmap: Bitmap,
- onMaximizeHoverAnimationFinishedListener: () -> Unit
-) : DesktopModeWindowDecorationViewHolder(rootView) {
-
- private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
- private val captionHandle: View = rootView.requireViewById(R.id.caption_handle)
- private val openMenuButton: View = rootView.requireViewById(R.id.open_menu_button)
- private val closeWindowButton: ImageButton = rootView.requireViewById(R.id.close_window)
- private val expandMenuButton: ImageButton = rootView.requireViewById(R.id.expand_menu_button)
- private val maximizeButtonView: MaximizeButtonView =
- rootView.requireViewById(R.id.maximize_button_view)
- private val maximizeWindowButton: ImageButton = rootView.requireViewById(R.id.maximize_window)
- private val appNameTextView: TextView = rootView.requireViewById(R.id.application_name)
- private val appIconImageView: ImageView = rootView.requireViewById(R.id.application_icon)
- val appNameTextWidth: Int
- get() = appNameTextView.width
-
- init {
- captionView.setOnTouchListener(onCaptionTouchListener)
- captionHandle.setOnTouchListener(onCaptionTouchListener)
- openMenuButton.setOnClickListener(onCaptionButtonClickListener)
- openMenuButton.setOnTouchListener(onCaptionTouchListener)
- closeWindowButton.setOnClickListener(onCaptionButtonClickListener)
- maximizeWindowButton.setOnClickListener(onCaptionButtonClickListener)
- maximizeWindowButton.setOnTouchListener(onCaptionTouchListener)
- maximizeWindowButton.setOnGenericMotionListener(onCaptionGenericMotionListener)
- maximizeWindowButton.onLongClickListener = onLongClickListener
- closeWindowButton.setOnTouchListener(onCaptionTouchListener)
- appNameTextView.text = appName
- appIconImageView.setImageBitmap(appIconBitmap)
- maximizeButtonView.onHoverAnimationFinishedListener =
- onMaximizeHoverAnimationFinishedListener
- }
-
- override fun bindData(taskInfo: RunningTaskInfo) {
- captionView.setBackgroundColor(getCaptionBackgroundColor(taskInfo))
- val color = getAppNameAndButtonColor(taskInfo)
- val alpha = Color.alpha(color)
- closeWindowButton.imageTintList = ColorStateList.valueOf(color)
- maximizeWindowButton.imageTintList = ColorStateList.valueOf(color)
- expandMenuButton.imageTintList = ColorStateList.valueOf(color)
- appNameTextView.isVisible = !taskInfo.isTransparentCaptionBarAppearance
- appNameTextView.setTextColor(color)
- appIconImageView.imageAlpha = alpha
- maximizeWindowButton.imageAlpha = alpha
- closeWindowButton.imageAlpha = alpha
- expandMenuButton.imageAlpha = alpha
-
- maximizeButtonView.setAnimationTints(isDarkMode())
- }
-
- override fun onHandleMenuOpened() {}
-
- override fun onHandleMenuClosed() {}
-
- fun setAnimatingTaskResize(animatingTaskResize: Boolean) {
- // If animating a task resize, cancel any running hover animations
- if (animatingTaskResize) {
- maximizeButtonView.cancelHoverAnimation()
- }
- maximizeButtonView.hoverDisabled = animatingTaskResize
- }
-
- fun onMaximizeWindowHoverExit() {
- maximizeButtonView.cancelHoverAnimation()
- }
-
- fun onMaximizeWindowHoverEnter() {
- maximizeButtonView.startHoverAnimation()
- }
-
- @ColorInt
- private fun getCaptionBackgroundColor(taskInfo: RunningTaskInfo): Int {
- if (taskInfo.isTransparentCaptionBarAppearance) {
- return Color.TRANSPARENT
- }
- val materialColorAttr: Int =
- if (isDarkMode()) {
- if (!taskInfo.isFocused) {
- materialColorSurfaceContainerHigh
- } else {
- materialColorSurfaceDim
- }
- } else {
- if (!taskInfo.isFocused) {
- materialColorSurfaceContainerLow
- } else {
- materialColorSecondaryContainer
- }
- }
- context.withStyledAttributes(null, intArrayOf(materialColorAttr), 0, 0) {
- return getColor(0, 0)
- }
- return 0
- }
-
- @ColorInt
- private fun getAppNameAndButtonColor(taskInfo: RunningTaskInfo): Int {
- val materialColorAttr = when {
- taskInfo.isTransparentCaptionBarAppearance &&
- taskInfo.isLightCaptionBarAppearance -> materialColorOnSecondaryContainer
- taskInfo.isTransparentCaptionBarAppearance &&
- !taskInfo.isLightCaptionBarAppearance -> materialColorOnSurface
- isDarkMode() -> materialColorOnSurface
- else -> materialColorOnSecondaryContainer
- }
- val appDetailsOpacity = when {
- isDarkMode() && !taskInfo.isFocused -> DARK_THEME_UNFOCUSED_OPACITY
- !isDarkMode() && !taskInfo.isFocused -> LIGHT_THEME_UNFOCUSED_OPACITY
- else -> FOCUSED_OPACITY
- }
- context.withStyledAttributes(null, intArrayOf(materialColorAttr), 0, 0) {
- val color = getColor(0, 0)
- return if (appDetailsOpacity == FOCUSED_OPACITY) {
- color
- } else {
- Color.argb(
- appDetailsOpacity,
- Color.red(color),
- Color.green(color),
- Color.blue(color)
- )
- }
- }
- return 0
- }
-
- private fun isDarkMode(): Boolean {
- return context.resources.configuration.uiMode and
- Configuration.UI_MODE_NIGHT_MASK ==
- Configuration.UI_MODE_NIGHT_YES
- }
-
- companion object {
- private const val TAG = "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
- }
-}
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/OWNERS b/libs/WindowManager/Shell/tests/OWNERS
index 0f24bb549158..b8a19ad35307 100644
--- a/libs/WindowManager/Shell/tests/OWNERS
+++ b/libs/WindowManager/Shell/tests/OWNERS
@@ -13,3 +13,5 @@ nmusgrave@google.com
pbdr@google.com
tkachenkoi@google.com
mpodolian@google.com
+jeremysim@google.com
+peanutbutter@google.com
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
index 5b2ffec67e93..f69a90cc793f 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
@@ -20,6 +20,8 @@
<option name="isolated-storage" value="false"/>
<target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <!-- disable DeprecatedTargetSdk warning -->
+ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
<!-- keeps the screen on during tests -->
<option name="screen-always-on" value="on"/>
<!-- prevents the phone from restarting -->
@@ -89,6 +91,7 @@
value="trace_config.textproto"
/>
<option name="instrumentation-arg" key="per_run" value="true"/>
+ <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/>
</test>
<!-- Needed for pulling the collected trace config on to the host -->
<metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
index 3380adac0b3f..e9eabb4162e3 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
@@ -17,10 +17,10 @@
package com.android.wm.shell.flicker.appcompat
import android.content.Context
-import android.tools.traces.component.ComponentNameMatcher
import android.tools.flicker.legacy.FlickerBuilder
import android.tools.flicker.legacy.FlickerTestData
import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.traces.component.ComponentNameMatcher
import com.android.server.wm.flicker.helpers.LetterboxAppHelper
import com.android.server.wm.flicker.helpers.setRotation
import com.android.wm.shell.flicker.BaseTest
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
index f08eba5a73a3..16c2d47f9db3 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt
@@ -18,11 +18,11 @@ package com.android.wm.shell.flicker.appcompat
import android.platform.test.annotations.Postsubmit
import android.tools.flicker.assertions.FlickerTest
-import android.tools.traces.component.ComponentNameMatcher
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 org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
index 826fc541687e..d85b7718aa56 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
@@ -19,11 +19,11 @@ package com.android.wm.shell.flicker.appcompat
import android.platform.test.annotations.Postsubmit
import android.tools.Rotation
import android.tools.flicker.assertions.FlickerTest
-import android.tools.traces.component.ComponentNameMatcher
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 org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
index 26e78bf625ba..164534c14d28 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
@@ -16,17 +16,17 @@
package com.android.wm.shell.flicker.appcompat
+import android.graphics.Rect
import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.RequiresDevice
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.datatypes.Rect
import android.tools.flicker.assertions.FlickerTest
-import android.tools.traces.component.ComponentNameMatcher
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 org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -260,7 +260,7 @@ class QuickSwitchLauncherToLetterboxAppTest(flicker: LegacyFlickerTest) : BaseAp
companion object {
/** {@inheritDoc} */
- private var startDisplayBounds = Rect.EMPTY
+ private var startDisplayBounds = Rect()
@Parameterized.Parameters(name = "{0}")
@JvmStatic
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
index 2aa84b4e55b8..034d54b185ed 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt
@@ -53,7 +53,7 @@ import org.junit.runners.Parameterized
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
class RepositionFixedPortraitAppTest(flicker: LegacyFlickerTest) : BaseAppCompat(flicker) {
- val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation).bounds
+ val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation)
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
get() = {
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt
index 7ffa23345589..22543aa9f773 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/src/com/android/wm/shell/flicker/appcompat/RotateImmersiveAppInFullscreenTest.kt
@@ -16,19 +16,19 @@
package com.android.wm.shell.flicker.appcompat
+import android.graphics.Rect
import android.os.Build
import android.platform.test.annotations.Postsubmit
import android.system.helpers.CommandsHelper
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.datatypes.Rect
import android.tools.flicker.assertions.FlickerTest
-import android.tools.traces.component.ComponentNameMatcher
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.helpers.FIND_TIMEOUT
+import android.tools.traces.component.ComponentNameMatcher
import android.tools.traces.parsers.toFlickerComponent
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
@@ -167,7 +167,7 @@ class RotateImmersiveAppInFullscreenTest(flicker: LegacyFlickerTest) : BaseAppCo
}
companion object {
- private var startDisplayBounds = Rect.EMPTY
+ private var startDisplayBounds = Rect()
const val LAUNCHER_PACKAGE = "com.google.android.apps.nexuslauncher"
/**
diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
index 9f7d9fcf1326..b76d06565700 100644
--- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
@@ -20,6 +20,8 @@
<option name="isolated-storage" value="false"/>
<target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <!-- disable DeprecatedTargetSdk warning -->
+ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
<!-- keeps the screen on during tests -->
<option name="screen-always-on" value="on"/>
<!-- prevents the phone from restarting -->
@@ -89,6 +91,7 @@
value="trace_config.textproto"
/>
<option name="instrumentation-arg" key="per_run" value="true"/>
+ <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/>
</test>
<!-- Needed for pulling the collected trace config on to the host -->
<metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
index bc486c277aa5..984abf8cf8b4 100644
--- a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
@@ -32,7 +32,7 @@ import org.junit.runners.Parameterized
/**
* Test launching a new activity from bubble.
*
- * To run this test: `atest WMShellFlickerTests:MultiBubblesScreen`
+ * To run this test: `atest WMShellFlickerTestsBubbles:ChangeActiveActivityFromBubbleTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
index 521c0d0aaeb7..886b70c5e464 100644
--- a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/DragToDismissBubbleScreenTest.kt
@@ -19,11 +19,11 @@ package com.android.wm.shell.flicker.bubble
import android.content.Context
import android.graphics.Point
import android.platform.test.annotations.Presubmit
-import android.tools.flicker.subject.layers.LayersTraceSubject
-import android.tools.traces.component.ComponentNameMatcher
import android.tools.flicker.junit.FlickerParametersRunnerFactory
import android.tools.flicker.legacy.FlickerBuilder
import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.flicker.subject.layers.LayersTraceSubject
+import android.tools.traces.component.ComponentNameMatcher
import android.util.DisplayMetrics
import android.view.WindowManager
import androidx.test.uiautomator.By
@@ -35,7 +35,7 @@ import org.junit.runners.Parameterized
/**
* Test launching a new activity from bubble.
*
- * To run this test: `atest WMShellFlickerTests:DismissBubbleScreen`
+ * To run this test: `atest WMShellFlickerTestsBubbles:DragToDismissBubbleScreenTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt
index e059ac78dc6b..2ee53f4fce66 100644
--- a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleOnLocksreenTest.kt
@@ -17,10 +17,10 @@
package com.android.wm.shell.flicker.bubble
import android.platform.test.annotations.Postsubmit
-import android.tools.traces.component.ComponentNameMatcher
import android.tools.flicker.junit.FlickerParametersRunnerFactory
import android.tools.flicker.legacy.FlickerBuilder
import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.traces.component.ComponentNameMatcher
import android.view.WindowInsets
import android.view.WindowManager
import androidx.test.filters.FlakyTest
@@ -38,7 +38,7 @@ import org.junit.runners.Parameterized
/**
* Test launching a new activity from bubble.
*
- * To run this test: `atest WMShellFlickerTests:OpenActivityFromBubbleOnLocksreenTest`
+ * To run this test: `atest WMShellFlickerTestsBubbles:OpenActivityFromBubbleOnLocksreenTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt
index ef7fbfb79beb..463fe0e60da3 100644
--- a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/OpenActivityFromBubbleTest.kt
@@ -29,7 +29,7 @@ import org.junit.runners.Parameterized
/**
* Test launching a new activity from bubble.
*
- * To run this test: `atest WMShellFlickerTests:ExpandBubbleScreen`
+ * To run this test: `atest WMShellFlickerTestsBubbles:OpenActivityFromBubbleTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
index 87224b151b78..8df50567a29c 100644
--- a/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/bubble/src/com/android/wm/shell/flicker/bubble/SendBubbleNotificationTest.kt
@@ -29,7 +29,7 @@ import org.junit.runners.Parameterized
/**
* Test creating a bubble notification
*
- * To run this test: `atest WMShellFlickerTests:LaunchBubbleScreen`
+ * To run this test: `atest WMShellFlickerTestsBubbles:SendBubbleNotificationTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
index 882b200da3a2..041978c371ff 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
@@ -20,6 +20,8 @@
<option name="isolated-storage" value="false"/>
<target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <!-- disable DeprecatedTargetSdk warning -->
+ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
<!-- keeps the screen on during tests -->
<option name="screen-always-on" value="on"/>
<!-- prevents the phone from restarting -->
@@ -89,6 +91,7 @@
value="trace_config.textproto"
/>
<option name="instrumentation-arg" key="per_run" value="true"/>
+ <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/>
</test>
<!-- Needed for pulling the collected trace config on to the host -->
<metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
index f5a8655b81f0..bf040d2a95f4 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
@@ -20,6 +20,8 @@
<option name="isolated-storage" value="false"/>
<target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <!-- disable DeprecatedTargetSdk warning -->
+ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
<!-- keeps the screen on during tests -->
<option name="screen-always-on" value="on"/>
<!-- prevents the phone from restarting -->
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
index d64bfed382b9..b85d7936efc2 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
@@ -33,7 +33,7 @@ import org.junit.runners.Parameterized
/**
* Test entering pip from an app via auto-enter property when navigating to home.
*
- * To run this test: `atest WMShellFlickerTests:AutoEnterPipOnGoToHomeTest`
+ * To run this test: `atest WMShellFlickerTestsPip1:AutoEnterPipOnGoToHomeTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt
index a0edcfb17971..d059211088aa 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipWithSourceRectHintTest.kt
@@ -17,10 +17,10 @@
package com.android.wm.shell.flicker.pip
import android.platform.test.annotations.Presubmit
-import android.tools.traces.component.ComponentNameMatcher
import android.tools.flicker.junit.FlickerParametersRunnerFactory
import android.tools.flicker.legacy.FlickerBuilder
import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.traces.component.ComponentNameMatcher
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -30,7 +30,7 @@ import org.junit.runners.Parameterized
/**
* Test auto entering pip using a source rect hint.
*
- * To run this test: `atest AutoEnterPipWithSourceRectHintTest`
+ * To run this test: `atest WMShellFlickerTestsPip1:AutoEnterPipWithSourceRectHintTest`
*
* Actions:
* ```
@@ -66,9 +66,7 @@ class AutoEnterPipWithSourceRectHintTest(flicker: LegacyFlickerTest) :
@Test
fun pipOverlayNotShown() {
val overlay = ComponentNameMatcher.PIP_CONTENT_OVERLAY
- flicker.assertLayers {
- this.notContains(overlay)
- }
+ flicker.assertLayers { this.notContains(overlay) }
}
@Presubmit
@Test
@@ -83,4 +81,4 @@ class AutoEnterPipWithSourceRectHintTest(flicker: LegacyFlickerTest) :
// auto enter and sourceRectHint that causes the app to move outside of the display
// bounds during the transition.
}
-} \ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt
index 031acf4919eb..a5e0550d9c79 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipBySwipingDownTest.kt
@@ -17,10 +17,10 @@
package com.android.wm.shell.flicker.pip
import android.platform.test.annotations.Presubmit
-import android.tools.traces.component.ComponentNameMatcher
import android.tools.flicker.junit.FlickerParametersRunnerFactory
import android.tools.flicker.legacy.FlickerBuilder
import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.traces.component.ComponentNameMatcher
import com.android.wm.shell.flicker.pip.common.ClosePipTransition
import org.junit.FixMethodOrder
import org.junit.Test
@@ -31,7 +31,7 @@ import org.junit.runners.Parameterized
/**
* Test closing a pip window by swiping it to the bottom-center of the screen
*
- * To run this test: `atest WMShellFlickerTests:ExitPipWithSwipeDownTest`
+ * To run this test: `atest WMShellFlickerTestsPip1:ClosePipBySwipingDownTest`
*
* Actions:
* ```
@@ -69,7 +69,8 @@ class ClosePipBySwipingDownTest(flicker: LegacyFlickerTest) : ClosePipTransition
wmHelper.currentState.layerState
.getLayerWithBuffer(barComponent)
?.visibleRegion
- ?.height
+ ?.bounds
+ ?.height()
?: error("Couldn't find Nav or Task bar layer")
// The dismiss button doesn't appear at the complete bottom of the screen,
// it appears above the hot seat but `hotseatBarSize` is not available outside
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt
index 860307f2bb76..d177624378c1 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ClosePipWithDismissButtonTest.kt
@@ -30,7 +30,7 @@ import org.junit.runners.Parameterized
/**
* Test closing a pip window via the dismiss button
*
- * To run this test: `atest WMShellFlickerTests:ExitPipWithDismissButtonTest`
+ * To run this test: `atest WMShellFlickerTestsPip1:ClosePipWithDismissButtonTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
index c5541613fece..a86803d058f8 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
@@ -31,7 +31,7 @@ import org.junit.runners.Parameterized
/**
* Test entering pip from an app via [onUserLeaveHint] and by navigating to home.
*
- * To run this test: `atest WMShellFlickerTests:EnterPipOnUserLeaveHintTest`
+ * To run this test: `atest WMShellFlickerTestsPip2:EnterPipOnUserLeaveHintTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
index 9a1bd267ea1f..a0a61fe2cf72 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientation.kt
@@ -21,12 +21,12 @@ import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import android.tools.Rotation
import android.tools.flicker.assertions.FlickerTest
-import android.tools.traces.component.ComponentNameMatcher
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.helpers.WindowUtils
+import android.tools.traces.component.ComponentNameMatcher
import androidx.test.filters.FlakyTest
import com.android.server.wm.flicker.entireScreenCovered
import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper
@@ -46,7 +46,7 @@ import org.junit.runners.Parameterized
/**
* Test entering pip while changing orientation (from app in landscape to pip window in portrait)
*
- * To run this test: `atest WMShellFlickerTests:EnterPipToOtherOrientationTest`
+ * To run this test: `atest WMShellFlickerTestsPip2:EnterPipToOtherOrientation`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt
index f97d8d1842b0..d92f55af578f 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipViaAppUiButtonTest.kt
@@ -28,7 +28,7 @@ import org.junit.runners.Parameterized
/**
* Test entering pip from an app by interacting with the app UI
*
- * To run this test: `atest WMShellFlickerTests:EnterPipTest`
+ * To run this test: `atest WMShellFlickerTestsPip2:EnterPipViaAppUiButtonTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt
index 47bf41814d17..8c0817d6e287 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt
@@ -28,7 +28,7 @@ import org.junit.runners.Parameterized
/**
* Test expanding a pip window back to full screen via the expand button
*
- * To run this test: `atest WMShellFlickerTests:ExitPipViaExpandButtonClickTest`
+ * To run this test: `atest WMShellFlickerTestsPip2:ExitPipToAppViaExpandButtonTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt
index a356e68d14dd..90a9623056ce 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaIntentTest.kt
@@ -28,7 +28,7 @@ import org.junit.runners.Parameterized
/**
* Test expanding a pip window back to full screen via an intent
*
- * To run this test: `atest WMShellFlickerTests:ExitPipViaIntentTest`
+ * To run this test: `atest WMShellFlickerTestsPip2:ExitPipToAppViaIntentTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
index 25614ef63ccc..9306c77a1c43 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExpandPipOnDoubleClickTest.kt
@@ -18,11 +18,11 @@ package com.android.wm.shell.flicker.pip
import android.platform.test.annotations.Presubmit
import android.tools.Rotation
-import android.tools.traces.component.ComponentNameMatcher
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 com.android.wm.shell.flicker.pip.common.PipTransition
import org.junit.FixMethodOrder
import org.junit.Test
@@ -33,7 +33,7 @@ import org.junit.runners.Parameterized
/**
* Test expanding a pip window by double-clicking it
*
- * To run this test: `atest WMShellFlickerTests:ExpandPipOnDoubleClickTest`
+ * To run this test: `atest WMShellFlickerTestsPip2:ExpandPipOnDoubleClickTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt
index b94989d98e97..cb8ee27f29e2 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenAutoEnterPipOnGoToHomeTest.kt
@@ -24,6 +24,7 @@ import android.tools.flicker.legacy.LegacyFlickerTest
import android.tools.flicker.legacy.LegacyFlickerTestFactory
import android.tools.helpers.WindowUtils
import android.tools.traces.parsers.toFlickerComponent
+import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.server.wm.flicker.testapp.ActivityOptions
@@ -38,7 +39,7 @@ import org.junit.runners.Parameterized
/**
* Test entering pip from an app via auto-enter property when navigating to home from split screen.
*
- * To run this test: `atest WMShellFlickerTests:AutoEnterPipOnGoToHomeTest`
+ * To run this test: `atest WMShellFlickerTestsPip1:FromSplitScreenAutoEnterPipOnGoToHomeTest`
*
* Actions:
* ```
@@ -143,6 +144,10 @@ class FromSplitScreenAutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) :
}
}
+ @FlakyTest(bugId = 293133362)
+ @Test
+ override fun entireScreenCovered() = super.entireScreenCovered()
+
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
index 1ccc7d8084a6..d03d7799d675 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
@@ -24,6 +24,7 @@ import android.tools.flicker.legacy.LegacyFlickerTest
import android.tools.flicker.legacy.LegacyFlickerTestFactory
import android.tools.helpers.WindowUtils
import android.tools.traces.parsers.toFlickerComponent
+import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.server.wm.flicker.testapp.ActivityOptions
@@ -39,7 +40,7 @@ import org.junit.runners.Parameterized
/**
* Test entering pip from an app via auto-enter property when navigating to home from split screen.
*
- * To run this test: `atest WMShellFlickerTests:AutoEnterPipOnGoToHomeTest`
+ * To run this test: `atest WMShellFlickerTestsPip1:FromSplitScreenEnterPipOnUserLeaveHintTest`
*
* Actions:
* ```
@@ -181,11 +182,25 @@ class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) :
}
}
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 312446524)
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
+ /** {@inheritDoc} */
+ @Test
+ @FlakyTest(bugId = 336510055)
+ override fun entireScreenCovered() {
+ super.entireScreenCovered()
+ }
+
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
- fun getParams() = LegacyFlickerTestFactory.nonRotationTests(
- supportedRotations = listOf(Rotation.ROTATION_0)
- )
+ fun getParams() =
+ LegacyFlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
index 9b746224a1a0..265eb4416a2b 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
@@ -32,7 +32,7 @@ import org.junit.runners.Parameterized
/**
* Test Pip movement with Launcher shelf height change (increase).
*
- * To run this test: `atest WMShellFlickerTests:MovePipUpShelfHeightChangeTest`
+ * To run this test: `atest WMShellFlickerTestsPip3:MovePipDownOnShelfHeightChange`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt
index e184cf04e4ae..04fedf4f2550 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipOnImeVisibilityChangeTest.kt
@@ -19,12 +19,12 @@ package com.android.wm.shell.flicker.pip
import android.platform.test.annotations.Presubmit
import android.tools.Rotation
import android.tools.flicker.assertions.FlickerTest
-import android.tools.traces.component.ComponentNameMatcher
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.helpers.WindowUtils
+import android.tools.traces.component.ComponentNameMatcher
import com.android.server.wm.flicker.helpers.ImeAppHelper
import com.android.server.wm.flicker.helpers.setRotation
import com.android.wm.shell.flicker.pip.common.PipTransition
@@ -34,7 +34,10 @@ import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.junit.runners.Parameterized
-/** Test Pip launch. To run this test: `atest WMShellFlickerTests:PipKeyboardTest` */
+/**
+ * Test Pip launch. To run this test:
+ * `atest WMShellFlickerTestsPip3:MovePipOnImeVisibilityChangeTest`
+ */
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
index 490ebd190ee8..8d6be64da21d 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
@@ -32,7 +32,7 @@ import org.junit.runners.Parameterized
/**
* Test Pip movement with Launcher shelf height change (decrease).
*
- * To run this test: `atest WMShellFlickerTests:MovePipDownShelfHeightChangeTest`
+ * To run this test: `atest WMShellFlickerTestsPip3:MovePipUpOnShelfHeightChangeTest`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt
index 68417066ac0a..16d08e5e9055 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/PipPinchInTest.kt
@@ -18,11 +18,11 @@ package com.android.wm.shell.flicker.pip
import android.platform.test.annotations.Presubmit
import android.tools.Rotation
-import android.tools.flicker.subject.exceptions.IncorrectRegionException
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.flicker.subject.exceptions.IncorrectRegionException
import androidx.test.filters.RequiresDevice
import com.android.wm.shell.flicker.pip.common.PipTransition
import org.junit.FixMethodOrder
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt
index 9a6dacb187ef..ed2a0a718c6c 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/SetRequestedOrientationWhilePinned.kt
@@ -41,8 +41,8 @@ import org.junit.runners.MethodSorters
import org.junit.runners.Parameterized
/**
- * Test exiting Pip with orientation changes. To run this test: `atest
- * WMShellFlickerTests:SetRequestedOrientationWhilePinnedTest`
+ * Test exiting Pip with orientation changes. To run this test:
+ * `atest WMShellFlickerTestsPip1:SetRequestedOrientationWhilePinned`
*/
@RequiresDevice
@RunWith(Parameterized::class)
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt
index d2f803ec9352..9109eafacf63 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ShowPipAndRotateDisplay.kt
@@ -35,7 +35,7 @@ import org.junit.runners.Parameterized
/**
* Test Pip Stack in bounds after rotations.
*
- * To run this test: `atest WMShellFlickerTests:PipRotationTest`
+ * To run this test: `atest WMShellFlickerTestsPip1:ShowPipAndRotateDisplay`
*
* Actions:
* ```
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt
index c9f4a6ca75b1..65b60ce1022b 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/AppsEnterPipTransition.kt
@@ -18,12 +18,12 @@ package com.android.wm.shell.flicker.pip.apps
import android.platform.test.annotations.Postsubmit
import android.tools.Rotation
-import android.tools.traces.component.ComponentNameMatcher
import android.tools.device.apphelpers.StandardAppHelper
import android.tools.flicker.junit.FlickerBuilderProvider
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 com.android.wm.shell.flicker.pip.common.EnterPipTransition
import org.junit.Test
import org.junit.runners.Parameterized
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt
index 88650107e63a..1fc9d9910a15 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/MapsEnterPipTest.kt
@@ -65,8 +65,8 @@ import org.junit.runners.Parameterized
open class MapsEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransition(flicker) {
override val standardAppHelper: MapsAppHelper = MapsAppHelper(instrumentation)
- override val permissions: Array<String> = arrayOf(Manifest.permission.POST_NOTIFICATIONS,
- Manifest.permission.ACCESS_FINE_LOCATION)
+ override val permissions: Array<String> =
+ arrayOf(Manifest.permission.POST_NOTIFICATIONS, Manifest.permission.ACCESS_FINE_LOCATION)
val locationManager: LocationManager =
instrumentation.context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt
index 9b5153875987..3a0eeb67995b 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/NetflixEnterPipTest.kt
@@ -18,14 +18,14 @@ package com.android.wm.shell.flicker.pip.apps
import android.Manifest
import android.platform.test.annotations.Postsubmit
-import android.tools.NavBar
import android.tools.Rotation
-import android.tools.traces.component.ComponentNameMatcher
import android.tools.device.apphelpers.NetflixAppHelper
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.helpers.WindowUtils
+import android.tools.traces.component.ComponentNameMatcher
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.statusBarLayerPositionAtEnd
import org.junit.Assume
@@ -62,6 +62,8 @@ import org.junit.runners.Parameterized
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
open class NetflixEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransition(flicker) {
override val standardAppHelper: NetflixAppHelper = NetflixAppHelper(instrumentation)
+ private val startingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_90)
+ private val endingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0)
override val permissions: Array<String> = arrayOf(Manifest.permission.POST_NOTIFICATIONS)
@@ -134,6 +136,31 @@ open class NetflixEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransit
// Netflix plays in immersive fullscreen mode, so taskbar will be gone at some point
}
+ @Postsubmit
+ @Test
+ override fun pipWindowRemainInsideVisibleBounds() {
+ // during the transition we assert the center point is within the display bounds, since it
+ // might go outside of bounds as we resize from landscape fullscreen to destination bounds,
+ // and once the animation is over we assert that it's fully within the display bounds, at
+ // which point the device also performs orientation change from landscape to portrait
+ flicker.assertWmVisibleRegion(standardAppHelper.packageNameMatcher) {
+ regionsCenterPointInside(startingBounds).then().coversAtMost(endingBounds)
+ }
+ }
+
+ @Postsubmit
+ @Test
+ override fun pipLayerOrOverlayRemainInsideVisibleBounds() {
+ // during the transition we assert the center point is within the display bounds, since it
+ // might go outside of bounds as we resize from landscape fullscreen to destination bounds,
+ // and once the animation is over we assert that it's fully within the display bounds, at
+ // which point the device also performs orientation change from landscape to portrait
+ // since Netflix uses source rect hint, there is no PiP overlay present
+ flicker.assertLayersVisibleRegion(standardAppHelper.packageNameMatcher) {
+ regionsCenterPointInside(startingBounds).then().coversAtMost(endingBounds)
+ }
+ }
+
companion object {
/**
* Creates the test configurations.
@@ -145,8 +172,7 @@ open class NetflixEnterPipTest(flicker: LegacyFlickerTest) : AppsEnterPipTransit
@JvmStatic
fun getParams() =
LegacyFlickerTestFactory.nonRotationTests(
- supportedRotations = listOf(Rotation.ROTATION_0),
- supportedNavigationModes = listOf(NavBar.MODE_GESTURAL)
+ supportedRotations = listOf(Rotation.ROTATION_0)
)
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt
index 3ae5937df4d0..35ed8de3a464 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipTest.kt
@@ -18,11 +18,11 @@ package com.android.wm.shell.flicker.pip.apps
import android.Manifest
import android.platform.test.annotations.Postsubmit
-import android.tools.traces.component.ComponentNameMatcher
import android.tools.device.apphelpers.YouTubeAppHelper
import android.tools.flicker.junit.FlickerParametersRunnerFactory
import android.tools.flicker.legacy.FlickerBuilder
import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.traces.component.ComponentNameMatcher
import androidx.test.filters.RequiresDevice
import org.junit.Assume
import org.junit.FixMethodOrder
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipToOtherOrientationTest.kt
new file mode 100644
index 000000000000..879034f32514
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/apps/YouTubeEnterPipToOtherOrientationTest.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.pip.apps
+
+import android.Manifest
+import android.platform.test.annotations.Postsubmit
+import android.tools.Rotation
+import android.tools.device.apphelpers.YouTubeAppHelper
+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.helpers.WindowUtils
+import android.tools.traces.component.ComponentNameMatcher
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.statusBarLayerPositionAtEnd
+import org.junit.Assume
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test entering pip from YouTube app by interacting with the app UI
+ *
+ * To run this test: `atest WMShellFlickerTests:YouTubeEnterPipTest`
+ *
+ * Actions:
+ * ```
+ * Launch YouTube and start playing a video
+ * Make the video fullscreen, aka immersive mode
+ * Go home to enter PiP
+ * ```
+ *
+ * Notes:
+ * ```
+ * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ * are inherited from [PipTransition]
+ * 2. Part of the test setup occurs automatically via
+ * [android.tools.flicker.legacy.runner.TransitionRunner],
+ * including configuring navigation mode, initial orientation and ensuring no
+ * apps are running before setup
+ * ```
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+open class YouTubeEnterPipToOtherOrientationTest(flicker: LegacyFlickerTest) :
+ YouTubeEnterPipTest(flicker) {
+ override val standardAppHelper: YouTubeAppHelper = YouTubeAppHelper(instrumentation)
+ private val startingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_90)
+ private val endingBounds = WindowUtils.getDisplayBounds(Rotation.ROTATION_0)
+
+ override val permissions: Array<String> = arrayOf(Manifest.permission.POST_NOTIFICATIONS)
+
+ override val defaultEnterPip: FlickerBuilder.() -> Unit = {
+ setup {
+ standardAppHelper.launchViaIntent(
+ wmHelper,
+ YouTubeAppHelper.getYoutubeVideoIntent("HPcEAtoXXLA"),
+ ComponentNameMatcher(YouTubeAppHelper.PACKAGE_NAME, "")
+ )
+ standardAppHelper.enterFullscreen()
+ standardAppHelper.waitForVideoPlaying()
+ }
+ }
+
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ transitions { tapl.goHomeFromImmersiveFullscreenApp() }
+ }
+
+ @Postsubmit
+ @Test
+ override fun taskBarLayerIsVisibleAtStartAndEnd() {
+ Assume.assumeTrue(flicker.scenario.isTablet)
+ // YouTube starts in immersive fullscreen mode, so taskbar bar is not visible at start
+ flicker.assertLayersStart { this.isInvisible(ComponentNameMatcher.TASK_BAR) }
+ flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.TASK_BAR) }
+ }
+
+ @Postsubmit
+ @Test
+ override fun pipWindowRemainInsideVisibleBounds() {
+ // during the transition we assert the center point is within the display bounds, since it
+ // might go outside of bounds as we resize from landscape fullscreen to destination bounds,
+ // and once the animation is over we assert that it's fully within the display bounds, at
+ // which point the device also performs orientation change from landscape to portrait
+ flicker.assertWmVisibleRegion(standardAppHelper.packageNameMatcher) {
+ regionsCenterPointInside(startingBounds).then().coversAtMost(endingBounds)
+ }
+ }
+
+ @Postsubmit
+ @Test
+ override fun pipLayerOrOverlayRemainInsideVisibleBounds() {
+ // during the transition we assert the center point is within the display bounds, since it
+ // might go outside of bounds as we resize from landscape fullscreen to destination bounds,
+ // and once the animation is over we assert that it's fully within the display bounds, at
+ // which point the device also performs orientation change from landscape to portrait
+ // since YouTube uses source rect hint, there is no PiP overlay present
+ flicker.assertLayersVisibleRegion(standardAppHelper.packageNameMatcher) {
+ regionsCenterPointInside(startingBounds).then().coversAtMost(endingBounds)
+ }
+ }
+
+ @Postsubmit
+ @Test
+ override fun taskBarWindowIsAlwaysVisible() {
+ // YouTube plays in immersive fullscreen mode, so taskbar will be gone at some point
+ }
+
+ @Postsubmit
+ @Test
+ override fun statusBarLayerIsVisibleAtStartAndEnd() {
+ // YouTube starts in immersive fullscreen mode, so status bar is not visible at start
+ flicker.assertLayersStart { this.isInvisible(ComponentNameMatcher.STATUS_BAR) }
+ flicker.assertLayersEnd { this.isVisible(ComponentNameMatcher.STATUS_BAR) }
+ }
+
+ @Postsubmit
+ @Test
+ override fun statusBarLayerPositionAtStartAndEnd() {
+ // YouTube starts in immersive fullscreen mode, so status bar is not visible at start
+ flicker.statusBarLayerPositionAtEnd()
+ }
+
+ @Postsubmit
+ @Test
+ override fun statusBarWindowIsAlwaysVisible() {
+ // YouTube plays in immersive fullscreen mode, so taskbar will be gone at some point
+ }
+
+ companion object {
+ /**
+ * Creates the test configurations.
+ *
+ * See [LegacyFlickerTestFactory.nonRotationTests] for configuring repetitions, screen
+ * orientation and navigation modes.
+ */
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams() =
+ LegacyFlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt
index dc122590388f..8cb81b46cf4d 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ClosePipTransition.kt
@@ -18,10 +18,10 @@ package com.android.wm.shell.flicker.pip.common
import android.platform.test.annotations.Presubmit
import android.tools.Rotation
-import android.tools.traces.component.ComponentNameMatcher.Companion.LAUNCHER
import android.tools.flicker.legacy.FlickerBuilder
import android.tools.flicker.legacy.LegacyFlickerTest
import android.tools.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.traces.component.ComponentNameMatcher.Companion.LAUNCHER
import com.android.server.wm.flicker.helpers.setRotation
import org.junit.Test
import org.junit.runners.Parameterized
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/EnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/EnterPipTransition.kt
index 3d9eae62b499..6dd3a175da65 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/EnterPipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/EnterPipTransition.kt
@@ -18,10 +18,10 @@ package com.android.wm.shell.flicker.pip.common
import android.platform.test.annotations.Presubmit
import android.tools.Rotation
-import android.tools.traces.component.ComponentNameMatcher
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 org.junit.Test
import org.junit.runners.Parameterized
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt
index 7b6839dc123f..0742cf9c5887 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/ExitPipToAppTransition.kt
@@ -18,9 +18,9 @@ package com.android.wm.shell.flicker.pip.common
import android.platform.test.annotations.Presubmit
import android.tools.Rotation
-import android.tools.traces.component.ComponentNameMatcher
import android.tools.flicker.legacy.LegacyFlickerTest
import android.tools.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.traces.component.ComponentNameMatcher
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import org.junit.Test
import org.junit.runners.Parameterized
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/MovePipShelfHeightTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/MovePipShelfHeightTransition.kt
index f4baf5f75928..c4881e7e17a1 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/MovePipShelfHeightTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/MovePipShelfHeightTransition.kt
@@ -18,9 +18,9 @@ package com.android.wm.shell.flicker.pip.common
import android.platform.test.annotations.Presubmit
import android.tools.Rotation
-import android.tools.flicker.subject.region.RegionSubject
import android.tools.flicker.legacy.LegacyFlickerTest
import android.tools.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.flicker.subject.region.RegionSubject
import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper
import com.android.wm.shell.flicker.utils.Direction
import org.junit.Test
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt
index fd467e32e0dc..99c1ad2aaa4e 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt
@@ -20,11 +20,11 @@ import android.app.Instrumentation
import android.content.Intent
import android.platform.test.annotations.Presubmit
import android.tools.Rotation
-import android.tools.traces.component.ComponentNameMatcher
import android.tools.flicker.legacy.FlickerBuilder
import android.tools.flicker.legacy.LegacyFlickerTest
import android.tools.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome
import android.tools.helpers.WindowUtils
+import android.tools.traces.component.ComponentNameMatcher
import com.android.server.wm.flicker.helpers.PipAppHelper
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.testapp.ActivityOptions
diff --git a/libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml
index 51a55e359acf..a66dfb4566f9 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/service/AndroidTestTemplate.xml
@@ -20,6 +20,8 @@
<option name="isolated-storage" value="false"/>
<target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <!-- disable DeprecatedTargetSdk warning -->
+ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
<!-- keeps the screen on during tests -->
<option name="screen-always-on" value="on"/>
<!-- prevents the phone from restarting -->
@@ -89,6 +91,7 @@
value="trace_config.textproto"
/>
<option name="instrumentation-arg" key="per_run" value="true"/>
+ <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/>
</test>
<!-- Needed for pulling the collected trace config on to the host -->
<metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/OWNERS b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/OWNERS
new file mode 100644
index 000000000000..73a5a23909c5
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/OWNERS
@@ -0,0 +1,5 @@
+# Android > Android OS & Apps > Framework (Java + Native) > Window Manager > WM Shell > Freeform
+# Bug component: 929241
+
+uysalorhan@google.com
+pragyabajoria@google.com \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt
new file mode 100644
index 000000000000..5563bb9fa934
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitLandscape.kt
@@ -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.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.flicker
+
+import android.tools.Rotation
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class CloseAllAppWithAppHeaderExitLandscape : CloseAllAppsWithAppHeaderExit(Rotation.ROTATION_90) {
+ @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"])
+ @Test
+ override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig()
+ .use(FlickerServiceConfig.DEFAULT)
+ .use(CLOSE_APP)
+ .use(CLOSE_LAST_APP)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt
new file mode 100644
index 000000000000..3d16d2219c78
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/CloseAllAppWithAppHeaderExitPortrait.kt
@@ -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.
+ */
+
+package com.android.wm.shell.flicker.service.desktopmode.flicker
+
+import android.tools.Rotation
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_APP
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CLOSE_LAST_APP
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.CloseAllAppsWithAppHeaderExit
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class CloseAllAppWithAppHeaderExitPortrait : CloseAllAppsWithAppHeaderExit(Rotation.ROTATION_0) {
+ @ExpectedScenarios(["CLOSE_APP", "CLOSE_LAST_APP"])
+ @Test
+ override fun closeAllAppsInDesktop() = super.closeAllAppsInDesktop()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig()
+ .use(FlickerServiceConfig.DEFAULT)
+ .use(CLOSE_APP)
+ .use(CLOSE_LAST_APP)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt
new file mode 100644
index 000000000000..d485b82f5ddb
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/DesktopModeFlickerScenarios.kt
@@ -0,0 +1,144 @@
+/*
+ * 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.service.desktopmode.flicker
+
+import android.tools.flicker.AssertionInvocationGroup
+import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd
+import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways
+import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart
+import android.tools.flicker.assertors.assertions.AppWindowHasDesktopModeInitialBoundsAtTheEnd
+import android.tools.flicker.assertors.assertions.AppWindowIsVisibleAlways
+import android.tools.flicker.assertors.assertions.AppWindowOnTopAtEnd
+import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart
+import android.tools.flicker.assertors.assertions.AppWindowRemainInsideDisplayBounds
+import android.tools.flicker.assertors.assertions.LauncherWindowMovesToTop
+import android.tools.flicker.config.AssertionTemplates
+import android.tools.flicker.config.FlickerConfigEntry
+import android.tools.flicker.config.ScenarioId
+import android.tools.flicker.config.desktopmode.Components
+import android.tools.flicker.extractors.ITransitionMatcher
+import android.tools.flicker.extractors.ShellTransitionScenarioExtractor
+import android.tools.traces.wm.Transition
+import android.tools.traces.wm.TransitionType
+
+class DesktopModeFlickerScenarios {
+ companion object {
+ val END_DRAG_TO_DESKTOP =
+ FlickerConfigEntry(
+ scenarioId = ScenarioId("END_DRAG_TO_DESKTOP"),
+ extractor =
+ ShellTransitionScenarioExtractor(
+ transitionMatcher =
+ object : ITransitionMatcher {
+ override fun findAll(
+ transitions: Collection<Transition>
+ ): Collection<Transition> {
+ return transitions.filter {
+ it.type == TransitionType.DESKTOP_MODE_END_DRAG_TO_DESKTOP
+ }
+ }
+ }
+ ),
+ assertions =
+ AssertionTemplates.COMMON_ASSERTIONS +
+ listOf(
+ AppLayerIsVisibleAlways(Components.DESKTOP_MODE_APP),
+ AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP),
+ AppWindowHasDesktopModeInitialBoundsAtTheEnd(
+ Components.DESKTOP_MODE_APP
+ )
+ )
+ .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ )
+
+ val CLOSE_APP =
+ FlickerConfigEntry(
+ scenarioId = ScenarioId("CLOSE_APP"),
+ extractor =
+ ShellTransitionScenarioExtractor(
+ transitionMatcher =
+ object : ITransitionMatcher {
+ override fun findAll(
+ transitions: Collection<Transition>
+ ): Collection<Transition> {
+ return transitions.filter { it.type == TransitionType.CLOSE }
+ }
+ }
+ ),
+ assertions =
+ AssertionTemplates.COMMON_ASSERTIONS +
+ listOf(
+ AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP),
+ AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP),
+ AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP),
+ )
+ .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ )
+
+ val CLOSE_LAST_APP =
+ FlickerConfigEntry(
+ scenarioId = ScenarioId("CLOSE_LAST_APP"),
+ extractor =
+ ShellTransitionScenarioExtractor(
+ transitionMatcher =
+ object : ITransitionMatcher {
+ override fun findAll(
+ transitions: Collection<Transition>
+ ): Collection<Transition> {
+ val lastTransition =
+ transitions.findLast { it.type == TransitionType.CLOSE }
+ return if (lastTransition != null) listOf(lastTransition)
+ else emptyList()
+ }
+ }
+ ),
+ assertions =
+ AssertionTemplates.COMMON_ASSERTIONS +
+ listOf(
+ AppWindowOnTopAtStart(Components.DESKTOP_MODE_APP),
+ AppLayerIsVisibleAtStart(Components.DESKTOP_MODE_APP),
+ AppLayerIsInvisibleAtEnd(Components.DESKTOP_MODE_APP),
+ LauncherWindowMovesToTop()
+ )
+ .associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ )
+
+ val CORNER_RESIZE =
+ FlickerConfigEntry(
+ scenarioId = ScenarioId("CORNER_RESIZE"),
+ extractor =
+ ShellTransitionScenarioExtractor(
+ transitionMatcher =
+ object : ITransitionMatcher {
+ override fun findAll(
+ transitions: Collection<Transition>
+ ): Collection<Transition> {
+ return transitions.filter {
+ it.type == TransitionType.CHANGE
+ }
+ }
+ }
+ ),
+ assertions =
+ listOf(
+ AppWindowIsVisibleAlways(Components.DESKTOP_MODE_APP),
+ AppWindowOnTopAtEnd(Components.DESKTOP_MODE_APP),
+ AppWindowRemainInsideDisplayBounds(Components.DESKTOP_MODE_APP),
+ ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }),
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt
new file mode 100644
index 000000000000..9dfafe958b0b
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragLandscape.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.service.desktopmode.flicker
+
+import android.tools.Rotation
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class EnterDesktopWithDragLandscape : EnterDesktopWithDrag(Rotation.ROTATION_90) {
+ @ExpectedScenarios(["END_DRAG_TO_DESKTOP"])
+ @Test
+ override fun enterDesktopWithDrag() = super.enterDesktopWithDrag()
+
+ companion object {
+
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(END_DRAG_TO_DESKTOP)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt
new file mode 100644
index 000000000000..1c7d6237eb8a
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/EnterDesktopWithDragPortrait.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.service.desktopmode.flicker
+
+import android.tools.Rotation
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.END_DRAG_TO_DESKTOP
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.EnterDesktopWithDrag
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class EnterDesktopWithDragPortrait : EnterDesktopWithDrag(Rotation.ROTATION_0) {
+ @ExpectedScenarios(["END_DRAG_TO_DESKTOP"])
+ @Test
+ override fun enterDesktopWithDrag() = super.enterDesktopWithDrag()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(END_DRAG_TO_DESKTOP)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizeLandscape.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizeLandscape.kt
new file mode 100644
index 000000000000..8d1a53021683
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizeLandscape.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.service.desktopmode.flicker
+
+import android.tools.Rotation
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.ResizeAppWithCornerResize
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class ResizeAppWithCornerResizeLandscape : ResizeAppWithCornerResize(Rotation.ROTATION_90) {
+ @ExpectedScenarios(["CORNER_RESIZE"])
+ @Test
+ override fun resizeAppWithCornerResize() = super.resizeAppWithCornerResize()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizePortrait.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizePortrait.kt
new file mode 100644
index 000000000000..2d81c8c44799
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/flicker/ResizeAppWithCornerResizePortrait.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.service.desktopmode.flicker
+
+import android.tools.Rotation
+import android.tools.flicker.FlickerConfig
+import android.tools.flicker.annotation.ExpectedScenarios
+import android.tools.flicker.annotation.FlickerConfigProvider
+import android.tools.flicker.config.FlickerConfig
+import android.tools.flicker.config.FlickerServiceConfig
+import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner
+import com.android.wm.shell.flicker.service.desktopmode.flicker.DesktopModeFlickerScenarios.Companion.CORNER_RESIZE
+import com.android.wm.shell.flicker.service.desktopmode.scenarios.ResizeAppWithCornerResize
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(FlickerServiceJUnit4ClassRunner::class)
+class ResizeAppWithCornerResizePortrait : ResizeAppWithCornerResize(Rotation.ROTATION_0) {
+ @ExpectedScenarios(["CORNER_RESIZE"])
+ @Test
+ override fun resizeAppWithCornerResize() = super.resizeAppWithCornerResize()
+
+ companion object {
+ @JvmStatic
+ @FlickerConfigProvider
+ fun flickerConfigProvider(): FlickerConfig =
+ FlickerConfig().use(FlickerServiceConfig.DEFAULT).use(CORNER_RESIZE)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt
new file mode 100644
index 000000000000..e77a45729124
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/CloseAllAppsWithAppHeaderExit.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.service.desktopmode.scenarios
+
+import android.app.Instrumentation
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.MailAppHelper
+import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import com.android.wm.shell.flicker.service.common.Utils
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class CloseAllAppsWithAppHeaderExit
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val tapl = LauncherInstrumentation()
+ private val wmHelper = WindowManagerStateHelper(instrumentation)
+ private val device = UiDevice.getInstance(instrumentation)
+ private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+ private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation))
+ private val nonResizeableApp = DesktopModeAppHelper(NonResizeableAppHelper(instrumentation))
+
+
+
+ @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+ tapl.setEnableRotation(true)
+ tapl.setExpectedRotation(rotation.value)
+ testApp.enterDesktopWithDrag(wmHelper, device)
+ mailApp.launchViaIntent(wmHelper)
+ nonResizeableApp.launchViaIntent(wmHelper)
+ }
+
+ @Test
+ open fun closeAllAppsInDesktop() {
+ nonResizeableApp.closeDesktopApp(wmHelper, device)
+ mailApp.closeDesktopApp(wmHelper, device)
+ testApp.closeDesktopApp(wmHelper, device)
+ }
+
+ @After
+ fun teardown() {
+ testApp.exit(wmHelper)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.kt
new file mode 100644
index 000000000000..fe139d2d24a0
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/EnterDesktopWithDrag.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.service.desktopmode.scenarios
+
+import android.app.Instrumentation
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import com.android.wm.shell.flicker.service.common.Utils
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+
+@Ignore("Base Test Class")
+abstract class EnterDesktopWithDrag
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val tapl = LauncherInstrumentation()
+ private val wmHelper = WindowManagerStateHelper(instrumentation)
+ private val device = UiDevice.getInstance(instrumentation)
+ private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+
+ @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+ tapl.setEnableRotation(true)
+ tapl.setExpectedRotation(rotation.value)
+ }
+
+ @Test
+ open fun enterDesktopWithDrag() {
+ testApp.enterDesktopWithDrag(wmHelper, device)
+ }
+
+ @After
+ fun teardown() {
+ testApp.exit(wmHelper)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.kt
new file mode 100644
index 000000000000..ac9089a5c1bd
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/desktopmode/scenarios/ResizeAppWithCornerResize.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.flicker.service.desktopmode.scenarios
+
+import android.app.Instrumentation
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.DesktopModeAppHelper
+import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.window.flags.Flags
+import com.android.wm.shell.flicker.service.common.Utils
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+
+@Ignore("Base Test Class")
+abstract class ResizeAppWithCornerResize
+@JvmOverloads
+constructor(val rotation: Rotation = Rotation.ROTATION_0) {
+
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val tapl = LauncherInstrumentation()
+ private val wmHelper = WindowManagerStateHelper(instrumentation)
+ private val device = UiDevice.getInstance(instrumentation)
+ private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation))
+
+ @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation)
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+ tapl.setEnableRotation(true)
+ tapl.setExpectedRotation(rotation.value)
+ testApp.enterDesktopWithDrag(wmHelper, device)
+ }
+
+ @Test
+ open fun resizeAppWithCornerResize() {
+ testApp.cornerResize(wmHelper, device, DesktopModeAppHelper.Corners.RIGHT_TOP, 50, -50)
+ }
+
+ @After
+ fun teardown() {
+ testApp.exit(wmHelper)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt
index 89ef91e12758..61710742abb4 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt
@@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios
import android.app.Instrumentation
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.AndroidLoggerSetupRule
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
@@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
-import org.junit.ClassRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -66,8 +64,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) {
primaryApp.exit(wmHelper)
secondaryApp.exit(wmHelper)
}
-
- companion object {
- @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule()
- }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt
index 433669205834..bcd0f126daef 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt
@@ -19,18 +19,17 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios
import android.app.Instrumentation
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.AndroidLoggerSetupRule
import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.MultiWindowUtils
import com.android.wm.shell.flicker.service.common.Utils
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Assume
import org.junit.Before
-import org.junit.ClassRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -53,6 +52,10 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) {
fun setup() {
Assume.assumeTrue(tapl.isTablet)
+ MultiWindowUtils.executeShellCommand(
+ instrumentation,
+ "settings put system notification_cooldown_enabled 0"
+ )
// Send a notification
sendNotificationApp.launchViaIntent(wmHelper)
sendNotificationApp.postNotification(wmHelper)
@@ -76,9 +79,10 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) {
primaryApp.exit(wmHelper)
secondaryApp.exit(wmHelper)
sendNotificationApp.exit(wmHelper)
- }
- companion object {
- @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule()
+ MultiWindowUtils.executeShellCommand(
+ instrumentation,
+ "settings reset system notification_cooldown_enabled"
+ )
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt
index 8c7e63f7471f..3f07be083041 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt
@@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios
import android.app.Instrumentation
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.AndroidLoggerSetupRule
import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
@@ -30,7 +29,6 @@ import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Assume
import org.junit.Before
-import org.junit.ClassRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -88,8 +86,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) {
secondaryApp.exit(wmHelper)
tapl.enableBlockTimeout(false)
}
-
- companion object {
- @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule()
- }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt
index 2072831d7d1b..532801357d60 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt
@@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios
import android.app.Instrumentation
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.AndroidLoggerSetupRule
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
@@ -29,7 +28,6 @@ import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Assume
import org.junit.Before
-import org.junit.ClassRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -76,8 +74,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) {
secondaryApp.exit(wmHelper)
tapl.enableBlockTimeout(false)
}
-
- companion object {
- @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule()
- }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt
index 09e77ccffba7..be4035d6af7f 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt
@@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios
import android.app.Instrumentation
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.AndroidLoggerSetupRule
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
@@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
-import org.junit.ClassRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -72,8 +70,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) {
primaryApp.exit(wmHelper)
secondaryApp.exit(wmHelper)
}
-
- companion object {
- @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule()
- }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
index babdae164835..db962e717a3b 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
@@ -20,7 +20,6 @@ import android.app.Instrumentation
import android.graphics.Point
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.AndroidLoggerSetupRule
import android.tools.helpers.WindowUtils
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
@@ -30,7 +29,6 @@ import com.android.wm.shell.flicker.service.common.Utils
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
-import org.junit.ClassRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -143,7 +141,7 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) {
private fun isLandscape(rotation: Rotation): Boolean {
val displayBounds = WindowUtils.getDisplayBounds(rotation)
- return displayBounds.width > displayBounds.height
+ return displayBounds.width() > displayBounds.height()
}
private fun isTablet(): Boolean {
@@ -151,8 +149,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) {
val LARGE_SCREEN_DP_THRESHOLD = 600
return sizeDp.x >= LARGE_SCREEN_DP_THRESHOLD && sizeDp.y >= LARGE_SCREEN_DP_THRESHOLD
}
-
- companion object {
- @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule()
- }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt
index 3e8547961ea0..de26982501a3 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt
@@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios
import android.app.Instrumentation
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.AndroidLoggerSetupRule
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
@@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
-import org.junit.ClassRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -69,8 +67,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) {
primaryApp.exit(wmHelper)
secondaryApp.exit(wmHelper)
}
-
- companion object {
- @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule()
- }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt
index 655ae4e29af3..873b0199f0e8 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt
@@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios
import android.app.Instrumentation
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.AndroidLoggerSetupRule
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
@@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
-import org.junit.ClassRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -68,8 +66,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) {
primaryApp.exit(wmHelper)
secondaryApp.exit(wmHelper)
}
-
- companion object {
- @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule()
- }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt
index 22082586bb62..15934d0f3944 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt
@@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios
import android.app.Instrumentation
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.AndroidLoggerSetupRule
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
@@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
-import org.junit.ClassRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -70,8 +68,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) {
primaryApp.exit(wmHelper)
secondaryApp.exit(wmHelper)
}
-
- companion object {
- @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule()
- }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt
index 2ac63c2afefc..79e69ae084f4 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt
@@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios
import android.app.Instrumentation
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.AndroidLoggerSetupRule
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
@@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
-import org.junit.ClassRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -71,8 +69,4 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) {
thirdApp.exit(wmHelper)
fourthApp.exit(wmHelper)
}
-
- companion object {
- @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule()
- }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt
index 35b122d7bc9e..0f932d46d3d3 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/service/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt
@@ -19,7 +19,6 @@ package com.android.wm.shell.flicker.service.splitscreen.scenarios
import android.app.Instrumentation
import android.tools.NavBar
import android.tools.Rotation
-import android.tools.AndroidLoggerSetupRule
import android.tools.traces.parsers.WindowManagerStateHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
@@ -28,7 +27,6 @@ import com.android.wm.shell.flicker.service.common.Utils
import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
-import org.junit.ClassRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -68,8 +66,4 @@ abstract class UnlockKeyguardToSplitScreen {
primaryApp.exit(wmHelper)
secondaryApp.exit(wmHelper)
}
-
- companion object {
- @ClassRule @JvmField val setupLoggerRule = AndroidLoggerSetupRule()
- }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp
index f813b0d3b0b7..0fe7a16be851 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp
@@ -65,9 +65,11 @@ android_test {
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",
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml
index 05f937ab6795..85715db3d952 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml
@@ -20,6 +20,8 @@
<option name="isolated-storage" value="false"/>
<target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <!-- disable DeprecatedTargetSdk warning -->
+ <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/>
<!-- keeps the screen on during tests -->
<option name="screen-always-on" value="on"/>
<!-- prevents the phone from restarting -->
@@ -89,6 +91,7 @@
value="trace_config.textproto"
/>
<option name="instrumentation-arg" key="per_run" value="true"/>
+ <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/>
</test>
<!-- Needed for pulling the collected trace config on to the host -->
<metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index d74c59ef0879..7f48499b0558 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -17,12 +17,12 @@
package com.android.wm.shell.flicker.splitscreen
import android.platform.test.annotations.Presubmit
-import android.tools.traces.component.ComponentNameMatcher
-import android.tools.traces.component.EdgeExtensionComponentMatcher
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 android.tools.traces.component.EdgeExtensionComponentMatcher
import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
import com.android.wm.shell.flicker.splitscreen.benchmark.CopyContentInSplitBenchmark
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/SwitchBetweenSplitPairsNoPip.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairsNoPip.kt
index 8724346427f4..a72b3d15eb9e 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairsNoPip.kt
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairsNoPip.kt
@@ -18,11 +18,11 @@ package com.android.wm.shell.flicker.splitscreen
import android.platform.test.annotations.Presubmit
import android.tools.NavBar
-import android.tools.traces.component.ComponentNameMatcher
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.server.wm.flicker.helpers.PipAppHelper
import com.android.wm.shell.flicker.splitscreen.benchmark.SplitScreenBase
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 16d73318bd3a..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
@@ -19,13 +19,13 @@ package com.android.wm.shell.flicker.splitscreen
import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import android.tools.NavBar
-import android.tools.flicker.subject.layers.LayersTraceSubject
-import android.tools.flicker.subject.region.RegionSubject
-import android.tools.traces.component.ComponentNameMatcher.Companion.WALLPAPER_BBQ_WRAPPER
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.flicker.subject.layers.LayersTraceSubject
+import android.tools.flicker.subject.region.RegionSubject
+import android.tools.traces.component.ComponentNameMatcher.Companion.WALLPAPER_BBQ_WRAPPER
import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
import com.android.wm.shell.flicker.splitscreen.benchmark.UnlockKeyguardToSplitScreenBenchmark
@@ -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/CopyContentInSplitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt
index 9c5a3fe35bfe..7e8e50843b90 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt
@@ -16,11 +16,11 @@
package com.android.wm.shell.flicker.splitscreen.benchmark
-import android.tools.traces.component.ComponentNameMatcher
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.utils.SplitScreenUtils
import org.junit.FixMethodOrder
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/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt
index 38206c396efb..6a6aa1abc9f3 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt
@@ -128,7 +128,7 @@ abstract class SwitchAppByDoubleTapDividerBenchmark(override val flicker: Legacy
private fun isLandscape(rotation: Rotation): Boolean {
val displayBounds = WindowUtils.getDisplayBounds(rotation)
- return displayBounds.width > displayBounds.height
+ return displayBounds.width() > displayBounds.height()
}
companion object {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
index a19d232c9a2f..90d2635f6a51 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
@@ -17,8 +17,8 @@
package com.android.wm.shell.flicker
import android.app.Instrumentation
-import android.tools.traces.component.ComponentNameMatcher
import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.traces.component.ComponentNameMatcher
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.utils.ICommonAssertions
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt
index 3df0954da2e9..509f4f202b6b 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt
@@ -18,13 +18,13 @@
package com.android.wm.shell.flicker.utils
+import android.graphics.Region
import android.tools.Rotation
-import android.tools.datatypes.Region
+import android.tools.flicker.legacy.LegacyFlickerTest
import android.tools.flicker.subject.layers.LayerTraceEntrySubject
import android.tools.flicker.subject.layers.LayersTraceSubject
-import android.tools.traces.component.IComponentMatcher
-import android.tools.flicker.legacy.LegacyFlickerTest
import android.tools.helpers.WindowUtils
+import android.tools.traces.component.IComponentMatcher
fun LegacyFlickerTest.appPairsDividerIsVisibleAtEnd() {
assertLayersEnd { this.isVisible(APP_PAIR_SPLIT_DIVIDER_COMPONENT) }
@@ -263,41 +263,41 @@ fun LayerTraceEntrySubject.splitAppLayerBoundsSnapToDivider(
val displayBounds = WindowUtils.getDisplayBounds(rotation)
return invoke {
val dividerRegion =
- layer(SPLIT_SCREEN_DIVIDER_COMPONENT)?.visibleRegion?.region
+ layer(SPLIT_SCREEN_DIVIDER_COMPONENT)?.visibleRegion?.region?.bounds
?: error("$SPLIT_SCREEN_DIVIDER_COMPONENT component not found")
visibleRegion(component).isNotEmpty()
visibleRegion(component)
.coversAtMost(
- if (displayBounds.width > displayBounds.height) {
+ if (displayBounds.width() > displayBounds.height()) {
if (landscapePosLeft) {
- Region.from(
+ Region(
0,
0,
- (dividerRegion.bounds.left + dividerRegion.bounds.right) / 2,
- displayBounds.bounds.bottom
+ (dividerRegion.left + dividerRegion.right) / 2,
+ displayBounds.bottom
)
} else {
- Region.from(
- (dividerRegion.bounds.left + dividerRegion.bounds.right) / 2,
+ Region(
+ (dividerRegion.left + dividerRegion.right) / 2,
0,
- displayBounds.bounds.right,
- displayBounds.bounds.bottom
+ displayBounds.right,
+ displayBounds.bottom
)
}
} else {
if (portraitPosTop) {
- Region.from(
+ Region(
0,
0,
- displayBounds.bounds.right,
- (dividerRegion.bounds.top + dividerRegion.bounds.bottom) / 2
+ displayBounds.right,
+ (dividerRegion.top + dividerRegion.bottom) / 2
)
} else {
- Region.from(
+ Region(
0,
- (dividerRegion.bounds.top + dividerRegion.bounds.bottom) / 2,
- displayBounds.bounds.right,
- displayBounds.bounds.bottom
+ (dividerRegion.top + dividerRegion.bottom) / 2,
+ displayBounds.right,
+ displayBounds.bottom
)
}
}
@@ -420,17 +420,17 @@ fun LegacyFlickerTest.dockedStackSecondaryBoundsIsVisibleAtEnd(
fun getPrimaryRegion(dividerRegion: Region, rotation: Rotation): Region {
val displayBounds = WindowUtils.getDisplayBounds(rotation)
return if (rotation.isRotated()) {
- Region.from(
+ Region(
0,
0,
dividerRegion.bounds.left + WindowUtils.dockedStackDividerInset,
- displayBounds.bounds.bottom
+ displayBounds.bottom
)
} else {
- Region.from(
+ Region(
0,
0,
- displayBounds.bounds.right,
+ displayBounds.right,
dividerRegion.bounds.top + WindowUtils.dockedStackDividerInset
)
}
@@ -439,18 +439,18 @@ fun getPrimaryRegion(dividerRegion: Region, rotation: Rotation): Region {
fun getSecondaryRegion(dividerRegion: Region, rotation: Rotation): Region {
val displayBounds = WindowUtils.getDisplayBounds(rotation)
return if (rotation.isRotated()) {
- Region.from(
+ Region(
dividerRegion.bounds.right - WindowUtils.dockedStackDividerInset,
0,
- displayBounds.bounds.right,
- displayBounds.bounds.bottom
+ displayBounds.right,
+ displayBounds.bottom
)
} else {
- Region.from(
+ Region(
0,
dividerRegion.bounds.bottom - WindowUtils.dockedStackDividerInset,
- displayBounds.bounds.right,
- displayBounds.bounds.bottom
+ displayBounds.right,
+ displayBounds.bottom
)
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt
index 50c04354528f..4465a16a8e0f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt
@@ -17,8 +17,8 @@
package com.android.wm.shell.flicker.utils
import android.platform.test.annotations.Presubmit
-import android.tools.traces.component.ComponentNameMatcher
import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.traces.component.ComponentNameMatcher
import com.android.server.wm.flicker.entireScreenCovered
import com.android.server.wm.flicker.navBarLayerIsVisibleAtStartAndEnd
import com.android.server.wm.flicker.navBarLayerPositionAtStartAndEnd
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt
index 4e9a9d65dbf9..c4954f90179c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt
@@ -20,11 +20,11 @@ import android.app.Instrumentation
import android.graphics.Point
import android.os.SystemClock
import android.tools.Rotation
+import android.tools.device.apphelpers.StandardAppHelper
+import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.traces.component.ComponentNameMatcher
import android.tools.traces.component.IComponentMatcher
import android.tools.traces.component.IComponentNameMatcher
-import android.tools.device.apphelpers.StandardAppHelper
-import android.tools.flicker.rules.ChangeDisplayOrientationRule
import android.tools.traces.parsers.WindowManagerStateHelper
import android.tools.traces.parsers.toFlickerComponent
import android.view.InputDevice
@@ -179,15 +179,10 @@ object SplitScreenUtils {
val displayBounds =
wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
?: error("Display not found")
+ val swipeXCoordinate = displayBounds.centerX() / 2
// Pull down the notifications
- device.swipe(
- displayBounds.centerX(),
- 5,
- displayBounds.centerX(),
- displayBounds.bottom,
- 50 /* steps */
- )
+ device.swipe(swipeXCoordinate, 5, swipeXCoordinate, displayBounds.bottom, 50 /* steps */)
SystemClock.sleep(TIMEOUT_MS)
// Find the target notification
@@ -210,7 +205,7 @@ object SplitScreenUtils {
// Drag to split
val dragStart = notificationContent.visibleCenter
val dragMiddle = Point(dragStart.x + 50, dragStart.y)
- val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
+ val dragEnd = Point(displayBounds.width() / 4, displayBounds.width() / 4)
val downTime = SystemClock.uptimeMillis()
touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, dragStart)
@@ -317,7 +312,7 @@ object SplitScreenUtils {
wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
?: error("Display not found")
val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
- dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3), 200)
+ dividerBar.drag(Point(displayBounds.width() * 1 / 3, displayBounds.height() * 2 / 3), 200)
wmHelper
.StateSyncBuilder()
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index 32c070305e05..13f95ccea640 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -39,7 +39,7 @@ android_test {
static_libs: [
"WindowManager-Shell",
"junit",
- "flag-junit-base",
+ "flag-junit",
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.ext.junit",
@@ -55,6 +55,9 @@ android_test {
"platform-test-annotations",
"servicestests-utils",
"com_android_wm_shell_flags_lib",
+ "guava-android-testlib",
+ "com.android.window.flags.window-aconfig-java",
+ "platform-test-annotations",
],
libs: [
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index 9c1a88e1caa0..f9b4108bc8c2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -16,10 +16,11 @@
package com.android.wm.shell;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
@@ -62,6 +63,7 @@ import androidx.test.filters.SmallTest;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.compatui.CompatUIController;
+import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellInit;
@@ -78,7 +80,7 @@ import java.util.Optional;
* Tests for the shell task organizer.
*
* Build/Install/Run:
- * atest WMShellUnitTests:ShellTaskOrganizerTests
+ * atest WMShellUnitTests:ShellTaskOrganizerTests
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -92,6 +94,8 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
private ShellExecutor mTestExecutor;
@Mock
private ShellCommandHandler mShellCommandHandler;
+ @Mock
+ private RecentTasksController mRecentTasksController;
private ShellTaskOrganizer mOrganizer;
private ShellInit mShellInit;
@@ -120,6 +124,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
private class TrackingLocusIdListener implements ShellTaskOrganizer.LocusIdListener {
final SparseArray<LocusId> visibleLocusTasks = new SparseArray<>();
final SparseArray<LocusId> invisibleLocusTasks = new SparseArray<>();
+
@Override
public void onVisibilityChanged(int taskId, LocusId locus, boolean visible) {
if (visible) {
@@ -130,18 +135,18 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
}
}
-
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
try {
doReturn(ParceledListSlice.<TaskAppearedInfo>emptyList())
.when(mTaskOrganizerController).registerTaskOrganizer(any());
- } catch (RemoteException e) {}
+ } catch (RemoteException e) {
+ }
mShellInit = spy(new ShellInit(mTestExecutor));
mOrganizer = spy(new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
- mTaskOrganizerController, mCompatUI, Optional.empty(), Optional.empty(),
- mTestExecutor));
+ mTaskOrganizerController, mCompatUI, Optional.empty(),
+ Optional.of(mRecentTasksController), mTestExecutor));
mShellInit.init();
}
@@ -163,7 +168,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testTaskLeashReleasedAfterVanished() throws RemoteException {
assumeFalse(ENABLE_SHELL_TRANSITIONS);
- RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
SurfaceControl taskLeash = new SurfaceControl.Builder(new SurfaceSession())
.setName("task").build();
mOrganizer.registerOrganizer();
@@ -188,8 +193,8 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testRegisterWithExistingTasks() throws RemoteException {
// Setup some tasks
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
- RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo task2 = createTaskInfo(/* taskId= */ 2, WINDOWING_MODE_MULTI_WINDOW);
ArrayList<TaskAppearedInfo> taskInfos = new ArrayList<>();
taskInfos.add(new TaskAppearedInfo(task1, new SurfaceControl()));
taskInfos.add(new TaskAppearedInfo(task2, new SurfaceControl()));
@@ -208,10 +213,10 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testAppearedVanished() {
- RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
TrackingTaskListener listener = new TrackingTaskListener();
mOrganizer.addListenerForType(listener, TASK_LISTENER_TYPE_MULTI_WINDOW);
- mOrganizer.onTaskAppeared(taskInfo, null);
+ mOrganizer.onTaskAppeared(taskInfo, /* leash= */ null);
assertTrue(listener.appeared.contains(taskInfo));
mOrganizer.onTaskVanished(taskInfo);
@@ -220,7 +225,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testAddListenerExistingTasks() {
- RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
mOrganizer.onTaskAppeared(taskInfo, null);
TrackingTaskListener listener = new TrackingTaskListener();
@@ -230,9 +235,9 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testAddListenerForMultipleTypes() {
- RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN);
+ RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN);
mOrganizer.onTaskAppeared(taskInfo1, null);
- RunningTaskInfo taskInfo2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo taskInfo2 = createTaskInfo(/* taskId= */ 2, WINDOWING_MODE_MULTI_WINDOW);
mOrganizer.onTaskAppeared(taskInfo2, null);
TrackingTaskListener listener = new TrackingTaskListener();
@@ -247,10 +252,10 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testRemoveListenerForMultipleTypes() {
- RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN);
- mOrganizer.onTaskAppeared(taskInfo1, null);
- RunningTaskInfo taskInfo2 = createTaskInfo(2, WINDOWING_MODE_MULTI_WINDOW);
- mOrganizer.onTaskAppeared(taskInfo2, null);
+ RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN);
+ mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null);
+ RunningTaskInfo taskInfo2 = createTaskInfo(/* taskId= */ 2, WINDOWING_MODE_MULTI_WINDOW);
+ mOrganizer.onTaskAppeared(taskInfo2, /* leash= */ null);
TrackingTaskListener listener = new TrackingTaskListener();
mOrganizer.addListenerForType(listener,
@@ -267,12 +272,12 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testWindowingModeChange() {
- RunningTaskInfo taskInfo = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo taskInfo = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
TrackingTaskListener mwListener = new TrackingTaskListener();
TrackingTaskListener pipListener = new TrackingTaskListener();
mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW);
mOrganizer.addListenerForType(pipListener, TASK_LISTENER_TYPE_PIP);
- mOrganizer.onTaskAppeared(taskInfo, null);
+ mOrganizer.onTaskAppeared(taskInfo, /* leash= */ null);
assertTrue(mwListener.appeared.contains(taskInfo));
assertTrue(pipListener.appeared.isEmpty());
@@ -284,11 +289,11 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testAddListenerForTaskId_afterTypeListener() {
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
TrackingTaskListener mwListener = new TrackingTaskListener();
TrackingTaskListener task1Listener = new TrackingTaskListener();
mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW);
- mOrganizer.onTaskAppeared(task1, null);
+ mOrganizer.onTaskAppeared(task1, /* leash= */ null);
assertTrue(mwListener.appeared.contains(task1));
// Add task 1 specific listener
@@ -299,11 +304,11 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testAddListenerForTaskId_beforeTypeListener() {
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
TrackingTaskListener mwListener = new TrackingTaskListener();
TrackingTaskListener task1Listener = new TrackingTaskListener();
- mOrganizer.onTaskAppeared(task1, null);
- mOrganizer.addListenerForTaskId(task1Listener, 1);
+ mOrganizer.onTaskAppeared(task1, /* leash= */ null);
+ mOrganizer.addListenerForTaskId(task1Listener, /* taskId= */ 1);
assertTrue(task1Listener.appeared.contains(task1));
mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW);
@@ -312,7 +317,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testGetTaskListener() {
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
TrackingTaskListener mwListener = new TrackingTaskListener();
mOrganizer.addListenerForType(mwListener, TASK_LISTENER_TYPE_MULTI_WINDOW);
@@ -324,7 +329,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
// Priority goes to the cookie listener so we would expect the task appear to show up there
// instead of the multi-window type listener.
- mOrganizer.onTaskAppeared(task1, null);
+ mOrganizer.onTaskAppeared(task1, /* leash= */ null);
assertTrue(cookieListener.appeared.contains(task1));
assertFalse(mwListener.appeared.contains(task1));
@@ -332,7 +337,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
boolean gotException = false;
try {
- mOrganizer.addListenerForTaskId(task1Listener, 1);
+ mOrganizer.addListenerForTaskId(task1Listener, /* taskId= */ 1);
} catch (Exception e) {
gotException = true;
}
@@ -343,26 +348,27 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testGetParentTaskListener() {
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
TrackingTaskListener mwListener = new TrackingTaskListener();
- mOrganizer.onTaskAppeared(task1, null);
+ mOrganizer.onTaskAppeared(task1, /* leash= */ null);
mOrganizer.addListenerForTaskId(mwListener, task1.taskId);
RunningTaskInfo task2 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
task2.parentTaskId = task1.taskId;
- mOrganizer.onTaskAppeared(task2, null);
+ mOrganizer.onTaskAppeared(task2, /* leash= */ null);
assertTrue(mwListener.appeared.contains(task2));
}
@Test
public void testOnSizeCompatActivityChanged() {
- final RunningTaskInfo taskInfo1 = createTaskInfo(12, WINDOWING_MODE_FULLSCREEN);
+ final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 12,
+ WINDOWING_MODE_FULLSCREEN);
taskInfo1.displayId = DEFAULT_DISPLAY;
taskInfo1.appCompatTaskInfo.topActivityInSizeCompat = false;
final TrackingTaskListener taskListener = new TrackingTaskListener();
mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN);
- mOrganizer.onTaskAppeared(taskInfo1, null);
+ mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null);
// sizeCompatActivity is null if top activity is not in size compat.
verify(mCompatUI).onCompatInfoChanged(taskInfo1, null /* taskListener */);
@@ -394,12 +400,13 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testOnEligibleForLetterboxEducationActivityChanged() {
- final RunningTaskInfo taskInfo1 = createTaskInfo(12, WINDOWING_MODE_FULLSCREEN);
+ final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 12,
+ WINDOWING_MODE_FULLSCREEN);
taskInfo1.displayId = DEFAULT_DISPLAY;
taskInfo1.appCompatTaskInfo.topActivityEligibleForLetterboxEducation = false;
final TrackingTaskListener taskListener = new TrackingTaskListener();
mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN);
- mOrganizer.onTaskAppeared(taskInfo1, null);
+ mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null);
// Task listener sent to compat UI is null if top activity isn't eligible for letterbox
// education.
@@ -433,12 +440,14 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testOnCameraCompatActivityChanged() {
- final RunningTaskInfo taskInfo1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN);
+ final RunningTaskInfo taskInfo1 = createTaskInfo(/* taskId= */ 1,
+ WINDOWING_MODE_FULLSCREEN);
taskInfo1.displayId = DEFAULT_DISPLAY;
- taskInfo1.appCompatTaskInfo.cameraCompatControlState = CAMERA_COMPAT_CONTROL_HIDDEN;
+ taskInfo1.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
+ CAMERA_COMPAT_CONTROL_HIDDEN;
final TrackingTaskListener taskListener = new TrackingTaskListener();
mOrganizer.addListenerForType(taskListener, TASK_LISTENER_TYPE_FULLSCREEN);
- mOrganizer.onTaskAppeared(taskInfo1, null);
+ mOrganizer.onTaskAppeared(taskInfo1, /* leash= */ null);
// Task listener sent to compat UI is null if top activity doesn't request a camera
// compat control.
@@ -449,7 +458,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
final RunningTaskInfo taskInfo2 =
createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode());
taskInfo2.displayId = taskInfo1.displayId;
- taskInfo2.appCompatTaskInfo.cameraCompatControlState =
+ taskInfo2.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
taskInfo2.isVisible = true;
mOrganizer.onTaskInfoChanged(taskInfo2);
@@ -461,7 +470,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
final RunningTaskInfo taskInfo3 =
createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode());
taskInfo3.displayId = taskInfo1.displayId;
- taskInfo3.appCompatTaskInfo.cameraCompatControlState =
+ taskInfo3.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
taskInfo3.isVisible = true;
mOrganizer.onTaskInfoChanged(taskInfo3);
@@ -474,7 +483,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode());
taskInfo4.displayId = taskInfo1.displayId;
taskInfo4.appCompatTaskInfo.topActivityInSizeCompat = true;
- taskInfo4.appCompatTaskInfo.cameraCompatControlState =
+ taskInfo4.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
taskInfo4.isVisible = true;
mOrganizer.onTaskInfoChanged(taskInfo4);
@@ -485,7 +494,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
final RunningTaskInfo taskInfo5 =
createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode());
taskInfo5.displayId = taskInfo1.displayId;
- taskInfo5.appCompatTaskInfo.cameraCompatControlState =
+ taskInfo5.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
CAMERA_COMPAT_CONTROL_DISMISSED;
taskInfo5.isVisible = true;
mOrganizer.onTaskInfoChanged(taskInfo5);
@@ -496,7 +505,7 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
final RunningTaskInfo taskInfo6 =
createTaskInfo(taskInfo1.taskId, taskInfo1.getWindowingMode());
taskInfo6.displayId = taskInfo1.displayId;
- taskInfo6.appCompatTaskInfo.cameraCompatControlState =
+ taskInfo6.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
taskInfo6.isVisible = false;
mOrganizer.onTaskInfoChanged(taskInfo6);
@@ -509,20 +518,20 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testAddLocusListener() {
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
task1.isVisible = true;
task1.mTopActivityLocusId = new LocusId("10");
- RunningTaskInfo task2 = createTaskInfo(2, WINDOWING_MODE_FULLSCREEN);
+ RunningTaskInfo task2 = createTaskInfo(/* taskId= */ 2, WINDOWING_MODE_FULLSCREEN);
task2.isVisible = true;
task2.mTopActivityLocusId = new LocusId("20");
- RunningTaskInfo task3 = createTaskInfo(3, WINDOWING_MODE_FULLSCREEN);
+ RunningTaskInfo task3 = createTaskInfo(/* taskId= */ 3, WINDOWING_MODE_FULLSCREEN);
task3.isVisible = true;
- mOrganizer.onTaskAppeared(task1, null);
- mOrganizer.onTaskAppeared(task2, null);
- mOrganizer.onTaskAppeared(task3, null);
+ mOrganizer.onTaskAppeared(task1, /* leash= */ null);
+ mOrganizer.onTaskAppeared(task2, /* leash= */ null);
+ mOrganizer.onTaskAppeared(task3, /* leash= */ null);
TrackingLocusIdListener listener = new TrackingLocusIdListener();
mOrganizer.addLocusIdListener(listener);
@@ -538,11 +547,11 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
TrackingLocusIdListener listener = new TrackingLocusIdListener();
mOrganizer.addLocusIdListener(listener);
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN);
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN);
task1.mTopActivityLocusId = new LocusId("10");
task1.isVisible = true;
- mOrganizer.onTaskAppeared(task1, null);
+ mOrganizer.onTaskAppeared(task1, /* leash= */ null);
assertTrue(listener.visibleLocusTasks.contains(task1.taskId));
assertEquals(listener.visibleLocusTasks.get(task1.taskId), task1.mTopActivityLocusId);
@@ -557,9 +566,9 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
TrackingLocusIdListener listener = new TrackingLocusIdListener();
mOrganizer.addLocusIdListener(listener);
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
task1.isVisible = true;
- mOrganizer.onTaskAppeared(task1, null);
+ mOrganizer.onTaskAppeared(task1, /* leash= */ null);
assertEquals(listener.visibleLocusTasks.size(), 0);
task1.mTopActivityLocusId = new LocusId("10");
@@ -584,9 +593,9 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
TrackingLocusIdListener listener = new TrackingLocusIdListener();
mOrganizer.addLocusIdListener(listener);
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN);
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN);
task1.isVisible = true;
- mOrganizer.onTaskAppeared(task1, null);
+ mOrganizer.onTaskAppeared(task1, /* leash= */ null);
task1.mTopActivityLocusId = new LocusId("10");
mOrganizer.onTaskInfoChanged(task1);
@@ -608,9 +617,9 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
TrackingLocusIdListener listener = new TrackingLocusIdListener();
mOrganizer.addLocusIdListener(listener);
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
task1.isVisible = true;
- mOrganizer.onTaskAppeared(task1, null);
+ mOrganizer.onTaskAppeared(task1, /* leash= */ null);
assertEquals(listener.visibleLocusTasks.size(), 0);
assertEquals(listener.invisibleLocusTasks.size(), 0);
@@ -626,20 +635,63 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
@Test
public void testOnSizeCompatRestartButtonClicked() throws RemoteException {
- RunningTaskInfo task1 = createTaskInfo(1, WINDOWING_MODE_MULTI_WINDOW);
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_MULTI_WINDOW);
task1.token = mock(WindowContainerToken.class);
- mOrganizer.onTaskAppeared(task1, null);
+ mOrganizer.onTaskAppeared(task1, /* leash= */ null);
mOrganizer.onSizeCompatRestartButtonClicked(task1.taskId);
verify(mTaskOrganizerController).restartTaskTopActivityProcessIfVisible(task1.token);
}
+ @Test
+ public void testRecentTasks_onTaskAppeared_shouldNotifyTaskController() {
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM);
+
+ mOrganizer.onTaskAppeared(task1, null);
+
+ verify(mRecentTasksController).onTaskAdded(task1);
+ }
+
+ @Test
+ public void testRecentTasks_onTaskVanished_shouldNotifyTaskController() {
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM);
+ mOrganizer.onTaskAppeared(task1, /* leash= */ null);
+
+ mOrganizer.onTaskVanished(task1);
+
+ verify(mRecentTasksController).onTaskRemoved(task1);
+ }
+
+ @Test
+ public void testRecentTasks_visibilityChanges_shouldNotifyTaskController() {
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM);
+ mOrganizer.onTaskAppeared(task1, /* leash= */ null);
+ RunningTaskInfo task2 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM);
+ task2.isVisible = false;
+
+ mOrganizer.onTaskInfoChanged(task2);
+
+ verify(mRecentTasksController).onTaskRunningInfoChanged(task2);
+ }
+
+ @Test
+ public void testRecentTasks_windowingModeChanges_shouldNotifyTaskController() {
+ RunningTaskInfo task1 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FULLSCREEN);
+ mOrganizer.onTaskAppeared(task1, /* leash= */ null);
+ RunningTaskInfo task2 = createTaskInfo(/* taskId= */ 1, WINDOWING_MODE_FREEFORM);
+
+ mOrganizer.onTaskInfoChanged(task2);
+
+ verify(mRecentTasksController).onTaskRunningInfoChanged(task2);
+ }
+
private static RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
RunningTaskInfo taskInfo = new RunningTaskInfo();
taskInfo.taskId = taskId;
taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode);
+ taskInfo.isVisible = true;
return taskInfo;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
index 3672ae386dc4..24f4d92af9d7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java
@@ -23,8 +23,10 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.WindowConfiguration;
+import android.content.Intent;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
@@ -38,6 +40,7 @@ public final class TestRunningTaskInfoBuilder {
private WindowContainerToken mToken = createMockWCToken();
private int mParentTaskId = INVALID_TASK_ID;
+ private Intent mBaseIntent = new Intent();
private @WindowConfiguration.ActivityType int mActivityType = ACTIVITY_TYPE_STANDARD;
private @WindowConfiguration.WindowingMode int mWindowingMode = WINDOWING_MODE_UNDEFINED;
private int mDisplayId = Display.DEFAULT_DISPLAY;
@@ -68,6 +71,15 @@ public final class TestRunningTaskInfoBuilder {
return this;
}
+ /**
+ * Set {@link ActivityManager.RunningTaskInfo#baseIntent} for the task info, by default
+ * an empty intent is assigned
+ */
+ public TestRunningTaskInfoBuilder setBaseIntent(@NonNull Intent intent) {
+ mBaseIntent = intent;
+ return this;
+ }
+
public TestRunningTaskInfoBuilder setActivityType(
@WindowConfiguration.ActivityType int activityType) {
mActivityType = activityType;
@@ -109,6 +121,7 @@ public final class TestRunningTaskInfoBuilder {
public ActivityManager.RunningTaskInfo build() {
final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
info.taskId = sNextTaskId++;
+ info.baseIntent = mBaseIntent;
info.parentTaskId = mParentTaskId;
info.displayId = mDisplayId;
info.configuration.windowConfiguration.setBounds(mBounds);
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 2ac72affbb0c..bd20c1143262 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,6 +20,8 @@ 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.transition.Transitions.TRANSIT_TASK_FRAGMENT_DRAG_RESIZE;
+
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@@ -30,14 +32,19 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.animation.Animator;
+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;
@@ -54,12 +61,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)
@@ -85,6 +96,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)
@@ -99,8 +111,24 @@ 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)
+ .addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY))
+ .build();
+ final Animator animator = mAnimRunner.createAnimator(
+ info, mStartTransaction, mFinishTransaction,
+ () -> mFinishCallback.onTransitionFinished(null /* wct */),
+ new ArrayList());
+
+ // The animation should be empty when it is a jump cut for drag resize.
+ 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))
.build();
@@ -115,4 +143,22 @@ 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))
+ .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());
+ }
}
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 9ded6ea1d187..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
@@ -16,7 +16,7 @@
package com.android.wm.shell.back;
-import static android.window.BackNavigationInfo.KEY_TRIGGER_BACK;
+import static android.window.BackNavigationInfo.KEY_NAVIGATION_FINISHED;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -61,6 +61,7 @@ import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;
import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -68,7 +69,6 @@ import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.sysui.ShellSharedConstants;
-
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -113,12 +113,17 @@ public class BackAnimationControllerTest extends ShellTestCase {
private InputManager mInputManager;
@Mock
private ShellCommandHandler mShellCommandHandler;
+ @Mock
+ private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
private BackAnimationController mController;
private TestableContentResolver mContentResolver;
private TestableLooper mTestableLooper;
+ private DefaultCrossActivityBackAnimation mDefaultCrossActivityBackAnimation;
+ private CrossTaskBackAnimation mCrossTaskBackAnimation;
private ShellBackAnimationRegistry mShellBackAnimationRegistry;
+ private Rect mTouchableRegion;
@Before
public void setUp() throws Exception {
@@ -131,12 +136,14 @@ public class BackAnimationControllerTest extends ShellTestCase {
ANIMATION_ENABLED);
mTestableLooper = TestableLooper.get(this);
mShellInit = spy(new ShellInit(mShellExecutor));
+ mDefaultCrossActivityBackAnimation = new DefaultCrossActivityBackAnimation(mContext,
+ mAnimationBackground, mRootTaskDisplayAreaOrganizer);
+ mCrossTaskBackAnimation = new CrossTaskBackAnimation(mContext, mAnimationBackground);
mShellBackAnimationRegistry =
- new ShellBackAnimationRegistry(
- new CrossActivityBackAnimation(mContext, mAnimationBackground),
- new CrossTaskBackAnimation(mContext, mAnimationBackground),
- /* dialogCloseAnimation= */ null,
- new CustomizeActivityAnimation(mContext, mAnimationBackground),
+ new ShellBackAnimationRegistry(mDefaultCrossActivityBackAnimation,
+ mCrossTaskBackAnimation, /* dialogCloseAnimation= */ null,
+ new CustomCrossActivityBackAnimation(mContext, mAnimationBackground,
+ mRootTaskDisplayAreaOrganizer),
/* defaultBackToHomeAnimation= */ null);
mController =
new BackAnimationController(
@@ -152,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,
@@ -163,7 +172,8 @@ public class BackAnimationControllerTest extends ShellTestCase {
.setOnBackNavigationDone(new RemoteCallback((bundle) -> {}))
.setOnBackInvokedCallback(mAppCallback)
.setPrepareRemoteAnimation(enableAnimation)
- .setAnimationCallback(isAnimationCallback);
+ .setAnimationCallback(isAnimationCallback)
+ .setTouchableRegion(mTouchableRegion);
createNavigationInfo(builder);
}
@@ -178,7 +188,9 @@ public class BackAnimationControllerTest extends ShellTestCase {
}
RemoteAnimationTarget createAnimationTarget() {
- SurfaceControl topWindowLeash = new SurfaceControl();
+ SurfaceControl topWindowLeash = new SurfaceControl.Builder()
+ .setName("FakeLeash")
+ .build();
return new RemoteAnimationTarget(-1, RemoteAnimationTarget.MODE_CLOSING, topWindowLeash,
false, new Rect(), new Rect(), -1,
new Point(0, 0), new Rect(), new Rect(), new WindowConfiguration(),
@@ -226,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();
@@ -347,6 +360,7 @@ public class BackAnimationControllerTest extends ShellTestCase {
// Verify that we prevent any interaction with the animator callback in case a new gesture
// starts while the current back animation has not ended, instead the gesture is queued
triggerBackGesture();
+ verify(mAnimatorCallback).setTriggerBack(eq(true));
verifyNoMoreInteractions(mAnimatorCallback);
// Finish previous back navigation.
@@ -387,6 +401,7 @@ public class BackAnimationControllerTest extends ShellTestCase {
// starts while the current back animation has not ended, instead the gesture is queued
triggerBackGesture();
releaseBackGesture();
+ verify(mAnimatorCallback).setTriggerBack(eq(true));
verifyNoMoreInteractions(mAnimatorCallback);
// Finish previous back navigation.
@@ -405,6 +420,32 @@ public class BackAnimationControllerTest extends ShellTestCase {
}
@Test
+ public void gestureNotQueued_WhenPreviousGestureIsPostCommitCancelling()
+ throws RemoteException {
+ registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
+ createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME,
+ /* enableAnimation = */ true,
+ /* isAnimationCallback = */ false);
+
+ doStartEvents(0, 100);
+ simulateRemoteAnimationStart();
+ releaseBackGesture();
+
+ // Check that back cancellation is dispatched.
+ verify(mAnimatorCallback).onBackCancelled();
+ verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
+
+ reset(mAnimatorCallback);
+ reset(mBackAnimationRunner);
+
+ // Verify that a new start event is dispatched if a new gesture is started during the
+ // post-commit cancel phase
+ triggerBackGesture();
+ verify(mAnimatorCallback).onBackStarted(any());
+ verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any());
+ }
+
+ @Test
public void acceptsGesture_transitionTimeout() throws RemoteException {
registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME,
@@ -476,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();
@@ -499,7 +541,7 @@ public class BackAnimationControllerTest extends ShellTestCase {
}
@Test
- public void callbackShouldDeliverProgress() throws RemoteException {
+ public void appCallback_receivesStartAndInvoke() throws RemoteException {
registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME);
final int type = BackNavigationInfo.TYPE_CALLBACK;
@@ -507,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();
@@ -518,8 +562,9 @@ public class BackAnimationControllerTest extends ShellTestCase {
assertTrue("TriggerBack should have been true", result.mTriggerBack);
verify(mAppCallback, times(1)).onBackStarted(any());
- verify(mAppCallback, times(1)).onBackProgressed(any());
verify(mAppCallback, times(1)).onBackInvoked();
+ // Progress events should be generated from the app process.
+ verify(mAppCallback, never()).onBackProgressed(any());
verify(mAnimatorCallback, never()).onBackStarted(any());
verify(mAnimatorCallback, never()).onBackProgressed(any());
@@ -527,17 +572,33 @@ public class BackAnimationControllerTest extends ShellTestCase {
}
@Test
+ public void skipsCancelWithoutStart() throws RemoteException {
+ final int type = BackNavigationInfo.TYPE_CALLBACK;
+ final ResultListener result = new ResultListener();
+ createNavigationInfo(new BackNavigationInfo.Builder()
+ .setType(type)
+ .setOnBackInvokedCallback(mAppCallback)
+ .setOnBackNavigationDone(new RemoteCallback(result))
+ .setTouchableRegion(mTouchableRegion));
+ doMotionEvent(MotionEvent.ACTION_CANCEL, 0);
+ mShellExecutor.flushAll();
+
+ verify(mAppCallback, never()).onBackStarted(any());
+ verify(mAppCallback, never()).onBackProgressed(any());
+ verify(mAppCallback, never()).onBackInvoked();
+ verify(mAppCallback, never()).onBackCancelled();
+ }
+
+ @Test
public void testBackToActivity() throws RemoteException {
- final CrossActivityBackAnimation animation = new CrossActivityBackAnimation(mContext,
- mAnimationBackground);
- verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_ACTIVITY, animation.getRunner());
+ verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_ACTIVITY,
+ mDefaultCrossActivityBackAnimation.getRunner());
}
@Test
public void testBackToTask() throws RemoteException {
- final CrossTaskBackAnimation animation = new CrossTaskBackAnimation(mContext,
- mAnimationBackground);
- verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_TASK, animation.getRunner());
+ verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_TASK,
+ mCrossTaskBackAnimation.getRunner());
}
private void verifySystemBackBehavior(int type, BackAnimationRunner animation)
@@ -548,6 +609,7 @@ public class BackAnimationControllerTest extends ShellTestCase {
// Set up the monitoring objects.
doNothing().when(runner).onAnimationStart(anyInt(), any(), any(), any(), any());
+ doReturn(false).when(animationRunner).shouldMonitorCUJ(any());
doReturn(runner).when(animationRunner).getRunner();
doReturn(callback).when(animationRunner).getCallback();
@@ -590,7 +652,7 @@ public class BackAnimationControllerTest extends ShellTestCase {
*/
private void doStartEvents(int startX, int moveX) {
doMotionEvent(MotionEvent.ACTION_DOWN, startX);
- mController.onPilferPointers();
+ mController.onThresholdCrossed();
doMotionEvent(MotionEvent.ACTION_MOVE, moveX);
}
@@ -629,7 +691,7 @@ public class BackAnimationControllerTest extends ShellTestCase {
@Override
public void onResult(@Nullable Bundle result) {
mBackNavigationDone = true;
- mTriggerBack = result.getBoolean(KEY_TRIGGER_BACK);
+ mTriggerBack = result.getBoolean(KEY_NAVIGATION_FINISHED);
}
}
}
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 7e26577e96d4..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);
@@ -134,6 +152,31 @@ public class BackProgressAnimatorTest {
assertEquals(0, cancelCallbackCalled.getCount());
}
+ @Test
+ public void testCancelFinishCallbackNotInvokedWhenRemoved() 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);
+
+ // call onBackCancelled (which animates progress to 0 before invoking the finishCallback)
+ CountDownLatch finishCallbackCalled = new CountDownLatch(1);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> mProgressAnimator.onBackCancelled(finishCallbackCalled::countDown));
+
+ // remove onBackCancelled finishCallback (while progress is still animating to 0)
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> mProgressAnimator.removeOnBackCancelledFinishCallback());
+
+ // call reset (which triggers the finishCallback invocation, if one is present)
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> mProgressAnimator.reset());
+
+ // verify that finishCallback is not invoked
+ assertEquals(1, finishCallbackCalled.getCount());
+ }
+
private void onGestureProgress(BackEvent backEvent) {
if (mTargetProgress == backEvent.getProgress()) {
mReceivedBackEvent = backEvent;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt
new file mode 100644
index 000000000000..8bf011192347
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt
@@ -0,0 +1,264 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.f
+ */
+package com.android.wm.shell.back
+
+import android.app.ActivityManager
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.AppCompatTaskInfo
+import android.app.WindowConfiguration
+import android.graphics.Color
+import android.graphics.Point
+import android.graphics.Rect
+import android.os.RemoteException
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.Choreographer
+import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+import android.view.animation.Animation
+import android.window.BackEvent
+import android.window.BackMotionEvent
+import android.window.BackNavigationInfo
+import androidx.test.filters.SmallTest
+import com.android.internal.policy.TransitionAnimation
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.ShellTestCase
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import junit.framework.TestCase.assertEquals
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+class CustomCrossActivityBackAnimationTest : ShellTestCase() {
+ @Mock private lateinit var backAnimationBackground: BackAnimationBackground
+ @Mock private lateinit var mockCloseAnimation: Animation
+ @Mock private lateinit var mockOpenAnimation: Animation
+ @Mock private lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+ @Mock private lateinit var transitionAnimation: TransitionAnimation
+ @Mock private lateinit var appCompatTaskInfo: AppCompatTaskInfo
+ @Mock private lateinit var transaction: Transaction
+
+ private lateinit var customCrossActivityBackAnimation: CustomCrossActivityBackAnimation
+ private lateinit var customAnimationLoader: CustomAnimationLoader
+
+ @Before
+ @Throws(Exception::class)
+ fun setUp() {
+ customAnimationLoader = CustomAnimationLoader(transitionAnimation)
+ customCrossActivityBackAnimation =
+ CustomCrossActivityBackAnimation(
+ context,
+ backAnimationBackground,
+ rootTaskDisplayAreaOrganizer,
+ transaction,
+ mock(Choreographer::class.java),
+ customAnimationLoader
+ )
+
+ whenever(transitionAnimation.loadAppTransitionAnimation(eq(PACKAGE_NAME), eq(OPEN_RES_ID)))
+ .thenReturn(mockOpenAnimation)
+ whenever(transitionAnimation.loadAppTransitionAnimation(eq(PACKAGE_NAME), eq(CLOSE_RES_ID)))
+ .thenReturn(mockCloseAnimation)
+ whenever(transaction.setColor(any(), any())).thenReturn(transaction)
+ whenever(transaction.setAlpha(any(), anyFloat())).thenReturn(transaction)
+ whenever(transaction.setCrop(any(), any())).thenReturn(transaction)
+ whenever(transaction.setRelativeLayer(any(), any(), anyInt())).thenReturn(transaction)
+ spy(customCrossActivityBackAnimation)
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ fun receiveFinishAfterInvoke() {
+ val finishCalled = startCustomAnimation()
+ try {
+ customCrossActivityBackAnimation.getRunner().callback.onBackInvoked()
+ } catch (r: RemoteException) {
+ Assert.fail("onBackInvoked throw remote exception")
+ }
+ finishCalled.await(1, TimeUnit.SECONDS)
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ fun receiveFinishAfterCancel() {
+ val finishCalled = startCustomAnimation()
+ try {
+ customCrossActivityBackAnimation.getRunner().callback.onBackCancelled()
+ } catch (r: RemoteException) {
+ Assert.fail("onBackCancelled throw remote exception")
+ }
+ finishCalled.await(1, TimeUnit.SECONDS)
+ }
+
+ @Test
+ @Throws(InterruptedException::class)
+ fun receiveFinishWithoutAnimationAfterInvoke() {
+ val finishCalled = startCustomAnimation(targets = arrayOf())
+ try {
+ customCrossActivityBackAnimation.getRunner().callback.onBackInvoked()
+ } catch (r: RemoteException) {
+ Assert.fail("onBackInvoked throw remote exception")
+ }
+ finishCalled.await(1, TimeUnit.SECONDS)
+ }
+
+ @Test
+ fun testLoadCustomAnimation() {
+ testLoadCustomAnimation(OPEN_RES_ID, CLOSE_RES_ID, 0)
+ }
+
+ @Test
+ fun testLoadCustomAnimationNoEnter() {
+ testLoadCustomAnimation(0, CLOSE_RES_ID, 0)
+ }
+
+ @Test
+ fun testLoadWindowAnimations() {
+ testLoadCustomAnimation(0, 0, 30)
+ }
+
+ @Test
+ fun testCustomAnimationHigherThanWindowAnimations() {
+ testLoadCustomAnimation(OPEN_RES_ID, CLOSE_RES_ID, 30)
+ }
+
+ private fun testLoadCustomAnimation(enterResId: Int, exitResId: Int, windowAnimations: Int) {
+ val builder =
+ BackNavigationInfo.Builder()
+ .setCustomAnimation(PACKAGE_NAME, enterResId, exitResId, Color.GREEN)
+ .setWindowAnimations(PACKAGE_NAME, windowAnimations)
+ val info = builder.build().customAnimationInfo!!
+ whenever(
+ transitionAnimation.loadAnimationAttr(
+ eq(PACKAGE_NAME),
+ eq(windowAnimations),
+ anyInt(),
+ anyBoolean()
+ )
+ )
+ .thenReturn(mockCloseAnimation)
+ whenever(transitionAnimation.loadDefaultAnimationAttr(anyInt(), anyBoolean()))
+ .thenReturn(mockOpenAnimation)
+ val result = customAnimationLoader.loadAll(info)!!
+ if (exitResId != 0) {
+ if (enterResId == 0) {
+ verify(transitionAnimation, never())
+ .loadAppTransitionAnimation(eq(PACKAGE_NAME), eq(enterResId))
+ verify(transitionAnimation).loadDefaultAnimationAttr(anyInt(), anyBoolean())
+ } else {
+ assertEquals(result.enterAnimation, mockOpenAnimation)
+ }
+ assertEquals(result.backgroundColor.toLong(), Color.GREEN.toLong())
+ assertEquals(result.closeAnimation, mockCloseAnimation)
+ verify(transitionAnimation, never())
+ .loadAnimationAttr(eq(PACKAGE_NAME), anyInt(), anyInt(), anyBoolean())
+ } else if (windowAnimations != 0) {
+ verify(transitionAnimation, times(2))
+ .loadAnimationAttr(eq(PACKAGE_NAME), anyInt(), anyInt(), anyBoolean())
+ Assert.assertEquals(result.closeAnimation, mockCloseAnimation)
+ }
+ }
+
+ private fun startCustomAnimation(
+ targets: Array<RemoteAnimationTarget> =
+ arrayOf(createAnimationTarget(false), createAnimationTarget(true))
+ ): CountDownLatch {
+ val backNavigationInfo =
+ BackNavigationInfo.Builder()
+ .setCustomAnimation(PACKAGE_NAME, OPEN_RES_ID, CLOSE_RES_ID, /*backgroundColor*/ 0)
+ .build()
+ customCrossActivityBackAnimation.prepareNextAnimation(
+ backNavigationInfo.customAnimationInfo,
+ 0
+ )
+ val finishCalled = CountDownLatch(1)
+ val finishCallback = Runnable { finishCalled.countDown() }
+ customCrossActivityBackAnimation
+ .getRunner()
+ .startAnimation(targets, null, null, finishCallback)
+ customCrossActivityBackAnimation.runner.callback.onBackStarted(backMotionEventFrom(0f, 0f))
+ if (targets.isNotEmpty()) {
+ verify(mockCloseAnimation)
+ .initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), eq(BOUND_SIZE), eq(BOUND_SIZE))
+ verify(mockOpenAnimation)
+ .initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), eq(BOUND_SIZE), eq(BOUND_SIZE))
+ }
+ return finishCalled
+ }
+
+ private fun backMotionEventFrom(touchX: Float, progress: Float) =
+ BackMotionEvent(
+ /* touchX = */ touchX,
+ /* touchY = */ 0f,
+ /* progress = */ progress,
+ /* velocityX = */ 0f,
+ /* velocityY = */ 0f,
+ /* triggerBack = */ false,
+ /* swipeEdge = */ BackEvent.EDGE_LEFT,
+ /* departingAnimationTarget = */ null
+ )
+
+ private fun createAnimationTarget(open: Boolean): RemoteAnimationTarget {
+ val topWindowLeash = SurfaceControl()
+ val taskInfo = RunningTaskInfo()
+ taskInfo.appCompatTaskInfo = appCompatTaskInfo
+ taskInfo.taskDescription = ActivityManager.TaskDescription()
+ return RemoteAnimationTarget(
+ 1,
+ if (open) RemoteAnimationTarget.MODE_OPENING else RemoteAnimationTarget.MODE_CLOSING,
+ topWindowLeash,
+ false,
+ Rect(),
+ Rect(),
+ -1,
+ Point(0, 0),
+ Rect(0, 0, BOUND_SIZE, BOUND_SIZE),
+ Rect(),
+ WindowConfiguration(),
+ true,
+ null,
+ null,
+ taskInfo,
+ false,
+ -1
+ )
+ }
+
+ companion object {
+ private const val BOUND_SIZE = 100
+ private const val OPEN_RES_ID = 1000
+ private const val CLOSE_RES_ID = 1001
+ private const val PACKAGE_NAME = "TestPackage"
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java
deleted file mode 100644
index cebbbd890f05..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java
+++ /dev/null
@@ -1,237 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.wm.shell.back;
-
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-
-import android.app.WindowConfiguration;
-import android.graphics.Color;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.os.RemoteException;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.Choreographer;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.view.animation.Animation;
-import android.window.BackNavigationInfo;
-
-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 java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-@SmallTest
-@TestableLooper.RunWithLooper
-@RunWith(AndroidTestingRunner.class)
-public class CustomizeActivityAnimationTest extends ShellTestCase {
- private static final int BOUND_SIZE = 100;
- @Mock
- private BackAnimationBackground mBackAnimationBackground;
- @Mock
- private Animation mMockCloseAnimation;
- @Mock
- private Animation mMockOpenAnimation;
-
- private CustomizeActivityAnimation mCustomizeActivityAnimation;
-
- @Before
- public void setUp() throws Exception {
- mCustomizeActivityAnimation = new CustomizeActivityAnimation(mContext,
- mBackAnimationBackground, mock(SurfaceControl.Transaction.class),
- mock(Choreographer.class));
- spyOn(mCustomizeActivityAnimation);
- spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation);
- }
-
- RemoteAnimationTarget createAnimationTarget(boolean open) {
- SurfaceControl topWindowLeash = new SurfaceControl();
- return new RemoteAnimationTarget(1,
- open ? RemoteAnimationTarget.MODE_OPENING : RemoteAnimationTarget.MODE_CLOSING,
- topWindowLeash, false, new Rect(), new Rect(), -1,
- new Point(0, 0), new Rect(0, 0, BOUND_SIZE, BOUND_SIZE), new Rect(),
- new WindowConfiguration(), true, null, null, null, false, -1);
- }
-
- @Test
- public void receiveFinishAfterInvoke() throws InterruptedException {
- spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader);
- doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader)
- .loadAnimation(any(), eq(false));
- doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader)
- .loadAnimation(any(), eq(true));
-
- mCustomizeActivityAnimation.prepareNextAnimation(
- new BackNavigationInfo.CustomAnimationInfo("TestPackage"));
- final RemoteAnimationTarget close = createAnimationTarget(false);
- final RemoteAnimationTarget open = createAnimationTarget(true);
- // start animation with remote animation targets
- final CountDownLatch finishCalled = new CountDownLatch(1);
- final Runnable finishCallback = finishCalled::countDown;
- mCustomizeActivityAnimation
- .getRunner()
- .startAnimation(
- new RemoteAnimationTarget[] {close, open}, null, null, finishCallback);
- verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
- eq(BOUND_SIZE), eq(BOUND_SIZE));
- verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
- eq(BOUND_SIZE), eq(BOUND_SIZE));
-
- try {
- mCustomizeActivityAnimation.getRunner().getCallback().onBackInvoked();
- } catch (RemoteException r) {
- fail("onBackInvoked throw remote exception");
- }
- verify(mCustomizeActivityAnimation).onGestureCommitted();
- finishCalled.await(1, TimeUnit.SECONDS);
- }
-
- @Test
- public void receiveFinishAfterCancel() throws InterruptedException {
- spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader);
- doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader)
- .loadAnimation(any(), eq(false));
- doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader)
- .loadAnimation(any(), eq(true));
-
- mCustomizeActivityAnimation.prepareNextAnimation(
- new BackNavigationInfo.CustomAnimationInfo("TestPackage"));
- final RemoteAnimationTarget close = createAnimationTarget(false);
- final RemoteAnimationTarget open = createAnimationTarget(true);
- // start animation with remote animation targets
- final CountDownLatch finishCalled = new CountDownLatch(1);
- final Runnable finishCallback = finishCalled::countDown;
- mCustomizeActivityAnimation
- .getRunner()
- .startAnimation(
- new RemoteAnimationTarget[] {close, open}, null, null, finishCallback);
- verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
- eq(BOUND_SIZE), eq(BOUND_SIZE));
- verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE),
- eq(BOUND_SIZE), eq(BOUND_SIZE));
-
- try {
- mCustomizeActivityAnimation.getRunner().getCallback().onBackCancelled();
- } catch (RemoteException r) {
- fail("onBackCancelled throw remote exception");
- }
- finishCalled.await(1, TimeUnit.SECONDS);
- }
-
- @Test
- public void receiveFinishWithoutAnimationAfterInvoke() throws InterruptedException {
- mCustomizeActivityAnimation.prepareNextAnimation(
- new BackNavigationInfo.CustomAnimationInfo("TestPackage"));
- // start animation without any remote animation targets
- final CountDownLatch finishCalled = new CountDownLatch(1);
- final Runnable finishCallback = finishCalled::countDown;
- mCustomizeActivityAnimation
- .getRunner()
- .startAnimation(new RemoteAnimationTarget[] {}, null, null, finishCallback);
-
- try {
- mCustomizeActivityAnimation.getRunner().getCallback().onBackInvoked();
- } catch (RemoteException r) {
- fail("onBackInvoked throw remote exception");
- }
- verify(mCustomizeActivityAnimation).onGestureCommitted();
- finishCalled.await(1, TimeUnit.SECONDS);
- }
-
- @Test
- public void testLoadCustomAnimation() {
- testLoadCustomAnimation(10, 20, 0);
- }
-
- @Test
- public void testLoadCustomAnimationNoEnter() {
- testLoadCustomAnimation(0, 10, 0);
- }
-
- @Test
- public void testLoadWindowAnimations() {
- testLoadCustomAnimation(0, 0, 30);
- }
-
- @Test
- public void testCustomAnimationHigherThanWindowAnimations() {
- testLoadCustomAnimation(10, 20, 30);
- }
-
- private void testLoadCustomAnimation(int enterResId, int exitResId, int windowAnimations) {
- final String testPackage = "TestPackage";
- BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder()
- .setCustomAnimation(testPackage, enterResId, exitResId, Color.GREEN)
- .setWindowAnimations(testPackage, windowAnimations);
- final BackNavigationInfo.CustomAnimationInfo info = builder.build()
- .getCustomAnimationInfo();
-
- doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader
- .mTransitionAnimation)
- .loadAppTransitionAnimation(eq(testPackage), eq(enterResId));
- doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader
- .mTransitionAnimation)
- .loadAppTransitionAnimation(eq(testPackage), eq(exitResId));
- doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader
- .mTransitionAnimation)
- .loadAnimationAttr(eq(testPackage), eq(windowAnimations), anyInt(), anyBoolean());
- doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader
- .mTransitionAnimation).loadDefaultAnimationAttr(anyInt(), anyBoolean());
-
- CustomizeActivityAnimation.AnimationLoadResult result =
- mCustomizeActivityAnimation.mCustomAnimationLoader.loadAll(info);
-
- if (exitResId != 0) {
- if (enterResId == 0) {
- verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation,
- never()).loadAppTransitionAnimation(eq(testPackage), eq(enterResId));
- verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation)
- .loadDefaultAnimationAttr(anyInt(), anyBoolean());
- } else {
- assertEquals(result.mEnterAnimation, mMockOpenAnimation);
- }
- assertEquals(result.mBackgroundColor, Color.GREEN);
- assertEquals(result.mCloseAnimation, mMockCloseAnimation);
- verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, never())
- .loadAnimationAttr(eq(testPackage), anyInt(), anyInt(), anyBoolean());
- } else if (windowAnimations != 0) {
- verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation,
- times(2)).loadAnimationAttr(eq(testPackage), anyInt(), anyInt(), anyBoolean());
- assertEquals(result.mCloseAnimation, mMockCloseAnimation);
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt
deleted file mode 100644
index 6dbb1e2b8d92..000000000000
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/TouchTrackerTest.kt
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Copyright (C) 2022 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.back
-
-import android.util.MathUtils
-import android.window.BackEvent
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-class TouchTrackerTest {
- private fun linearTouchTracker(): TouchTracker = TouchTracker().apply {
- setProgressThresholds(MAX_DISTANCE, MAX_DISTANCE, NON_LINEAR_FACTOR)
- }
-
- private fun nonLinearTouchTracker(): TouchTracker = TouchTracker().apply {
- setProgressThresholds(LINEAR_DISTANCE, MAX_DISTANCE, NON_LINEAR_FACTOR)
- }
-
- private fun TouchTracker.assertProgress(expected: Float) {
- val actualProgress = createProgressEvent().progress
- assertEquals(expected, actualProgress, /* delta = */ 0f)
- }
-
- @Test
- fun generatesProgress_onStart() {
- val linearTracker = linearTouchTracker()
- linearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT)
- val event = linearTracker.createStartEvent(null)
- assertEquals(0f, event.progress, 0f)
- }
-
- @Test
- fun generatesProgress_leftEdge() {
- val linearTracker = linearTouchTracker()
- linearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT)
- var touchX = 10f
- val velocityX = 0f
- val velocityY = 0f
-
- // Pre-commit
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE)
-
- // Post-commit
- touchX += 100f
- linearTracker.setTriggerBack(true)
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE)
-
- // Cancel
- touchX -= 10f
- linearTracker.setTriggerBack(false)
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress(0f)
-
- // Cancel more
- touchX -= 10f
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress(0f)
-
- // Restarted, but pre-commit
- val restartX = touchX
- touchX += 10f
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((touchX - restartX) / MAX_DISTANCE)
-
- // continue restart within pre-commit
- touchX += 10f
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((touchX - restartX) / MAX_DISTANCE)
-
- // Restarted, post-commit
- touchX += 10f
- linearTracker.setTriggerBack(true)
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE)
- }
-
- @Test
- fun generatesProgress_rightEdge() {
- val linearTracker = linearTouchTracker()
- linearTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0f, BackEvent.EDGE_RIGHT)
- var touchX = INITIAL_X_RIGHT_EDGE - 10 // Fake right edge
- val velocityX = 0f
- val velocityY = 0f
- val target = MAX_DISTANCE
-
- // Pre-commit
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target)
-
- // Post-commit
- touchX -= 100f
- linearTracker.setTriggerBack(true)
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target)
-
- // Cancel
- touchX += 10f
- linearTracker.setTriggerBack(false)
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress(0f)
-
- // Cancel more
- touchX += 10f
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress(0f)
-
- // Restarted, but pre-commit
- val restartX = touchX
- touchX -= 10f
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((restartX - touchX) / target)
-
- // continue restart within pre-commit
- touchX -= 10f
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((restartX - touchX) / target)
-
- // Restarted, post-commit
- touchX -= 10f
- linearTracker.setTriggerBack(true)
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / target)
- }
-
- @Test
- fun generatesNonLinearProgress_leftEdge() {
- val nonLinearTracker = nonLinearTouchTracker()
- nonLinearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT)
- var touchX = 10f
- val velocityX = 0f
- val velocityY = 0f
- val linearTarget = LINEAR_DISTANCE + (MAX_DISTANCE - LINEAR_DISTANCE) * NON_LINEAR_FACTOR
-
- // Pre-commit: linear progress
- nonLinearTracker.update(touchX, 0f, velocityX, velocityY)
- nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget)
-
- // Post-commit: still linear progress
- touchX += 100f
- nonLinearTracker.setTriggerBack(true)
- nonLinearTracker.update(touchX, 0f, velocityX, velocityY)
- nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget)
-
- // still linear progress
- touchX = INITIAL_X_LEFT_EDGE + LINEAR_DISTANCE
- nonLinearTracker.update(touchX, 0f, velocityX, velocityY)
- nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / linearTarget)
-
- // non linear progress
- touchX += 10
- nonLinearTracker.update(touchX, 0f, velocityX, velocityY)
- val nonLinearTouch = (touchX - INITIAL_X_LEFT_EDGE) - LINEAR_DISTANCE
- val nonLinearProgress = nonLinearTouch / NON_LINEAR_DISTANCE
- val nonLinearTarget = MathUtils.lerp(linearTarget, MAX_DISTANCE, nonLinearProgress)
- nonLinearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / nonLinearTarget)
- }
-
- @Test
- fun restartingGesture_resetsInitialTouchX_leftEdge() {
- val linearTracker = linearTouchTracker()
- linearTracker.setGestureStartLocation(INITIAL_X_LEFT_EDGE, 0f, BackEvent.EDGE_LEFT)
- var touchX = 100f
- val velocityX = 0f
- val velocityY = 0f
-
- // assert that progress is increased when increasing touchX
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((touchX - INITIAL_X_LEFT_EDGE) / MAX_DISTANCE)
-
- // assert that progress is reset to 0 when start location is updated
- linearTracker.updateStartLocation()
- linearTracker.assertProgress(0f)
-
- // assert that progress remains 0 when touchX is decreased
- touchX -= 50
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress(0f)
-
- // assert that progress uses new minimal touchX for progress calculation
- val newInitialTouchX = touchX
- touchX += 100
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((touchX - newInitialTouchX) / MAX_DISTANCE)
-
- // assert the same for triggerBack==true
- linearTracker.triggerBack = true
- linearTracker.assertProgress((touchX - newInitialTouchX) / MAX_DISTANCE)
- }
-
- @Test
- fun restartingGesture_resetsInitialTouchX_rightEdge() {
- val linearTracker = linearTouchTracker()
- linearTracker.setGestureStartLocation(INITIAL_X_RIGHT_EDGE, 0f, BackEvent.EDGE_RIGHT)
-
- var touchX = INITIAL_X_RIGHT_EDGE - 100f
- val velocityX = 0f
- val velocityY = 0f
-
- // assert that progress is increased when decreasing touchX
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((INITIAL_X_RIGHT_EDGE - touchX) / MAX_DISTANCE)
-
- // assert that progress is reset to 0 when start location is updated
- linearTracker.updateStartLocation()
- linearTracker.assertProgress(0f)
-
- // assert that progress remains 0 when touchX is increased
- touchX += 50
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress(0f)
-
- // assert that progress uses new maximal touchX for progress calculation
- val newInitialTouchX = touchX
- touchX -= 100
- linearTracker.update(touchX, 0f, velocityX, velocityY)
- linearTracker.assertProgress((newInitialTouchX - touchX) / MAX_DISTANCE)
-
- // assert the same for triggerBack==true
- linearTracker.triggerBack = true
- linearTracker.assertProgress((newInitialTouchX - touchX) / MAX_DISTANCE)
- }
-
- companion object {
- private const val MAX_DISTANCE = 500f
- private const val LINEAR_DISTANCE = 400f
- private const val NON_LINEAR_DISTANCE = MAX_DISTANCE - LINEAR_DISTANCE
- private const val NON_LINEAR_FACTOR = 0.2f
- private const val INITIAL_X_LEFT_EDGE = 5f
- private const val INITIAL_X_RIGHT_EDGE = MAX_DISTANCE - INITIAL_X_LEFT_EDGE
- }
-} \ No newline at end of file
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 fa0aba5a6ee9..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
@@ -49,6 +49,8 @@ import androidx.test.filters.SmallTest;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.bubbles.BubbleData.TimeSource;
import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
+import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
import com.google.common.collect.ImmutableList;
@@ -1168,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);
@@ -1191,20 +1193,93 @@ public class BubbleDataTest extends ShellTestCase {
}
@Test
- public void test_removeOverflowBubble() {
- sendUpdatedEntryAtTime(mEntryA1, 2000);
+ public void test_getInitialStateForBubbleBar_includesInitialBubblesAndPosition() {
+ sendUpdatedEntryAtTime(mEntryA1, 1000);
+ sendUpdatedEntryAtTime(mEntryA2, 2000);
+ mPositioner.setBubbleBarLocation(BubbleBarLocation.LEFT);
+
+ 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).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
+ public void setSelectedBubbleAndExpandStack() {
+ sendUpdatedEntryAtTime(mEntryA1, 1000);
+ sendUpdatedEntryAtTime(mEntryA2, 2000);
+ mBubbleData.setListener(mListener);
+
+ mBubbleData.setSelectedBubbleAndExpandStack(mBubbleA1);
+
+ verifyUpdateReceived();
+ assertSelectionChangedTo(mBubbleA1);
+ assertExpandedChangedTo(true);
+ }
+
+ @Test
+ public void testShowOverflowChanged_hasOverflowBubbles() {
+ assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+ sendUpdatedEntryAtTime(mEntryA1, 1000);
mBubbleData.setListener(mListener);
mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
verifyUpdateReceived();
- assertOverflowChangedTo(ImmutableList.of(mBubbleA1));
+ assertThat(mUpdateCaptor.getValue().showOverflowChanged).isTrue();
+ assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
+ }
- mBubbleData.removeOverflowBubble(mBubbleA1);
+ @Test
+ public void testShowOverflowChanged_false_hasOverflowBubbles() {
+ assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
+ sendUpdatedEntryAtTime(mEntryA1, 1000);
+ sendUpdatedEntryAtTime(mEntryA2, 1000);
+ mBubbleData.setListener(mListener);
+
+ // First overflowed causes change event
+ mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
verifyUpdateReceived();
+ assertThat(mUpdateCaptor.getValue().showOverflowChanged).isTrue();
+ assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
- BubbleData.Update update = mUpdateCaptor.getValue();
- assertThat(update.removedOverflowBubble).isEqualTo(mBubbleA1);
- assertOverflowChangedTo(ImmutableList.of());
+ // Second overflow does not
+ mBubbleData.dismissBubbleWithKey(mEntryA2.getKey(), Bubbles.DISMISS_USER_GESTURE);
+ verifyUpdateReceived();
+ assertThat(mUpdateCaptor.getValue().showOverflowChanged).isFalse();
+ }
+
+ @Test
+ public void testShowOverflowChanged_noOverflowBubbles() {
+ sendUpdatedEntryAtTime(mEntryA1, 1000);
+ mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_USER_GESTURE);
+ assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
+ mBubbleData.setListener(mListener);
+
+ mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
+
+ verifyUpdateReceived();
+ assertThat(mUpdateCaptor.getValue().showOverflowChanged).isTrue();
+ assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
}
private void verifyUpdateReceived() {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
index ae39fbcb4eed..4a4c5e860bb2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt
@@ -37,6 +37,7 @@ 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.common.DisplayController
+import com.android.wm.shell.common.DisplayInsetsController
import com.android.wm.shell.common.FloatingContentCoordinator
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
@@ -94,7 +95,8 @@ class BubbleViewInfoTest : ShellTestCase() {
val windowManager = context.getSystemService(WindowManager::class.java)
val shellInit = ShellInit(mainExecutor)
val shellCommandHandler = ShellCommandHandler()
- val shellController = ShellController(context, shellInit, shellCommandHandler, mainExecutor)
+ val shellController = ShellController(context, shellInit, shellCommandHandler,
+ mock<DisplayInsetsController>(), mainExecutor)
bubblePositioner = BubblePositioner(context, windowManager)
val bubbleData =
BubbleData(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTest.java
index 964711ee8dcb..043128583432 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/animation/PhysicsAnimationLayoutTest.java
@@ -69,7 +69,8 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
// to animate child views out before actually removing them).
mTestableController.setAnimatedProperties(Sets.newHashSet(
DynamicAnimation.TRANSLATION_X,
- DynamicAnimation.TRANSLATION_Y));
+ DynamicAnimation.TRANSLATION_Y,
+ DynamicAnimation.TRANSLATION_Z));
mTestableController.setChainedProperties(Sets.newHashSet(DynamicAnimation.TRANSLATION_X));
mTestableController.setOffsetForProperty(
DynamicAnimation.TRANSLATION_X, TEST_TRANSLATION_X_OFFSET);
@@ -282,10 +283,13 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
addOneMoreThanBubbleLimitBubbles();
assertFalse(mLayout.arePropertiesAnimating(
- DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y));
+ DynamicAnimation.TRANSLATION_X,
+ DynamicAnimation.TRANSLATION_Y,
+ DynamicAnimation.TRANSLATION_Z));
mTestableController.animationForChildAtIndex(0)
.translationX(100f)
+ .translationZ(100f)
.start();
// Wait for the animations to get underway.
@@ -293,11 +297,13 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
assertTrue(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_X));
assertFalse(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_Y));
+ assertTrue(mLayout.arePropertiesAnimating(DynamicAnimation.TRANSLATION_Z));
- waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X);
+ waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Z);
assertFalse(mLayout.arePropertiesAnimating(
- DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y));
+ DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y,
+ DynamicAnimation.TRANSLATION_Z));
}
@Test
@@ -307,7 +313,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
addOneMoreThanBubbleLimitBubbles();
mTestableController.animationForChildAtIndex(0)
- .position(1000, 1000)
+ .position(1000, 1000, 1000)
.start();
mLayout.cancelAllAnimations();
@@ -315,6 +321,7 @@ public class PhysicsAnimationLayoutTest extends PhysicsAnimationLayoutTestCase {
// Animations should be somewhere before their end point.
assertTrue(mViews.get(0).getTranslationX() < 1000);
assertTrue(mViews.get(0).getTranslationY() < 1000);
+ assertTrue(mViews.get(0).getZ() < 10000);
}
/** Standard test of chained translation animations. */
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt
index 2f5fe11634a4..bec91e910cf7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/MultiInstanceHelperTest.kt
@@ -32,9 +32,12 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.eq
+import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
@@ -77,7 +80,7 @@ class MultiInstanceHelperTest : ShellTestCase() {
@Test
fun supportsMultiInstanceSplit_inStaticAllowList() {
val allowList = arrayOf(TEST_PACKAGE)
- val helper = MultiInstanceHelper(mContext, context.packageManager, allowList)
+ val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true)
val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
assertEquals(true, helper.supportsMultiInstanceSplit(component))
}
@@ -85,7 +88,7 @@ class MultiInstanceHelperTest : ShellTestCase() {
@Test
fun supportsMultiInstanceSplit_notInStaticAllowList() {
val allowList = arrayOf(TEST_PACKAGE)
- val helper = MultiInstanceHelper(mContext, context.packageManager, allowList)
+ val helper = MultiInstanceHelper(mContext, context.packageManager, allowList, true)
val component = ComponentName(TEST_NOT_ALLOWED_PACKAGE, TEST_ACTIVITY)
assertEquals(false, helper.supportsMultiInstanceSplit(component))
}
@@ -104,7 +107,7 @@ class MultiInstanceHelperTest : ShellTestCase() {
eq(component.packageName)))
.thenReturn(appProp)
- val helper = MultiInstanceHelper(mContext, pm, emptyArray())
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
// Expect activity property to override application property
assertEquals(true, helper.supportsMultiInstanceSplit(component))
}
@@ -123,7 +126,7 @@ class MultiInstanceHelperTest : ShellTestCase() {
eq(component.packageName)))
.thenReturn(appProp)
- val helper = MultiInstanceHelper(mContext, pm, emptyArray())
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
// Expect activity property to override application property
assertEquals(false, helper.supportsMultiInstanceSplit(component))
}
@@ -141,7 +144,7 @@ class MultiInstanceHelperTest : ShellTestCase() {
eq(component.packageName)))
.thenReturn(appProp)
- val helper = MultiInstanceHelper(mContext, pm, emptyArray())
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
// Expect fall through to app property
assertEquals(true, helper.supportsMultiInstanceSplit(component))
}
@@ -158,10 +161,30 @@ class MultiInstanceHelperTest : ShellTestCase() {
eq(component.packageName)))
.thenThrow(PackageManager.NameNotFoundException())
- val helper = MultiInstanceHelper(mContext, pm, emptyArray())
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), true)
assertEquals(false, helper.supportsMultiInstanceSplit(component))
}
+ @Test
+ @Throws(PackageManager.NameNotFoundException::class)
+ fun checkNoMultiInstancePropertyFlag_ignoreProperty() {
+ val component = ComponentName(TEST_PACKAGE, TEST_ACTIVITY)
+ val pm = mock<PackageManager>()
+ val activityProp = PackageManager.Property("", true, "", "")
+ whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component)))
+ .thenReturn(activityProp)
+ val appProp = PackageManager.Property("", true, "", "")
+ whenever(pm.getProperty(eq(PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI),
+ eq(component.packageName)))
+ .thenReturn(appProp)
+
+ val helper = MultiInstanceHelper(mContext, pm, emptyArray(), false)
+ // Expect we only check the static list and not the property
+ assertEquals(false, helper.supportsMultiInstanceSplit(component))
+ verify(pm, never()).getProperty(any(), any<ComponentName>())
+ }
+
companion object {
val TEST_PACKAGE = "com.android.wm.shell.common"
val TEST_NOT_ALLOWED_PACKAGE = "com.android.wm.shell.common.fake";
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleBarLocationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleBarLocationTest.kt
new file mode 100644
index 000000000000..27e0b196f0be
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/bubbles/BubbleBarLocationTest.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.common.bubbles
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.bubbles.BubbleBarLocation.DEFAULT
+import com.android.wm.shell.common.bubbles.BubbleBarLocation.LEFT
+import com.android.wm.shell.common.bubbles.BubbleBarLocation.RIGHT
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class BubbleBarLocationTest : ShellTestCase() {
+
+ @Test
+ fun isOnLeft_rtlEnabled_defaultsToLeft() {
+ assertThat(DEFAULT.isOnLeft(isRtl = true)).isTrue()
+ }
+
+ @Test
+ fun isOnLeft_rtlDisabled_defaultsToRight() {
+ assertThat(DEFAULT.isOnLeft(isRtl = false)).isFalse()
+ }
+
+ @Test
+ fun isOnLeft_left_trueForAllLanguageDirections() {
+ assertThat(LEFT.isOnLeft(isRtl = false)).isTrue()
+ assertThat(LEFT.isOnLeft(isRtl = true)).isTrue()
+ }
+
+ @Test
+ fun isOnLeft_right_falseForAllLanguageDirections() {
+ assertThat(RIGHT.isOnLeft(isRtl = false)).isFalse()
+ assertThat(RIGHT.isOnLeft(isRtl = true)).isFalse()
+ }
+} \ No newline at end of file
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/common/magnetictarget/MagnetizedObjectTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
index a4fb3504f31d..8bb182de7668 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
@@ -22,7 +22,7 @@ import android.view.View
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.animation.PhysicsAnimatorTestUtils
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
index 56d0f8e13f08..cfe8e07aa6e5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java
@@ -26,6 +26,7 @@ import static com.android.wm.shell.common.split.SplitScreenConstants.SNAP_TO_STA
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
@@ -115,27 +116,27 @@ public class SplitLayoutTests extends ShellTestCase {
@Test
public void testUpdateDivideBounds() {
- mSplitLayout.updateDivideBounds(anyInt());
+ mSplitLayout.updateDividerBounds(anyInt(), anyBoolean());
verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class), anyInt(),
- anyInt());
+ anyInt(), anyBoolean());
}
@Test
public void testSetDividePosition() {
- mSplitLayout.setDividePosition(100, false /* applyLayoutChange */);
- assertThat(mSplitLayout.getDividePosition()).isEqualTo(100);
+ mSplitLayout.setDividerPosition(100, false /* applyLayoutChange */);
+ assertThat(mSplitLayout.getDividerPosition()).isEqualTo(100);
verify(mSplitLayoutHandler, never()).onLayoutSizeChanged(any(SplitLayout.class));
- mSplitLayout.setDividePosition(200, true /* applyLayoutChange */);
- assertThat(mSplitLayout.getDividePosition()).isEqualTo(200);
+ mSplitLayout.setDividerPosition(200, true /* applyLayoutChange */);
+ assertThat(mSplitLayout.getDividerPosition()).isEqualTo(200);
verify(mSplitLayoutHandler).onLayoutSizeChanged(any(SplitLayout.class));
}
@Test
public void testSetDivideRatio() {
- mSplitLayout.setDividePosition(200, false /* applyLayoutChange */);
+ mSplitLayout.setDividerPosition(200, false /* applyLayoutChange */);
mSplitLayout.setDivideRatio(SNAP_TO_50_50);
- assertThat(mSplitLayout.getDividePosition()).isEqualTo(
+ assertThat(mSplitLayout.getDividerPosition()).isEqualTo(
mSplitLayout.mDividerSnapAlgorithm.getMiddleTarget().position);
}
@@ -152,7 +153,7 @@ public class SplitLayoutTests extends ShellTestCase {
DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */,
SNAP_TO_START_AND_DISMISS);
- mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), snapTarget);
+ mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), snapTarget);
waitDividerFlingFinished();
verify(mSplitLayoutHandler).onSnappedToDismiss(eq(false), anyInt());
}
@@ -164,7 +165,7 @@ public class SplitLayoutTests extends ShellTestCase {
DividerSnapAlgorithm.SnapTarget snapTarget = getSnapTarget(0 /* position */,
SNAP_TO_END_AND_DISMISS);
- mSplitLayout.snapToTarget(mSplitLayout.getDividePosition(), snapTarget);
+ mSplitLayout.snapToTarget(mSplitLayout.getDividerPosition(), snapTarget);
waitDividerFlingFinished();
verify(mSplitLayoutHandler).onSnappedToDismiss(eq(true), anyInt());
}
@@ -188,7 +189,7 @@ public class SplitLayoutTests extends ShellTestCase {
}
private void waitDividerFlingFinished() {
- verify(mSplitLayout).flingDividePosition(anyInt(), anyInt(), anyInt(),
+ verify(mSplitLayout).flingDividerPosition(anyInt(), anyInt(), anyInt(),
mRunnableCaptor.capture());
mRunnableCaptor.getValue().run();
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt
new file mode 100644
index 000000000000..4cd2a366f5eb
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.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.compatui
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Tests for {@link AppCompatUtils}.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:AppCompatUtilsTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class AppCompatUtilsTest : ShellTestCase() {
+
+ @Test
+ fun testIsSingleTopActivityTranslucent() {
+ assertTrue(isSingleTopActivityTranslucent(
+ createFreeformTask(/* displayId */ 0)
+ .apply {
+ isTopActivityTransparent = true
+ numActivities = 1
+ }))
+ assertFalse(isSingleTopActivityTranslucent(
+ createFreeformTask(/* displayId */ 0)
+ .apply {
+ isTopActivityTransparent = true
+ numActivities = 0
+ }))
+ assertFalse(isSingleTopActivityTranslucent(
+ createFreeformTask(/* displayId */ 0)
+ .apply {
+ isTopActivityTransparent = false
+ numActivities = 1
+ }))
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index fef81af8946b..9c008647104a 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -16,8 +16,8 @@
package com.android.wm.shell.compatui;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
import static android.view.WindowInsets.Type.navigationBars;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -34,7 +34,7 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.ActivityManager.RunningTaskInfo;
-import android.app.AppCompatTaskInfo.CameraCompatControlState;
+import android.app.CameraCompatTaskInfo.CameraCompatControlState;
import android.app.TaskInfo;
import android.content.Context;
import android.content.res.Configuration;
@@ -668,6 +668,18 @@ public class CompatUIControllerTest extends ShellTestCase {
Assert.assertTrue(mController.hasShownUserAspectRatioSettingsButton());
}
+ @Test
+ public void testLetterboxEduLayout_notCreatedWhenLetterboxEducationIsDisabled() {
+ TaskInfo taskInfo = createTaskInfo(DISPLAY_ID, TASK_ID, /* hasSizeCompat= */ true,
+ CAMERA_COMPAT_CONTROL_HIDDEN);
+ taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = false;
+
+ mController.onCompatInfoChanged(taskInfo, mMockTaskListener);
+
+ verify(mController, never()).createLetterboxEduWindowManager(any(), eq(taskInfo),
+ eq(mMockTaskListener));
+ }
+
private static TaskInfo createTaskInfo(int displayId, int taskId, boolean hasSizeCompat,
@CameraCompatControlState int cameraCompatControlState) {
return createTaskInfo(displayId, taskId, hasSizeCompat, cameraCompatControlState,
@@ -689,10 +701,13 @@ public class CompatUIControllerTest extends ShellTestCase {
taskInfo.taskId = taskId;
taskInfo.displayId = displayId;
taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat;
- taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState;
+ taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
+ cameraCompatControlState;
taskInfo.isVisible = isVisible;
taskInfo.isFocused = isFocused;
taskInfo.isTopActivityTransparent = isTopActivityTransparent;
+ taskInfo.appCompatTaskInfo.isLetterboxEducationEnabled = true;
+ taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed = true;
return taskInfo;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
index dd358e757fde..cd3e8cb0e8e1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
@@ -16,10 +16,10 @@
package com.android.wm.shell.compatui;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -28,7 +28,7 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
-import android.app.AppCompatTaskInfo.CameraCompatControlState;
+import android.app.CameraCompatTaskInfo.CameraCompatControlState;
import android.app.TaskInfo;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
@@ -222,7 +222,8 @@ public class CompatUILayoutTest extends ShellTestCase {
ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
taskInfo.taskId = TASK_ID;
taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat;
- taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState;
+ taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
+ cameraCompatControlState;
taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000;
taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000;
taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000));
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
index 4f261cd79d39..41a81c1a9921 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -16,12 +16,13 @@
package com.android.wm.shell.compatui;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -37,7 +38,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
-import android.app.AppCompatTaskInfo;
+import android.app.CameraCompatTaskInfo;
import android.app.TaskInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
@@ -98,14 +99,28 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
private CompatUIWindowManager mWindowManager;
private TaskInfo mTaskInfo;
+ private DisplayLayout mDisplayLayout;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance();
mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
+
+ final DisplayInfo displayInfo = new DisplayInfo();
+ displayInfo.logicalWidth = TASK_WIDTH;
+ displayInfo.logicalHeight = TASK_HEIGHT;
+ mDisplayLayout = new DisplayLayout(displayInfo,
+ mContext.getResources(), /* hasNavigationBar= */ true, /* hasStatusBar= */ false);
+ final InsetsState insetsState = new InsetsState();
+ insetsState.setDisplayFrame(new Rect(0, 0, TASK_WIDTH, TASK_HEIGHT));
+ final InsetsSource insetsSource = new InsetsSource(
+ InsetsSource.createId(null, 0, navigationBars()), navigationBars());
+ insetsSource.setFrame(0, TASK_HEIGHT - 200, TASK_WIDTH, TASK_HEIGHT);
+ insetsState.addSource(insetsSource);
+ mDisplayLayout.setInsets(mContext.getResources(), insetsState);
mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
- mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
+ mCallback, mTaskListener, mDisplayLayout, new CompatUIHintsState(),
mCompatUIConfiguration, mOnRestartButtonClicked);
spyOn(mWindowManager);
@@ -363,9 +378,9 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
// Update if the insets change on the existing display layout
clearInvocations(mWindowManager);
- InsetsState insetsState = new InsetsState();
+ final InsetsState insetsState = new InsetsState();
insetsState.setDisplayFrame(new Rect(0, 0, 1000, 2000));
- InsetsSource insetsSource = new InsetsSource(
+ final InsetsSource insetsSource = new InsetsSource(
InsetsSource.createId(null, 0, navigationBars()), navigationBars());
insetsSource.setFrame(0, 1800, 1000, 2000);
insetsState.addSource(insetsSource);
@@ -493,16 +508,14 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
@Test
public void testShouldShowSizeCompatRestartButton() {
mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_HIDE_SCM_BUTTON);
-
- doReturn(86).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance();
+ doReturn(85).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance();
mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
- mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
+ mCallback, mTaskListener, mDisplayLayout, new CompatUIHintsState(),
mCompatUIConfiguration, mOnRestartButtonClicked);
// Simulate rotation of activity in square display
TaskInfo taskInfo = createTaskInfo(true, CAMERA_COMPAT_CONTROL_HIDDEN);
- taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000));
- taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 2000;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = TASK_HEIGHT;
taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1850;
assertFalse(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo));
@@ -512,26 +525,37 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo));
// Simulate folding
- taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 1000, 2000));
- assertFalse(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo));
+ final InsetsState insetsState = new InsetsState();
+ insetsState.setDisplayFrame(new Rect(0, 0, 1000, TASK_HEIGHT));
+ final InsetsSource insetsSource = new InsetsSource(
+ InsetsSource.createId(null, 0, navigationBars()), navigationBars());
+ insetsSource.setFrame(0, TASK_HEIGHT - 200, 1000, TASK_HEIGHT);
+ insetsState.addSource(insetsSource);
+ mDisplayLayout.setInsets(mContext.getResources(), insetsState);
+ mWindowManager.updateDisplayLayout(mDisplayLayout);
+ taskInfo.configuration.smallestScreenWidthDp = LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP - 100;
+ assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo));
- taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000;
- taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 500;
+ // Simulate floating app with 90& area, more than tolerance
+ taskInfo.configuration.smallestScreenWidthDp = LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 950;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1900;
assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo));
}
private static TaskInfo createTaskInfo(boolean hasSizeCompat,
- @AppCompatTaskInfo.CameraCompatControlState int cameraCompatControlState) {
+ @CameraCompatTaskInfo.CameraCompatControlState int cameraCompatControlState) {
ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
taskInfo.taskId = TASK_ID;
taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat;
- taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState;
+ taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
+ cameraCompatControlState;
taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK;
// Letterboxed activity that takes half the screen should show size compat restart button
- taskInfo.configuration.windowConfiguration.setBounds(
- new Rect(0, 0, TASK_WIDTH, TASK_HEIGHT));
taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000;
taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000;
+ // Screen width dp larger than a normal phone.
+ taskInfo.configuration.smallestScreenWidthDp = LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP;
return taskInfo;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
index 38d6ea1839c4..02316125bcc3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java
@@ -16,7 +16,7 @@
package com.android.wm.shell.compatui;
-import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
+import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -25,7 +25,7 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import android.app.ActivityManager;
-import android.app.AppCompatTaskInfo.CameraCompatControlState;
+import android.app.CameraCompatTaskInfo.CameraCompatControlState;
import android.app.TaskInfo;
import android.content.ComponentName;
import android.testing.AndroidTestingRunner;
@@ -148,7 +148,8 @@ public class UserAspectRatioSettingsLayoutTest extends ShellTestCase {
ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
taskInfo.taskId = TASK_ID;
taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat;
- taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState;
+ taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState =
+ cameraCompatControlState;
taskInfo.realActivity = new ComponentName("com.mypackage.test", "TestActivity");
return taskInfo;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
index 81ba4b37d13b..94e168ed70ed 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java
@@ -292,6 +292,24 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase {
}
@Test
+ public void testUserFullscreenOverrideEnabled_buttonAlwaysShown() {
+ TaskInfo taskInfo = createTaskInfo(/* eligibleForUserAspectRatioButton= */
+ true, /* topActivityBoundsLetterboxed */ true, ACTION_MAIN, CATEGORY_LAUNCHER);
+
+ final Rect stableBounds = mWindowManager.getTaskStableBounds();
+
+ // Letterboxed activity that has user fullscreen override should always show button,
+ // layout should be inflated
+ taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = stableBounds.height();
+ taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = stableBounds.width();
+ taskInfo.appCompatTaskInfo.isUserFullscreenOverrideEnabled = true;
+
+ mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true);
+
+ verify(mWindowManager).inflateLayout();
+ }
+
+ @Test
public void testUpdateDisplayLayout() {
final DisplayInfo displayInfo = new DisplayInfo();
displayInfo.logicalWidth = 1000;
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
new file mode 100644
index 000000000000..fb03f20f939c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -0,0 +1,637 @@
+/*
+ * 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.app.ActivityManager
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.content.Context
+import android.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.view.WindowManager.TRANSIT_CLOSE
+import android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS
+import android.view.WindowManager.TRANSIT_NONE
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_SLEEP
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.view.WindowManager.TRANSIT_TO_FRONT
+import android.view.WindowManager.TRANSIT_WAKE
+import android.window.IWindowContainerToken
+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.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.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 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.never
+import org.mockito.kotlin.same
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verifyZeroInteractions
+
+/**
+ * Test class for {@link DesktopModeLoggerTransitionObserver}
+ *
+ * Usage: atest WMShellUnitTests:DesktopModeLoggerTransitionObserverTest
+ */
+@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(
+ 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 transitOpen_notFreeformWindow_doesNotLogTaskAddedOrSessionEnter() {
+ 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 transitOpen_logTaskAddedAndEnterReasonAppFreeformIntent() {
+ 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())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitEndDragToDesktop_logTaskAddedAndEnterReasonAppHandleDrag() {
+ 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())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitEnterDesktopByButtonTap_logTaskAddedAndEnterReasonButtonTap() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, 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())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitEnterDesktopFromAppFromOverview_logTaskAddedAndEnterReasonAppFromOverview() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0)
+ .addChange(change)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FROM_OVERVIEW))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitEnterDesktopFromKeyboardShortcut_logTaskAddedAndEnterReasonKeyboardShortcut() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, 0)
+ .addChange(change)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.KEYBOARD_SHORTCUT_ENTER))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitToFront_logTaskAddedAndEnterReasonOverview() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.OVERVIEW))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitToFront_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() {
+ // previous exit to overview transition
+ val previousSessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(previousSessionId)
+ val previousChange = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val previousTransitionInfo =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+ .addChange(previousChange)
+ .build()
+
+ callOnTransitionReady(previousTransitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(previousSessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(previousSessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+
+ // 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(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.OVERVIEW))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitChange_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() {
+ // previous exit to overview transition
+ val previousSessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(previousSessionId)
+ val previousChange = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val previousTransitionInfo =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+ .addChange(previousChange)
+ .build()
+
+ callOnTransitionReady(previousTransitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(previousSessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(previousSessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+
+ // 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(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_CHANGE, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.OVERVIEW))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitOpen_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() {
+ // previous exit to overview transition
+ val previousSessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(previousSessionId)
+ val previousChange = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val previousTransitionInfo =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+ .addChange(previousChange)
+ .build()
+
+ callOnTransitionReady(previousTransitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(previousSessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(previousSessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+
+ // 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(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.OVERVIEW))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @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
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(previousSessionId)
+ val previousChange = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val previousTransitionInfo =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+ .addChange(previousChange)
+ .build()
+
+ callOnTransitionReady(previousTransitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(previousSessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(previousSessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+
+ // TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0)
+ .addChange(change)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FROM_OVERVIEW))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitEnterDesktopFromUnknown_logTaskAddedAndEnterReasonUnknown() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.UNKNOWN_ENTER))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitWake_logTaskAddedAndEnterReasonScreenOn() {
+ 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())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitSleep_logTaskAddedAndExitReasonScreenOff_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))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun transitExitDesktopTaskDrag_logTaskRemovedAndExitReasonDragToExit_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(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG).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))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun transitExitDesktopAppHandleButton_logTaskRemovedAndExitReasonButton_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(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON)
+ .addChange(change)
+ .build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(sessionId), eq(ExitReason.APP_HANDLE_MENU_BUTTON_EXIT))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun transitExitDesktopUsingKeyboard_logTaskRemovedAndExitReasonKeyboard_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(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(sessionId), eq(ExitReason.KEYBOARD_SHORTCUT_EXIT))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun transitExitDesktopUnknown_logTaskRemovedAndExitReasonUnknown_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(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(sessionId), eq(ExitReason.UNKNOWN_EXIT))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun transitToFrontWithFlagRecents_logTaskRemovedAndExitReasonOverview_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))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun transitClose_logTaskRemovedAndExitReasonTaskFinished_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))
+ verifyZeroInteractions(desktopModeEventLogger)
+ 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
+ }
+
+ 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
+ }
+ }
+}
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 445f74a52b0d..310ccc252469 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
@@ -16,8 +16,10 @@
package com.android.wm.shell.desktopmode
+import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.view.Display.DEFAULT_DISPLAY
+import android.view.Display.INVALID_DISPLAY
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
@@ -117,27 +119,66 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
}
@Test
- fun addListener_notifiesVisibleFreeformTask() {
- repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
- val listener = TestVisibilityListener()
- val executor = TestShellExecutor()
- repo.addVisibleTasksListener(listener, executor)
- executor.flushAll()
+ fun isOnlyActiveTask_noActiveTasks() {
+ // Not an active task
+ assertThat(repo.isOnlyActiveTask(1)).isFalse()
+ }
- assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
- assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
+ @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()
}
@Test
- fun addListener_notifiesStashed() {
- repo.setStashed(DEFAULT_DISPLAY, true)
+ fun isOnlyActiveTask_multipleActiveTasks() {
+ repo.addActiveTask(DEFAULT_DISPLAY, 1)
+ repo.addActiveTask(DEFAULT_DISPLAY, 2)
+ // Not the only task
+ assertThat(repo.isActiveTask(1)).isTrue()
+ assertThat(repo.isOnlyActiveTask(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()
+ }
+
+ @Test
+ fun isOnlyActiveTask_multipleDisplays() {
+ repo.addActiveTask(DEFAULT_DISPLAY, 1)
+ repo.addActiveTask(DEFAULT_DISPLAY, 2)
+ repo.addActiveTask(SECOND_DISPLAY, 3)
+ // Not the only task on DEFAULT_DISPLAY
+ assertThat(repo.isActiveTask(1)).isTrue()
+ assertThat(repo.isOnlyActiveTask(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()
+ }
+
+ @Test
+ fun addListener_notifiesVisibleFreeformTask() {
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
val listener = TestVisibilityListener()
val executor = TestShellExecutor()
repo.addVisibleTasksListener(listener, executor)
executor.flushAll()
- assertThat(listener.stashedOnDefaultDisplay).isTrue()
- assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1)
+ assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1)
}
@Test
@@ -237,6 +278,27 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4)
}
+ /**
+ * When a task vanishes, the displayId of the task is set to INVALID_DISPLAY.
+ * This tests that task is removed from the last parent display when it vanishes.
+ */
+ @Test
+ fun updateVisibleFreeformTasks_removeVisibleTasksRemovesTaskWithInvalidDisplay() {
+ val listener = TestVisibilityListener()
+ val executor = TestShellExecutor()
+ repo.addVisibleTasksListener(listener, executor)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true)
+ repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true)
+ executor.flushAll()
+
+ assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2)
+ repo.updateVisibleFreeformTasks(INVALID_DISPLAY, taskId = 1, visible = false)
+ executor.flushAll()
+
+ assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3)
+ assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1)
+ }
+
@Test
fun getVisibleTaskCount() {
// No tasks, count is 0
@@ -301,11 +363,11 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
@Test
fun addOrMoveFreeformTaskToTop_didNotExist_addsToTop() {
- repo.addOrMoveFreeformTaskToTop(5)
- repo.addOrMoveFreeformTaskToTop(6)
- repo.addOrMoveFreeformTaskToTop(7)
+ repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5)
+ repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6)
+ repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7)
- val tasks = repo.getFreeformTasksInZOrder()
+ val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
assertThat(tasks.size).isEqualTo(3)
assertThat(tasks[0]).isEqualTo(7)
assertThat(tasks[1]).isEqualTo(6)
@@ -314,76 +376,138 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
@Test
fun addOrMoveFreeformTaskToTop_alreadyExists_movesToTop() {
- repo.addOrMoveFreeformTaskToTop(5)
- repo.addOrMoveFreeformTaskToTop(6)
- repo.addOrMoveFreeformTaskToTop(7)
+ repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5)
+ repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6)
+ repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7)
- repo.addOrMoveFreeformTaskToTop(6)
+ repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6)
- val tasks = repo.getFreeformTasksInZOrder()
+ val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)
assertThat(tasks.size).isEqualTo(3)
assertThat(tasks.first()).isEqualTo(6)
}
@Test
- fun setStashed_stateIsUpdatedForTheDisplay() {
- repo.setStashed(DEFAULT_DISPLAY, true)
- assertThat(repo.isStashed(DEFAULT_DISPLAY)).isTrue()
- assertThat(repo.isStashed(SECOND_DISPLAY)).isFalse()
+ fun removeFreeformTask_removesTaskBoundsBeforeMaximize() {
+ val taskId = 1
+ repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200))
+ repo.removeFreeformTask(THIRD_DISPLAY, taskId)
+ assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull()
+ }
- repo.setStashed(DEFAULT_DISPLAY, false)
- assertThat(repo.isStashed(DEFAULT_DISPLAY)).isFalse()
+ @Test
+ fun saveBoundsBeforeMaximize_boundsSavedByTaskId() {
+ val taskId = 1
+ val bounds = Rect(0, 0, 200, 200)
+ repo.saveBoundsBeforeMaximize(taskId, bounds)
+ assertThat(repo.removeBoundsBeforeMaximize(taskId)).isEqualTo(bounds)
}
@Test
- fun setStashed_notifyListener() {
- val listener = TestVisibilityListener()
- val executor = TestShellExecutor()
- repo.addVisibleTasksListener(listener, executor)
- repo.setStashed(DEFAULT_DISPLAY, true)
- executor.flushAll()
- assertThat(listener.stashedOnDefaultDisplay).isTrue()
- assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1)
+ fun removeBoundsBeforeMaximize_returnsNullAfterBoundsRemoved() {
+ val taskId = 1
+ val bounds = Rect(0, 0, 200, 200)
+ repo.saveBoundsBeforeMaximize(taskId, bounds)
+ repo.removeBoundsBeforeMaximize(taskId)
+ assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull()
+ }
- repo.setStashed(DEFAULT_DISPLAY, false)
- executor.flushAll()
- assertThat(listener.stashedOnDefaultDisplay).isFalse()
- assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(2)
+ @Test
+ fun minimizeTaskNotCalled_noTasksMinimized() {
+ assertThat(repo.isMinimizedTask(taskId = 0)).isFalse()
+ assertThat(repo.isMinimizedTask(taskId = 1)).isFalse()
}
@Test
- fun setStashed_secondCallDoesNotNotify() {
- val listener = TestVisibilityListener()
- val executor = TestShellExecutor()
- repo.addVisibleTasksListener(listener, executor)
- repo.setStashed(DEFAULT_DISPLAY, true)
- repo.setStashed(DEFAULT_DISPLAY, true)
- executor.flushAll()
- assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1)
+ fun minimizeTask_onlyThatTaskIsMinimized() {
+ repo.minimizeTask(displayId = 0, taskId = 0)
+
+ assertThat(repo.isMinimizedTask(taskId = 0)).isTrue()
+ assertThat(repo.isMinimizedTask(taskId = 1)).isFalse()
+ assertThat(repo.isMinimizedTask(taskId = 2)).isFalse()
}
@Test
- fun setStashed_tracksPerDisplay() {
- val listener = TestVisibilityListener()
- val executor = TestShellExecutor()
- repo.addVisibleTasksListener(listener, executor)
+ fun unminimizeTask_taskNoLongerMinimized() {
+ repo.minimizeTask(displayId = 0, taskId = 0)
+ repo.unminimizeTask(displayId = 0, taskId = 0)
- repo.setStashed(DEFAULT_DISPLAY, true)
- executor.flushAll()
- assertThat(listener.stashedOnDefaultDisplay).isTrue()
- assertThat(listener.stashedOnSecondaryDisplay).isFalse()
+ assertThat(repo.isMinimizedTask(taskId = 0)).isFalse()
+ assertThat(repo.isMinimizedTask(taskId = 1)).isFalse()
+ assertThat(repo.isMinimizedTask(taskId = 2)).isFalse()
+ }
- repo.setStashed(SECOND_DISPLAY, true)
- executor.flushAll()
- assertThat(listener.stashedOnDefaultDisplay).isTrue()
- assertThat(listener.stashedOnSecondaryDisplay).isTrue()
+ @Test
+ fun unminimizeTask_nonExistentTask_doesntCrash() {
+ repo.unminimizeTask(displayId = 0, taskId = 0)
- repo.setStashed(DEFAULT_DISPLAY, false)
- executor.flushAll()
- assertThat(listener.stashedOnDefaultDisplay).isFalse()
- assertThat(listener.stashedOnSecondaryDisplay).isTrue()
+ assertThat(repo.isMinimizedTask(taskId = 0)).isFalse()
+ assertThat(repo.isMinimizedTask(taskId = 1)).isFalse()
+ assertThat(repo.isMinimizedTask(taskId = 2)).isFalse()
+ }
+
+
+ @Test
+ fun updateVisibleFreeformTasks_toVisible_taskIsUnminimized() {
+ repo.minimizeTask(displayId = 10, taskId = 2)
+
+ repo.updateVisibleFreeformTasks(displayId = 10, taskId = 2, visible = true)
+
+ assertThat(repo.isMinimizedTask(taskId = 2)).isFalse()
+ }
+
+ @Test
+ fun isDesktopModeShowing_noActiveTasks_returnsFalse() {
+ assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse()
}
+ @Test
+ fun isDesktopModeShowing_noTasksVisible_returnsFalse() {
+ repo.addActiveTask(displayId = 0, taskId = 1)
+ repo.addActiveTask(displayId = 0, taskId = 2)
+
+ assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse()
+ }
+
+ @Test
+ fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() {
+ repo.addActiveTask(displayId = 0, taskId = 1)
+ repo.addActiveTask(displayId = 0, taskId = 2)
+ repo.updateVisibleFreeformTasks(displayId = 0, taskId = 1, visible = true)
+
+ assertThat(repo.isDesktopModeShowing(displayId = 0)).isTrue()
+ }
+
+ @Test
+ fun getActiveNonMinimizedTasksOrderedFrontToBack_returnsFreeformTasksInCorrectOrder() {
+ repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1)
+ repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2)
+ repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 3)
+ // The front-most task will be the one added last through addOrMoveFreeformTaskToTop
+ repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 3)
+ repo.addOrMoveFreeformTaskToTop(displayId = 0, taskId = 2)
+ repo.addOrMoveFreeformTaskToTop(displayId = 0, taskId = 1)
+
+ assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0))
+ .containsExactly(1, 2, 3).inOrder()
+ }
+
+ @Test
+ fun getActiveNonMinimizedTasksOrderedFrontToBack_minimizedTaskNotIncluded() {
+ repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1)
+ repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2)
+ repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 3)
+ // The front-most task will be the one added last through addOrMoveFreeformTaskToTop
+ repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 3)
+ repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 2)
+ repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 1)
+ repo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2)
+
+ assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(
+ displayId = DEFAULT_DISPLAY)).containsExactly(1, 3).inOrder()
+ }
+
+
class TestListener : DesktopModeTaskRepository.ActiveTasksListener {
var activeChangesOnDefaultDisplay = 0
var activeChangesOnSecondaryDisplay = 0
@@ -403,12 +527,6 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
var visibleChangesOnDefaultDisplay = 0
var visibleChangesOnSecondaryDisplay = 0
- var stashedOnDefaultDisplay = false
- var stashedOnSecondaryDisplay = false
-
- var stashedChangesOnDefaultDisplay = 0
- var stashedChangesOnSecondaryDisplay = 0
-
override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
when (displayId) {
DEFAULT_DISPLAY -> {
@@ -422,23 +540,10 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
else -> fail("Visible task listener received unexpected display id: $displayId")
}
}
-
- override fun onStashedChanged(displayId: Int, stashed: Boolean) {
- when (displayId) {
- DEFAULT_DISPLAY -> {
- stashedOnDefaultDisplay = stashed
- stashedChangesOnDefaultDisplay++
- }
- SECOND_DISPLAY -> {
- stashedOnSecondaryDisplay = stashed
- stashedChangesOnDefaultDisplay++
- }
- else -> fail("Visible task listener received unexpected display id: $displayId")
- }
- }
}
companion object {
const val SECOND_DISPLAY = 1
+ const val THIRD_DISPLAY = 345
}
}
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/DesktopModeUiEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt
new file mode 100644
index 000000000000..51b291c0b7a4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.internal.logging.InstanceId
+import com.android.internal.logging.InstanceIdSequence
+import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.Companion.DesktopUiEventEnum.DESKTOP_WINDOW_EDGE_DRAG_RESIZE
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Test class for [DesktopModeUiEventLogger]
+ *
+ * Usage: atest WMShellUnitTests:DesktopModeUiEventLoggerTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopModeUiEventLoggerTest : ShellTestCase() {
+ private lateinit var uiEventLoggerFake: UiEventLoggerFake
+ private lateinit var logger: DesktopModeUiEventLogger
+ private val instanceIdSequence = InstanceIdSequence(/* instanceIdMax */ 1 shl 20)
+
+
+ @Before
+ fun setUp() {
+ uiEventLoggerFake = UiEventLoggerFake()
+ logger = DesktopModeUiEventLogger(uiEventLoggerFake, instanceIdSequence)
+ }
+
+ @Test
+ fun log_invalidUid_eventNotLogged() {
+ logger.log(-1, PACKAGE_NAME, DESKTOP_WINDOW_EDGE_DRAG_RESIZE)
+ assertThat(uiEventLoggerFake.numLogs()).isEqualTo(0)
+ }
+
+ @Test
+ fun log_emptyPackageName_eventNotLogged() {
+ logger.log(UID, "", DESKTOP_WINDOW_EDGE_DRAG_RESIZE)
+ assertThat(uiEventLoggerFake.numLogs()).isEqualTo(0)
+ }
+
+ @Test
+ fun log_eventLogged() {
+ val event =
+ DESKTOP_WINDOW_EDGE_DRAG_RESIZE
+ logger.log(UID, PACKAGE_NAME, event)
+ assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+ assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(event.id)
+ assertThat(uiEventLoggerFake[0].instanceId).isNull()
+ assertThat(uiEventLoggerFake[0].uid).isEqualTo(UID)
+ assertThat(uiEventLoggerFake[0].packageName).isEqualTo(PACKAGE_NAME)
+ }
+
+ @Test
+ fun getNewInstanceId() {
+ val first = logger.getNewInstanceId()
+ assertThat(first).isNotEqualTo(logger.getNewInstanceId())
+ }
+
+ @Test
+ fun logWithInstanceId_invalidUid_eventNotLogged() {
+ logger.logWithInstanceId(INSTANCE_ID, -1, PACKAGE_NAME, DESKTOP_WINDOW_EDGE_DRAG_RESIZE)
+ assertThat(uiEventLoggerFake.numLogs()).isEqualTo(0)
+ }
+
+ @Test
+ fun logWithInstanceId_emptyPackageName_eventNotLogged() {
+ logger.logWithInstanceId(INSTANCE_ID, UID, "", DESKTOP_WINDOW_EDGE_DRAG_RESIZE)
+ assertThat(uiEventLoggerFake.numLogs()).isEqualTo(0)
+ }
+
+ @Test
+ fun logWithInstanceId_eventLogged() {
+ val event =
+ DESKTOP_WINDOW_EDGE_DRAG_RESIZE
+ logger.logWithInstanceId(INSTANCE_ID, UID, PACKAGE_NAME, event)
+ assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1)
+ assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(event.id)
+ assertThat(uiEventLoggerFake[0].instanceId).isEqualTo(INSTANCE_ID)
+ assertThat(uiEventLoggerFake[0].uid).isEqualTo(UID)
+ assertThat(uiEventLoggerFake[0].packageName).isEqualTo(PACKAGE_NAME)
+ }
+
+
+ companion object {
+ private val INSTANCE_ID = InstanceId.fakeInstanceId(0)
+ private const val UID = 10
+ private const val PACKAGE_NAME = "com.foo"
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
index f8ce4ee8e1ce..bd39aa6ace42 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt
@@ -56,31 +56,29 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
context, taskSurface, taskDisplayAreaOrganizer)
whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width())
whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height())
+ whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS)
}
@Test
fun testFullscreenRegionCalculation() {
val transitionHeight = context.resources.getDimensionPixelSize(
- R.dimen.desktop_mode_transition_area_height)
+ R.dimen.desktop_mode_fullscreen_from_desktop_height)
val fromFreeformWidth = mContext.resources.getDimensionPixelSize(
R.dimen.desktop_mode_fullscreen_from_desktop_width
)
- val fromFreeformHeight = mContext.resources.getDimensionPixelSize(
- R.dimen.desktop_mode_fullscreen_from_desktop_height
- )
var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout,
WINDOWING_MODE_FULLSCREEN, CAPTION_HEIGHT)
- assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, transitionHeight))
+ assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top))
testRegion = visualIndicator.calculateFullscreenRegion(displayLayout,
WINDOWING_MODE_FREEFORM, CAPTION_HEIGHT)
assertThat(testRegion.bounds).isEqualTo(Rect(
DISPLAY_BOUNDS.width() / 2 - fromFreeformWidth / 2,
-50,
DISPLAY_BOUNDS.width() / 2 + fromFreeformWidth / 2,
- fromFreeformHeight))
+ transitionHeight))
testRegion = visualIndicator.calculateFullscreenRegion(displayLayout,
WINDOWING_MODE_MULTI_WINDOW, CAPTION_HEIGHT)
- assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, transitionHeight))
+ assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 2400, 2 * STABLE_INSETS.top))
}
@Test
@@ -135,5 +133,12 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() {
private const val TRANSITION_AREA_WIDTH = 32
private const val CAPTION_HEIGHT = 50
private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600)
+ private const val NAVBAR_HEIGHT = 50
+ private val STABLE_INSETS = Rect(
+ DISPLAY_BOUNDS.left,
+ DISPLAY_BOUNDS.top + CAPTION_HEIGHT,
+ DISPLAY_BOUNDS.right,
+ DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT
+ )
}
} \ No newline at end of file
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 35c803b78674..e17f7f2f7b12 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
@@ -16,6 +16,7 @@
package com.android.wm.shell.desktopmode
+import android.app.ActivityManager.RecentTaskInfo
import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
@@ -23,56 +24,88 @@ 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
+import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+import android.content.res.Configuration.ORIENTATION_LANDSCAPE
+import android.content.res.Configuration.ORIENTATION_PORTRAIT
+import android.graphics.Point
+import android.graphics.PointF
+import android.graphics.Rect
import android.os.Binder
+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.DEFAULT_DISPLAY
+import android.view.SurfaceControl
import android.view.WindowManager
import android.view.WindowManager.TRANSIT_CHANGE
import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.DisplayAreaInfo
+import android.window.IWindowContainerToken
import android.window.RemoteTransition
import android.window.TransitionRequestInfo
+import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
+import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_LAUNCH_TASK
+import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT
+import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK
import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
+import android.window.WindowContainerTransaction.HierarchyOp.LAUNCH_KEY_TASK_ID
import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
import com.android.dx.mockito.inline.extended.ExtendedMockito.never
import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.window.flags.Flags
import com.android.wm.shell.MockToken
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.transition.TestRemoteTransition
import com.android.wm.shell.TestRunningTaskInfoBuilder
import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayLayout
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
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask
import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplitScreenTask
import com.android.wm.shell.draganddrop.DragAndDropController
+import com.android.wm.shell.recents.RecentTasksController
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.splitscreen.SplitScreenController
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.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.android.wm.shell.windowdecor.DesktopModeWindowDecoration
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 org.junit.After
import org.junit.Assume.assumeTrue
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
@@ -84,836 +117,1754 @@ import org.mockito.Mockito
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
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
+/**
+ * Test class for {@link DesktopTasksController}
+ *
+ * Usage: atest WMShellUnitTests:DesktopTasksControllerTest
+ */
@SmallTest
@RunWith(AndroidTestingRunner::class)
class DesktopTasksControllerTest : ShellTestCase() {
- @Mock lateinit var testExecutor: ShellExecutor
- @Mock lateinit var shellCommandHandler: ShellCommandHandler
- @Mock lateinit var shellController: ShellController
- @Mock lateinit var displayController: DisplayController
- @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 mToggleResizeDesktopTaskTransitionHandler:
- ToggleResizeDesktopTaskTransitionHandler
- @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
- @Mock lateinit var launchAdjacentController: LaunchAdjacentController
- @Mock lateinit var desktopModeWindowDecoration: DesktopModeWindowDecoration
- @Mock lateinit var splitScreenController: SplitScreenController
- @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
- @Mock lateinit var dragAndDropController: DragAndDropController
- @Mock lateinit var multiInstanceHelper: MultiInstanceHelper
-
- private lateinit var mockitoSession: StaticMockitoSession
- private lateinit var controller: DesktopTasksController
- private lateinit var shellInit: ShellInit
- private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository
- 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>()
-
- @Before
- fun setUp() {
- mockitoSession = mockitoSession().mockStatic(DesktopModeStatus::class.java).startMocking()
- whenever(DesktopModeStatus.isEnabled()).thenReturn(true)
-
- shellInit = Mockito.spy(ShellInit(testExecutor))
- desktopModeTaskRepository = DesktopModeTaskRepository()
-
- whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
- whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
-
- 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,
- mToggleResizeDesktopTaskTransitionHandler,
- dragToDesktopTransitionHandler,
- desktopModeTaskRepository,
- launchAdjacentController,
- recentsTransitionHandler,
- multiInstanceHelper,
- shellExecutor
- )
- }
-
- @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
- fun showDesktopApps_allAppsInvisible_bringsToFront() {
- 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
- fun showDesktopApps_appsAlreadyVisible_bringsToFront() {
- 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
- fun showDesktopApps_someAppsInvisible_reordersAll() {
- 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
- fun showDesktopApps_noActiveTasks_reorderHomeToTop() {
- 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
- fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay() {
- 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
- 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
- fun moveToDesktop_displayFullscreen_windowingModeSetToFreeform() {
- val task = setUpFullscreenTask()
- task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN
- controller.moveToDesktop(task)
- val wct = getLatestMoveToDesktopWct()
- assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- }
-
- @Test
- fun moveToDesktop_displayFreeform_windowingModeSetToUndefined() {
- val task = setUpFullscreenTask()
- task.configuration.windowConfiguration.displayWindowingMode = 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_otherFreeformTasksBroughtToFront() {
- 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
- 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 moveToFullscreen_displayFullscreen_windowingModeSetToUndefined() {
- val task = setUpFreeformTask()
- task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN
- controller.moveToFullscreen(task.taskId)
- val wct = getLatestExitDesktopWct()
- assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_UNDEFINED)
- }
-
- @Test
- fun moveToFullscreen_displayFreeform_windowingModeSetToFullscreen() {
- val task = setUpFreeformTask()
- task.configuration.windowConfiguration.displayWindowingMode = 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())
+ @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
+ 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() {
+ 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, 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, 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, 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, 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
+ fun moveToDesktop_nonRunningTask_launchesInFreeform() {
+ whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null)
+
+ val task = createTaskInfo(1)
+
+ whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task)
+
+ controller.moveToDesktop(task.taskId, transitionSource = UNKNOWN)
+ with(getLatestEnterDesktopWct()) {
+ 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
}
- }
-
- @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 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()
+ 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
+ fun moveToDesktop_bringsTasksOverLimit_dontShowBackTask() {
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ val homeTask = setUpHomeTask()
+ val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
+ val newTask = setUpFullscreenTask()
+
+ controller.moveToDesktop(newTask, transitionSource = UNKNOWN)
+
+ val wct = getLatestEnterDesktopWct()
+ 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, 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, 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 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.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 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()
+ val result = controller.handleRequest(Binder(), createTransition(task))
+ assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+ }
+
+ @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
+ @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, 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(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() {
+ 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() {
+ 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)
+ val activityInfo = ActivityInfo()
+ task.topActivityInfo = activityInfo
+ 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
}
- }
-
- @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 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_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_fullscreenTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
- whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
-
- val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY)
- markTaskHidden(stashedFreeformTask)
-
- val fullscreenTask = createFullscreenTask(DEFAULT_DISPLAY)
-
- controller.stashDesktopApps(DEFAULT_DISPLAY)
-
- val result = controller.handleRequest(Binder(), createTransition(fullscreenTask))
- assertThat(result).isNotNull()
- result!!.assertReorderSequence(stashedFreeformTask, fullscreenTask)
- assertThat(result.changes[fullscreenTask.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
-
- // Stashed state should be cleared
- assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
- }
-
- @Test
- fun handleRequest_freeformTask_freeformVisible_returnNull() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val freeformTask1 = setUpFreeformTask()
- markTaskVisible(freeformTask1)
-
- val freeformTask2 = createFreeformTask()
- assertThat(controller.handleRequest(Binder(), createTransition(freeformTask2))).isNull()
- }
-
- @Test
- fun handleRequest_freeformTask_freeformNotVisible_returnSwitchToFullscreenWCT() {
- 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?.changes?.get(freeformTask2.token.asBinder())?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FULLSCREEN)
- }
-
- @Test
- fun handleRequest_freeformTask_noOtherTasks_returnSwitchToFullscreenWCT() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val task = createFreeformTask()
- val result = controller.handleRequest(Binder(), createTransition(task))
- assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FULLSCREEN)
- }
-
- @Test
- fun handleRequest_freeformTask_freeformOnOtherDisplayOnly_returnSwitchToFullscreenWCT() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
-
- val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY)
- createFreeformTask(displayId = SECOND_DISPLAY)
-
- val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay))
- assertThat(result?.changes?.get(taskDefaultDisplay.token.asBinder())?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FULLSCREEN)
- }
-
- @Test
- fun handleRequest_freeformTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() {
- assumeTrue(ENABLE_SHELL_TRANSITIONS)
- whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
-
- val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY)
- markTaskHidden(stashedFreeformTask)
-
- val freeformTask = createFreeformTask(DEFAULT_DISPLAY)
-
- controller.stashDesktopApps(DEFAULT_DISPLAY)
-
- val result = controller.handleRequest(Binder(), createTransition(freeformTask))
- assertThat(result).isNotNull()
- result?.assertReorderSequence(stashedFreeformTask, freeformTask)
-
- // Stashed state should be cleared
- assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
- }
-
- @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 stashDesktopApps_stateUpdates() {
- whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
-
- controller.stashDesktopApps(DEFAULT_DISPLAY)
-
- assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isTrue()
- assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isFalse()
- }
-
- @Test
- fun hideStashedDesktopApps_stateUpdates() {
- whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
-
- desktopModeTaskRepository.setStashed(DEFAULT_DISPLAY, true)
- desktopModeTaskRepository.setStashed(SECOND_DISPLAY, true)
- controller.hideStashedDesktopApps(DEFAULT_DISPLAY)
-
- assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
- // Check that second display is not affected
- assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isTrue()
- }
-
- @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 enterDesktop_fullscreenTaskIsMovedToDesktop() {
- val task1 = setUpFullscreenTask()
- val task2 = setUpFullscreenTask()
- val task3 = setUpFullscreenTask()
-
- task1.isFocused = true
- task2.isFocused = false
- task3.isFocused = false
-
- controller.enterDesktop(DEFAULT_DISPLAY)
-
- val wct = getLatestMoveToDesktopWct()
- assertThat(wct.changes[task1.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FREEFORM)
- }
-
- @Test
- fun enterDesktop_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.enterDesktop(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_FULLSCREEN)
- }
-
- private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
- val task = createFreeformTask(displayId)
- whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
- desktopModeTaskRepository.addActiveTask(displayId, task.taskId)
- desktopModeTaskRepository.addOrMoveFreeformTaskToTop(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): RunningTaskInfo {
- val task = createFullscreenTask(displayId)
- whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
- runningTasks.add(task)
- return task
- }
-
- 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
- )
- }
+ } 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 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 WindowContainerTransaction.assertIndexInBounds(index: Int) {
+ assertWithMessage("WCT does not have a hierarchy operation at index $index")
+ .that(hierarchyOps.size)
+ .isGreaterThan(index)
+}
- 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 WindowContainerTransaction.assertReorderAt(
+ 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) }
+}
- 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 WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) {
+ for (i in tasks.indices) {
+ assertReorderAt(i, tasks[i])
+ }
+}
- private fun verifyWCTNotExecuted() {
- if (ENABLE_SHELL_TRANSITIONS) {
- verify(transitions, never()).startTransition(anyInt(), any(), isNull())
- } else {
- verify(shellTaskOrganizer, never()).applyTransaction(any())
- }
- }
+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())
+}
- private fun createTransition(
- task: RunningTaskInfo?,
- @WindowManager.TransitionType type: Int = TRANSIT_OPEN
- ): TransitionRequestInfo {
- return TransitionRequestInfo(type, task, null /* remoteTransition */)
- }
+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)
+}
- companion object {
- const val SECOND_DISPLAY = 2
- }
+private fun WindowContainerTransaction.assertLaunchTaskAt(
+ index: Int,
+ 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)
}
-private fun WindowContainerTransaction.assertReorderAt(index: Int, task: RunningTaskInfo) {
- assertWithMessage("WCT does not have a hierarchy operation at index $index")
- .that(hierarchyOps.size)
- .isGreaterThan(index)
- val op = hierarchyOps[index]
- assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER)
- assertThat(op.container).isEqualTo(task.token.asBinder())
+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 WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) {
- for (i in tasks.indices) {
- assertReorderAt(i, tasks[i])
+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/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
new file mode 100644
index 000000000000..77f917cc28d8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt
@@ -0,0 +1,320 @@
+/*
+ * 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.app.ActivityManager.RunningTaskInfo
+import android.os.Binder
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.AndroidTestingRunner
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.WindowManager.TRANSIT_OPEN
+import android.view.WindowManager.TRANSIT_TO_BACK
+import android.window.WindowContainerTransaction
+import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.wm.shell.ShellTaskOrganizer
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import com.android.wm.shell.shared.DesktopModeStatus
+import com.android.wm.shell.transition.TransitionInfoBuilder
+import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.util.StubTransaction
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.`when`
+import org.mockito.quality.Strictness
+
+
+/**
+ * Test class for {@link DesktopTasksLimiter}
+ *
+ * Usage: atest WMShellUnitTests:DesktopTasksLimiterTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopTasksLimiterTest : ShellTestCase() {
+
+ @JvmField
+ @Rule
+ val setFlagsRule = SetFlagsRule()
+
+ @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
+ @Mock lateinit var transitions: Transitions
+
+ private lateinit var mockitoSession: StaticMockitoSession
+ private lateinit var desktopTasksLimiter: DesktopTasksLimiter
+ private lateinit var desktopTaskRepo: DesktopModeTaskRepository
+
+ @Before
+ fun setUp() {
+ mockitoSession = ExtendedMockito.mockitoSession().strictness(Strictness.LENIENT)
+ .spyStatic(DesktopModeStatus::class.java).startMocking()
+ doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) }
+
+ desktopTaskRepo = DesktopModeTaskRepository()
+
+ desktopTasksLimiter = DesktopTasksLimiter(
+ transitions, desktopTaskRepo, shellTaskOrganizer)
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
+ }
+
+ // Currently, the task limit can be overridden through an adb flag. This test ensures the limit
+ // hasn't been overridden.
+ @Test
+ fun getMaxTaskLimit_isSameAsConstant() {
+ assertThat(desktopTasksLimiter.getMaxTaskLimit()).isEqualTo(
+ DesktopModeStatus.DEFAULT_MAX_TASK_LIMIT)
+ }
+
+ @Test
+ fun addPendingMinimizeTransition_taskIsNotMinimized() {
+ val task = setUpFreeformTask()
+ markTaskHidden(task)
+
+ desktopTasksLimiter.addPendingMinimizeChange(Binder(), displayId = 1, taskId = task.taskId)
+
+ assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
+ }
+
+ @Test
+ fun onTransitionReady_noPendingTransition_taskIsNotMinimized() {
+ val task = setUpFreeformTask()
+ markTaskHidden(task)
+
+ desktopTasksLimiter.getTransitionObserver().onTransitionReady(
+ Binder() /* transition */,
+ TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
+ StubTransaction() /* startTransaction */,
+ StubTransaction() /* finishTransaction */)
+
+ assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
+ }
+
+ @Test
+ fun onTransitionReady_differentPendingTransition_taskIsNotMinimized() {
+ val pendingTransition = Binder()
+ val taskTransition = Binder()
+ val task = setUpFreeformTask()
+ markTaskHidden(task)
+ desktopTasksLimiter.addPendingMinimizeChange(
+ pendingTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+
+ desktopTasksLimiter.getTransitionObserver().onTransitionReady(
+ taskTransition /* transition */,
+ TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
+ StubTransaction() /* startTransaction */,
+ StubTransaction() /* finishTransaction */)
+
+ assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
+ }
+
+ @Test
+ fun onTransitionReady_pendingTransition_noTaskChange_taskVisible_taskIsNotMinimized() {
+ val transition = Binder()
+ val task = setUpFreeformTask()
+ markTaskVisible(task)
+ desktopTasksLimiter.addPendingMinimizeChange(
+ transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+
+ desktopTasksLimiter.getTransitionObserver().onTransitionReady(
+ transition,
+ TransitionInfoBuilder(TRANSIT_OPEN).build(),
+ StubTransaction() /* startTransaction */,
+ StubTransaction() /* finishTransaction */)
+
+ assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse()
+ }
+
+ @Test
+ fun onTransitionReady_pendingTransition_noTaskChange_taskInvisible_taskIsMinimized() {
+ val transition = Binder()
+ val task = setUpFreeformTask()
+ markTaskHidden(task)
+ desktopTasksLimiter.addPendingMinimizeChange(
+ transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+
+ desktopTasksLimiter.getTransitionObserver().onTransitionReady(
+ transition,
+ TransitionInfoBuilder(TRANSIT_OPEN).build(),
+ StubTransaction() /* startTransaction */,
+ StubTransaction() /* finishTransaction */)
+
+ assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
+ }
+
+ @Test
+ fun onTransitionReady_pendingTransition_changeTaskToBack_taskIsMinimized() {
+ val transition = Binder()
+ val task = setUpFreeformTask()
+ desktopTasksLimiter.addPendingMinimizeChange(
+ transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+
+ desktopTasksLimiter.getTransitionObserver().onTransitionReady(
+ transition,
+ TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
+ StubTransaction() /* startTransaction */,
+ StubTransaction() /* finishTransaction */)
+
+ assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
+ }
+
+ @Test
+ fun onTransitionReady_transitionMergedFromPending_taskIsMinimized() {
+ val mergedTransition = Binder()
+ val newTransition = Binder()
+ val task = setUpFreeformTask()
+ desktopTasksLimiter.addPendingMinimizeChange(
+ mergedTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId)
+ desktopTasksLimiter.getTransitionObserver().onTransitionMerged(
+ mergedTransition, newTransition)
+
+ desktopTasksLimiter.getTransitionObserver().onTransitionReady(
+ newTransition,
+ TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(),
+ StubTransaction() /* startTransaction */,
+ StubTransaction() /* finishTransaction */)
+
+ assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue()
+ }
+
+ @Test
+ fun addAndGetMinimizeTaskChangesIfNeeded_tasksWithinLimit_noTaskMinimized() {
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ (1..<taskLimit).forEach { _ -> setUpFreeformTask() }
+
+ val wct = WindowContainerTransaction()
+ val minimizedTaskId =
+ desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded(
+ displayId = DEFAULT_DISPLAY,
+ wct = wct,
+ newFrontTaskInfo = setUpFreeformTask())
+
+ assertThat(minimizedTaskId).isNull()
+ assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added
+ }
+
+ @Test
+ fun addAndGetMinimizeTaskChangesIfNeeded_tasksAboveLimit_backTaskMinimized() {
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ // The following list will be ordered bottom -> top, as the last task is moved to top last.
+ val tasks = (1..taskLimit).map { setUpFreeformTask() }
+
+ val wct = WindowContainerTransaction()
+ val minimizedTaskId =
+ desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded(
+ displayId = DEFAULT_DISPLAY,
+ wct = wct,
+ newFrontTaskInfo = setUpFreeformTask())
+
+ assertThat(minimizedTaskId).isEqualTo(tasks.first())
+ assertThat(wct.hierarchyOps.size).isEqualTo(1)
+ assertThat(wct.hierarchyOps[0].type).isEqualTo(HIERARCHY_OP_TYPE_REORDER)
+ assertThat(wct.hierarchyOps[0].toTop).isFalse() // Reorder to bottom
+ }
+
+ @Test
+ fun addAndGetMinimizeTaskChangesIfNeeded_nonMinimizedTasksWithinLimit_noTaskMinimized() {
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ val tasks = (1..taskLimit).map { setUpFreeformTask() }
+ desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = tasks[0].taskId)
+
+ val wct = WindowContainerTransaction()
+ val minimizedTaskId =
+ desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded(
+ displayId = 0,
+ wct = wct,
+ newFrontTaskInfo = setUpFreeformTask())
+
+ assertThat(minimizedTaskId).isNull()
+ assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added
+ }
+
+ @Test
+ fun getTaskToMinimizeIfNeeded_tasksWithinLimit_returnsNull() {
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ val tasks = (1..taskLimit).map { setUpFreeformTask() }
+
+ val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded(
+ visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId })
+
+ assertThat(minimizedTask).isNull()
+ }
+
+ @Test
+ fun getTaskToMinimizeIfNeeded_tasksAboveLimit_returnsBackTask() {
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ val tasks = (1..taskLimit + 1).map { setUpFreeformTask() }
+
+ val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded(
+ visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId })
+
+ // first == front, last == back
+ assertThat(minimizedTask).isEqualTo(tasks.last())
+ }
+
+ @Test
+ fun getTaskToMinimizeIfNeeded_withNewTask_tasksAboveLimit_returnsBackTask() {
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ val tasks = (1..taskLimit).map { setUpFreeformTask() }
+
+ val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded(
+ visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId },
+ newTaskIdInFront = setUpFreeformTask().taskId)
+
+ // first == front, last == back
+ assertThat(minimizedTask).isEqualTo(tasks.last())
+ }
+
+ private fun setUpFreeformTask(
+ displayId: Int = DEFAULT_DISPLAY,
+ ): RunningTaskInfo {
+ val task = createFreeformTask(displayId)
+ `when`(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+ desktopTaskRepo.addActiveTask(displayId, task.taskId)
+ desktopTaskRepo.addOrMoveFreeformTaskToTop(displayId, task.taskId)
+ return task
+ }
+
+ private fun markTaskVisible(task: RunningTaskInfo) {
+ desktopTaskRepo.updateVisibleFreeformTasks(
+ task.displayId,
+ task.taskId,
+ visible = true
+ )
+ }
+
+ private fun markTaskHidden(task: RunningTaskInfo) {
+ desktopTaskRepo.updateVisibleFreeformTasks(
+ task.displayId,
+ task.taskId,
+ visible = false
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
index 2f6f3207137d..52da7fb811d0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt
@@ -22,6 +22,7 @@ import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
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.Rect
import android.view.Display.DEFAULT_DISPLAY
import com.android.wm.shell.MockToken
import com.android.wm.shell.TestRunningTaskInfoBuilder
@@ -31,13 +32,17 @@ class DesktopTestHelpers {
/** Create a task that has windowing mode set to [WINDOWING_MODE_FREEFORM] */
@JvmStatic
@JvmOverloads
- fun createFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
+ fun createFreeformTask(
+ displayId: Int = DEFAULT_DISPLAY,
+ bounds: Rect? = null
+ ): RunningTaskInfo {
return TestRunningTaskInfoBuilder()
.setDisplayId(displayId)
.setToken(MockToken().token())
.setActivityType(ACTIVITY_TYPE_STANDARD)
.setWindowingMode(WINDOWING_MODE_FREEFORM)
.setLastActiveTime(100)
+ .apply { bounds?.let { setBounds(it) }}
.build()
}
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 98e90d60b3b6..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).endAnimator()
+ 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/draganddrop/DragAndDropPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
index 5dd9d8a859d6..6e72e8df8d62 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropPolicyTest.java
@@ -66,6 +66,7 @@ import android.graphics.Insets;
import android.os.RemoteException;
import android.view.DisplayInfo;
import android.view.DragEvent;
+import android.view.View;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -115,6 +116,7 @@ public class DragAndDropPolicyTest extends ShellTestCase {
private DragAndDropPolicy mPolicy;
private ClipData mActivityClipData;
+ private PendingIntent mLaunchableIntentPendingIntent;
private ClipData mLaunchableIntentClipData;
private ClipData mNonResizeableActivityClipData;
private ClipData mTaskClipData;
@@ -151,7 +153,10 @@ public class DragAndDropPolicyTest extends ShellTestCase {
mPolicy = spy(new DragAndDropPolicy(mContext, mSplitScreenStarter, mSplitScreenStarter));
mActivityClipData = createAppClipData(MIMETYPE_APPLICATION_ACTIVITY);
- mLaunchableIntentClipData = createIntentClipData();
+ mLaunchableIntentPendingIntent = mock(PendingIntent.class);
+ when(mLaunchableIntentPendingIntent.getCreatorUserHandle())
+ .thenReturn(android.os.Process.myUserHandle());
+ mLaunchableIntentClipData = createIntentClipData(mLaunchableIntentPendingIntent);
mNonResizeableActivityClipData = createAppClipData(MIMETYPE_APPLICATION_ACTIVITY);
setClipDataResizeable(mNonResizeableActivityClipData, false);
mTaskClipData = createAppClipData(MIMETYPE_APPLICATION_TASK);
@@ -202,16 +207,13 @@ public class DragAndDropPolicyTest extends ShellTestCase {
/**
* Creates an intent-based clip data that is by default resizeable.
*/
- private ClipData createIntentClipData() {
+ private ClipData createIntentClipData(PendingIntent intent) {
ClipDescription clipDescription = new ClipDescription("Intent",
new String[] { MIMETYPE_TEXT_INTENT });
- PendingIntent intent = mock(PendingIntent.class);
- when(intent.getCreatorUserHandle()).thenReturn(android.os.Process.myUserHandle());
ClipData.Item item = new ClipData.Item.Builder()
.setIntentSender(intent.getIntentSender())
.build();
ClipData data = new ClipData(clipDescription, item);
- when(DragUtils.getLaunchIntent((ClipData) any())).thenReturn(intent);
return data;
}
@@ -259,16 +261,22 @@ public class DragAndDropPolicyTest extends ShellTestCase {
@Test
public void testDragIntentOverFullscreenHome_expectOnlyFullscreenTarget() {
+ when(DragUtils.getLaunchIntent((ClipData) any(), anyInt())).thenReturn(
+ mLaunchableIntentPendingIntent);
dragOverFullscreenHome_expectOnlyFullscreenTarget(mLaunchableIntentClipData);
}
@Test
public void testDragIntentOverFullscreenApp_expectSplitScreenTargets() {
+ when(DragUtils.getLaunchIntent((ClipData) any(), anyInt())).thenReturn(
+ mLaunchableIntentPendingIntent);
dragOverFullscreenApp_expectSplitScreenTargets(mLaunchableIntentClipData);
}
@Test
public void testDragIntentOverFullscreenAppPhone_expectVerticalSplitScreenTargets() {
+ when(DragUtils.getLaunchIntent((ClipData) any(), anyInt())).thenReturn(
+ mLaunchableIntentPendingIntent);
dragOverFullscreenAppPhone_expectVerticalSplitScreenTargets(mLaunchableIntentClipData);
}
@@ -276,7 +284,7 @@ public class DragAndDropPolicyTest extends ShellTestCase {
doReturn(true).when(mSplitScreenStarter).isLeftRightSplit();
setRunningTask(mHomeTask);
DragSession dragSession = new DragSession(mActivityTaskManager,
- mLandscapeDisplayLayout, data);
+ mLandscapeDisplayLayout, data, 0 /* dragFlags */);
dragSession.update();
mPolicy.start(dragSession, mLoggerSessionId);
ArrayList<Target> targets = assertExactTargetTypes(
@@ -291,7 +299,7 @@ public class DragAndDropPolicyTest extends ShellTestCase {
doReturn(true).when(mSplitScreenStarter).isLeftRightSplit();
setRunningTask(mFullscreenAppTask);
DragSession dragSession = new DragSession(mActivityTaskManager,
- mLandscapeDisplayLayout, data);
+ mLandscapeDisplayLayout, data, 0 /* dragFlags */);
dragSession.update();
mPolicy.start(dragSession, mLoggerSessionId);
ArrayList<Target> targets = assertExactTargetTypes(
@@ -311,7 +319,7 @@ public class DragAndDropPolicyTest extends ShellTestCase {
doReturn(false).when(mSplitScreenStarter).isLeftRightSplit();
setRunningTask(mFullscreenAppTask);
DragSession dragSession = new DragSession(mActivityTaskManager,
- mPortraitDisplayLayout, data);
+ mPortraitDisplayLayout, data, 0 /* dragFlags */);
dragSession.update();
mPolicy.start(dragSession, mLoggerSessionId);
ArrayList<Target> targets = assertExactTargetTypes(
@@ -331,7 +339,7 @@ public class DragAndDropPolicyTest extends ShellTestCase {
public void testTargetHitRects() {
setRunningTask(mFullscreenAppTask);
DragSession dragSession = new DragSession(mActivityTaskManager,
- mLandscapeDisplayLayout, mActivityClipData);
+ mLandscapeDisplayLayout, mActivityClipData, 0 /* dragFlags */);
dragSession.update();
mPolicy.start(dragSession, mLoggerSessionId);
ArrayList<Target> targets = mPolicy.getTargets(mInsets);
@@ -345,6 +353,11 @@ public class DragAndDropPolicyTest extends ShellTestCase {
}
}
+ @Test
+ public void testDisallowLaunchIntentWithoutDelegationFlag() {
+ assertTrue(DragUtils.getLaunchIntent(mLaunchableIntentClipData, 0) == null);
+ }
+
private Target filterTargetByType(ArrayList<Target> targets, int type) {
for (Target t : targets) {
if (type == t.type) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt
index e731b06c0947..d410151b4602 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/GlobalDragListenerTest.kt
@@ -74,7 +74,7 @@ class UnhandledDragControllerTest : ShellTestCase() {
@Test
fun onUnhandledDrop_noListener_expectNotifyUnhandled() {
// Simulate an unhandled drop
- val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null,
+ val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null,
null, null, false)
val wmCallback = mock<IUnhandledDragCallback>()
mController.onUnhandledDrop(dropEvent, wmCallback)
@@ -98,7 +98,7 @@ class UnhandledDragControllerTest : ShellTestCase() {
// Simulate an unhandled drop
val dragSurface = mock<SurfaceControl>()
- val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null,
+ val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, 0, null, null, null,
dragSurface, null, false)
val wmCallback = mock<IUnhandledDragCallback>()
mController.onUnhandledDrop(dropEvent, wmCallback)
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
index 71eea4bb59b1..3f3cafcf6375 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java
@@ -19,11 +19,12 @@ package com.android.wm.shell.freeform;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import android.app.ActivityManager;
@@ -34,8 +35,8 @@ import com.android.dx.mockito.inline.extended.StaticMockitoSession;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
-import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.shared.DesktopModeStatus;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
@@ -72,8 +73,10 @@ public final class FreeformTaskListenerTests extends ShellTestCase {
public void setup() {
mMockitoSession = mockitoSession().initMocks(this)
.strictness(Strictness.LENIENT).mockStatic(DesktopModeStatus.class).startMocking();
- when(DesktopModeStatus.isEnabled()).thenReturn(true);
+ doReturn(true).when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
mFreeformTaskListener = new FreeformTaskListener(
+ mContext,
mShellInit,
mTaskOrganizer,
Optional.of(mDesktopModeTaskRepository),
@@ -88,7 +91,8 @@ public final class FreeformTaskListenerTests extends ShellTestCase {
mFreeformTaskListener.onFocusTaskChanged(task);
- verify(mDesktopModeTaskRepository).addOrMoveFreeformTaskToTop(task.taskId);
+ verify(mDesktopModeTaskRepository)
+ .addOrMoveFreeformTaskToTop(task.displayId, task.taskId);
}
@Test
@@ -100,7 +104,7 @@ public final class FreeformTaskListenerTests extends ShellTestCase {
mFreeformTaskListener.onFocusTaskChanged(fullscreenTask);
verify(mDesktopModeTaskRepository, never())
- .addOrMoveFreeformTaskToTop(fullscreenTask.taskId);
+ .addOrMoveFreeformTaskToTop(fullscreenTask.displayId, fullscreenTask.taskId);
}
@After
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java
index 3d5cd6939d1b..85f1da5322ea 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java
@@ -16,33 +16,26 @@
package com.android.wm.shell.pip.phone;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.Resources;
-import android.os.SystemProperties;
import android.testing.AndroidTestingRunner;
import android.util.Size;
import android.view.DisplayInfo;
-import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.wm.shell.R;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.common.pip.SizeSpecSource;
-import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.mockito.exceptions.misusing.InvalidUseOfMatchersException;
import java.util.HashMap;
import java.util.Map;
@@ -63,15 +56,24 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase {
private static final float DEFAULT_PERCENT = 0.6f;
/** Minimum sizing percentage */
private static final float MIN_PERCENT = 0.5f;
+ /** Threshold to determine if a Display is square-ish. */
+ private static final float SQUARE_DISPLAY_THRESHOLD = 0.95f;
+ /** Default sizing percentage for square-ish Display. */
+ private static final float SQUARE_DISPLAY_DEFAULT_PERCENT = 0.5f;
+ /** Minimum sizing percentage for square-ish Display. */
+ private static final float SQUARE_DISPLAY_MIN_PERCENT = 0.4f;
/** Aspect ratio that the new PIP size spec logic optimizes for. */
private static final float OPTIMIZED_ASPECT_RATIO = 9f / 16;
- /** A map of aspect ratios to be tested to expected sizes */
- private static Map<Float, Size> sExpectedMaxSizes;
- private static Map<Float, Size> sExpectedDefaultSizes;
- private static Map<Float, Size> sExpectedMinSizes;
- /** A static mockito session object to mock {@link SystemProperties} */
- private static StaticMockitoSession sStaticMockitoSession;
+ /** Maps of aspect ratios to be tested to expected sizes on non-square Display. */
+ private static Map<Float, Size> sNonSquareDisplayExpectedMaxSizes;
+ private static Map<Float, Size> sNonSquareDisplayExpectedDefaultSizes;
+ private static Map<Float, Size> sNonSquareDisplayExpectedMinSizes;
+
+ /** Maps of aspect ratios to be tested to expected sizes on square Display. */
+ private static Map<Float, Size> sSquareDisplayExpectedMaxSizes;
+ private static Map<Float, Size> sSquareDisplayExpectedDefaultSizes;
+ private static Map<Float, Size> sSquareDisplayExpectedMinSizes;
@Mock private Context mContext;
@Mock private Resources mResources;
@@ -80,49 +82,55 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase {
private SizeSpecSource mSizeSpecSource;
/**
- * Sets up static Mockito session for SystemProperties and mocks necessary static methods.
+ * Initializes the map with the aspect ratios to be tested and corresponding expected max sizes.
+ * This is to initialize the expectations on non-square Display only.
*/
- private static void setUpStaticSystemPropertiesSession() {
- sStaticMockitoSession = mockitoSession()
- .mockStatic(SystemProperties.class).startMocking();
- when(SystemProperties.get(anyString(), anyString())).thenAnswer(invocation -> {
- String property = invocation.getArgument(0);
- if (property.equals("com.android.wm.shell.pip.phone.def_percentage")) {
- return Float.toString(DEFAULT_PERCENT);
- } else if (property.equals("com.android.wm.shell.pip.phone.min_percentage")) {
- return Float.toString(MIN_PERCENT);
- }
-
- // throw an exception if illegal arguments are used for these tests
- throw new InvalidUseOfMatchersException(
- String.format("Argument %s does not match", property)
- );
- });
+ private static void initNonSquareDisplayExpectedSizes() {
+ sNonSquareDisplayExpectedMaxSizes = new HashMap<>();
+ sNonSquareDisplayExpectedDefaultSizes = new HashMap<>();
+ sNonSquareDisplayExpectedMinSizes = new HashMap<>();
+
+ sNonSquareDisplayExpectedMaxSizes.put(16f / 9, new Size(1000, 563));
+ sNonSquareDisplayExpectedDefaultSizes.put(16f / 9, new Size(600, 338));
+ sNonSquareDisplayExpectedMinSizes.put(16f / 9, new Size(501, 282));
+
+ sNonSquareDisplayExpectedMaxSizes.put(4f / 3, new Size(893, 670));
+ sNonSquareDisplayExpectedDefaultSizes.put(4f / 3, new Size(536, 402));
+ sNonSquareDisplayExpectedMinSizes.put(4f / 3, new Size(447, 335));
+
+ sNonSquareDisplayExpectedMaxSizes.put(3f / 4, new Size(670, 893));
+ sNonSquareDisplayExpectedDefaultSizes.put(3f / 4, new Size(402, 536));
+ sNonSquareDisplayExpectedMinSizes.put(3f / 4, new Size(335, 447));
+
+ sNonSquareDisplayExpectedMaxSizes.put(9f / 16, new Size(563, 1001));
+ sNonSquareDisplayExpectedDefaultSizes.put(9f / 16, new Size(338, 601));
+ sNonSquareDisplayExpectedMinSizes.put(9f / 16, new Size(282, 501));
}
/**
* Initializes the map with the aspect ratios to be tested and corresponding expected max sizes.
+ * This is to initialize the expectations on square Display only.
*/
- private static void initExpectedSizes() {
- sExpectedMaxSizes = new HashMap<>();
- sExpectedDefaultSizes = new HashMap<>();
- sExpectedMinSizes = new HashMap<>();
-
- sExpectedMaxSizes.put(16f / 9, new Size(1000, 563));
- sExpectedDefaultSizes.put(16f / 9, new Size(600, 338));
- sExpectedMinSizes.put(16f / 9, new Size(501, 282));
-
- sExpectedMaxSizes.put(4f / 3, new Size(893, 670));
- sExpectedDefaultSizes.put(4f / 3, new Size(536, 402));
- sExpectedMinSizes.put(4f / 3, new Size(447, 335));
-
- sExpectedMaxSizes.put(3f / 4, new Size(670, 893));
- sExpectedDefaultSizes.put(3f / 4, new Size(402, 536));
- sExpectedMinSizes.put(3f / 4, new Size(335, 447));
-
- sExpectedMaxSizes.put(9f / 16, new Size(563, 1001));
- sExpectedDefaultSizes.put(9f / 16, new Size(338, 601));
- sExpectedMinSizes.put(9f / 16, new Size(282, 501));
+ private static void initSquareDisplayExpectedSizes() {
+ sSquareDisplayExpectedMaxSizes = new HashMap<>();
+ sSquareDisplayExpectedDefaultSizes = new HashMap<>();
+ sSquareDisplayExpectedMinSizes = new HashMap<>();
+
+ sSquareDisplayExpectedMaxSizes.put(16f / 9, new Size(1000, 563));
+ sSquareDisplayExpectedDefaultSizes.put(16f / 9, new Size(500, 281));
+ sSquareDisplayExpectedMinSizes.put(16f / 9, new Size(400, 225));
+
+ sSquareDisplayExpectedMaxSizes.put(4f / 3, new Size(893, 670));
+ sSquareDisplayExpectedDefaultSizes.put(4f / 3, new Size(447, 335));
+ sSquareDisplayExpectedMinSizes.put(4f / 3, new Size(357, 268));
+
+ sSquareDisplayExpectedMaxSizes.put(3f / 4, new Size(670, 893));
+ sSquareDisplayExpectedDefaultSizes.put(3f / 4, new Size(335, 447));
+ sSquareDisplayExpectedMinSizes.put(3f / 4, new Size(268, 357));
+
+ sSquareDisplayExpectedMaxSizes.put(9f / 16, new Size(563, 1001));
+ sSquareDisplayExpectedDefaultSizes.put(9f / 16, new Size(282, 501));
+ sSquareDisplayExpectedMinSizes.put(9f / 16, new Size(225, 400));
}
private void forEveryTestCaseCheck(Map<Float, Size> expectedSizes,
@@ -137,20 +145,38 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase {
@Before
public void setUp() {
- initExpectedSizes();
-
- when(mResources.getDimensionPixelSize(anyInt())).thenReturn(DEFAULT_MIN_EDGE_SIZE);
- when(mResources.getFloat(anyInt())).thenReturn(OPTIMIZED_ASPECT_RATIO);
- when(mResources.getString(anyInt())).thenReturn("0x0");
+ initNonSquareDisplayExpectedSizes();
+ initSquareDisplayExpectedSizes();
+
+ when(mResources.getFloat(R.dimen.config_pipSystemPreferredDefaultSizePercent))
+ .thenReturn(DEFAULT_PERCENT);
+ when(mResources.getFloat(R.dimen.config_pipSystemPreferredMinimumSizePercent))
+ .thenReturn(MIN_PERCENT);
+ when(mResources.getDimensionPixelSize(R.dimen.default_minimal_size_pip_resizable_task))
+ .thenReturn(DEFAULT_MIN_EDGE_SIZE);
+ when(mResources.getFloat(R.dimen.config_pipLargeScreenOptimizedAspectRatio))
+ .thenReturn(OPTIMIZED_ASPECT_RATIO);
+ when(mResources.getString(R.string.config_defaultPictureInPictureScreenEdgeInsets))
+ .thenReturn("0x0");
when(mResources.getDisplayMetrics())
.thenReturn(getContext().getResources().getDisplayMetrics());
+ when(mResources.getFloat(R.dimen.config_pipSquareDisplayThresholdForSystemPreferredSize))
+ .thenReturn(SQUARE_DISPLAY_THRESHOLD);
+ when(mResources.getFloat(
+ R.dimen.config_pipSystemPreferredDefaultSizePercentForSquareDisplay))
+ .thenReturn(SQUARE_DISPLAY_DEFAULT_PERCENT);
+ when(mResources.getFloat(
+ R.dimen.config_pipSystemPreferredMinimumSizePercentForSquareDisplay))
+ .thenReturn(SQUARE_DISPLAY_MIN_PERCENT);
// set up the mock context for spec handler specifically
when(mContext.getResources()).thenReturn(mResources);
+ }
+ private void setupSizeSpecWithDisplayDimension(int width, int height) {
DisplayInfo displayInfo = new DisplayInfo();
- displayInfo.logicalWidth = DISPLAY_EDGE_SIZE;
- displayInfo.logicalHeight = DISPLAY_EDGE_SIZE;
+ displayInfo.logicalWidth = width;
+ displayInfo.logicalHeight = height;
// use the parent context (not the mocked one) to obtain the display layout
// this is done to avoid unnecessary mocking while allowing for custom display dimensions
@@ -159,38 +185,57 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase {
mPipDisplayLayoutState = new PipDisplayLayoutState(mContext);
mPipDisplayLayoutState.setDisplayLayout(displayLayout);
- setUpStaticSystemPropertiesSession();
mSizeSpecSource = new PhoneSizeSpecSource(mContext, mPipDisplayLayoutState);
// no overridden min edge size by default
mSizeSpecSource.setOverrideMinSize(null);
}
- @After
- public void cleanUp() {
- sStaticMockitoSession.finishMocking();
+ @Test
+ public void testGetMaxSize_nonSquareDisplay() {
+ setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE);
+ forEveryTestCaseCheck(sNonSquareDisplayExpectedMaxSizes,
+ (aspectRatio) -> mSizeSpecSource.getMaxSize(aspectRatio));
+ }
+
+ @Test
+ public void testGetDefaultSize_nonSquareDisplay() {
+ setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE);
+ forEveryTestCaseCheck(sNonSquareDisplayExpectedDefaultSizes,
+ (aspectRatio) -> mSizeSpecSource.getDefaultSize(aspectRatio));
+ }
+
+ @Test
+ public void testGetMinSize_nonSquareDisplay() {
+ setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE);
+ forEveryTestCaseCheck(sNonSquareDisplayExpectedMinSizes,
+ (aspectRatio) -> mSizeSpecSource.getMinSize(aspectRatio));
}
@Test
- public void testGetMaxSize() {
- forEveryTestCaseCheck(sExpectedMaxSizes,
+ public void testGetMaxSize_squareDisplay() {
+ setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE, DISPLAY_EDGE_SIZE);
+ forEveryTestCaseCheck(sSquareDisplayExpectedMaxSizes,
(aspectRatio) -> mSizeSpecSource.getMaxSize(aspectRatio));
}
@Test
- public void testGetDefaultSize() {
- forEveryTestCaseCheck(sExpectedDefaultSizes,
+ public void testGetDefaultSize_squareDisplay() {
+ setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE, DISPLAY_EDGE_SIZE);
+ forEveryTestCaseCheck(sSquareDisplayExpectedDefaultSizes,
(aspectRatio) -> mSizeSpecSource.getDefaultSize(aspectRatio));
}
@Test
- public void testGetMinSize() {
- forEveryTestCaseCheck(sExpectedMinSizes,
+ public void testGetMinSize_squareDisplay() {
+ setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE, DISPLAY_EDGE_SIZE);
+ forEveryTestCaseCheck(sSquareDisplayExpectedMinSizes,
(aspectRatio) -> mSizeSpecSource.getMinSize(aspectRatio));
}
@Test
public void testGetSizeForAspectRatio_noOverrideMinSize() {
+ setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE);
// an initial size with 16:9 aspect ratio
Size initSize = new Size(600, 337);
@@ -202,6 +247,7 @@ public class PhoneSizeSpecSourceTest extends ShellTestCase {
@Test
public void testGetSizeForAspectRatio_withOverrideMinSize() {
+ setupSizeSpecWithDisplayDimension(DISPLAY_EDGE_SIZE * 2, DISPLAY_EDGE_SIZE);
// an initial size with a 1:1 aspect ratio
Size initSize = new Size(OVERRIDE_MIN_EDGE_SIZE, OVERRIDE_MIN_EDGE_SIZE);
mSizeSpecSource.setOverrideMinSize(initSize);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index 3384509f1da9..d38fc6cb6418 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -129,7 +129,7 @@ public class PipControllerTest extends ShellTestCase {
}).when(mMockExecutor).execute(any());
mShellInit = spy(new ShellInit(mMockExecutor));
mShellController = spy(new ShellController(mContext, mShellInit, mMockShellCommandHandler,
- mMockExecutor));
+ mMockDisplayInsetsController, mMockExecutor));
mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler,
mShellController, mMockDisplayController, mMockPipAnimationController,
mMockPipAppOpsListener, mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java
new file mode 100644
index 000000000000..f3f3c37b645d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.testing.AndroidTestingRunner;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
+import com.android.wm.shell.pip2.phone.PipTransitionState;
+
+import junit.framework.Assert;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/**
+ * Unit test against {@link PhoneSizeSpecSource}.
+ *
+ * This test mocks the PiP2 flag to be true.
+ */
+@RunWith(AndroidTestingRunner.class)
+public class PipTransitionStateTest extends ShellTestCase {
+ private static final String EXTRA_ENTRY_KEY = "extra_entry_key";
+ private PipTransitionState mPipTransitionState;
+ private PipTransitionState.PipTransitionStateChangedListener mStateChangedListener;
+ private Parcelable mEmptyParcelable;
+
+ @Mock
+ private Handler mMainHandler;
+
+ @Before
+ public void setUp() {
+ mPipTransitionState = new PipTransitionState(mMainHandler);
+ mPipTransitionState.setState(PipTransitionState.UNDEFINED);
+ mEmptyParcelable = new Bundle();
+ }
+
+ @Test
+ public void testEnteredState_withoutExtra() {
+ mStateChangedListener = (oldState, newState, extra) -> {
+ Assert.assertEquals(PipTransitionState.ENTERED_PIP, newState);
+ Assert.assertNull(extra);
+ };
+ mPipTransitionState.addPipTransitionStateChangedListener(mStateChangedListener);
+ mPipTransitionState.setState(PipTransitionState.ENTERED_PIP);
+ mPipTransitionState.removePipTransitionStateChangedListener(mStateChangedListener);
+ }
+
+ @Test
+ public void testEnteredState_withExtra() {
+ mStateChangedListener = (oldState, newState, extra) -> {
+ Assert.assertEquals(PipTransitionState.ENTERED_PIP, newState);
+ Assert.assertNotNull(extra);
+ Assert.assertEquals(mEmptyParcelable, extra.getParcelable(EXTRA_ENTRY_KEY));
+ };
+ Bundle extra = new Bundle();
+ extra.putParcelable(EXTRA_ENTRY_KEY, mEmptyParcelable);
+
+ mPipTransitionState.addPipTransitionStateChangedListener(mStateChangedListener);
+ mPipTransitionState.setState(PipTransitionState.ENTERED_PIP, extra);
+ mPipTransitionState.removePipTransitionStateChangedListener(mStateChangedListener);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testEnteringState_withoutExtra() {
+ mPipTransitionState.setState(PipTransitionState.ENTERING_PIP);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSwipingToPipState_withoutExtra() {
+ mPipTransitionState.setState(PipTransitionState.SWIPING_TO_PIP);
+ }
+
+ @Test
+ public void testCustomState_withExtra_thenEntered_withoutExtra() {
+ final int customState = mPipTransitionState.getCustomState();
+ mStateChangedListener = (oldState, newState, extra) -> {
+ if (newState == customState) {
+ Assert.assertNotNull(extra);
+ Assert.assertEquals(mEmptyParcelable, extra.getParcelable(EXTRA_ENTRY_KEY));
+ return;
+ } else if (newState == PipTransitionState.ENTERED_PIP) {
+ Assert.assertNull(extra);
+ return;
+ }
+ Assert.fail("Neither custom not ENTERED_PIP state is received.");
+ };
+ Bundle extra = new Bundle();
+ extra.putParcelable(EXTRA_ENTRY_KEY, mEmptyParcelable);
+
+ mPipTransitionState.addPipTransitionStateChangedListener(mStateChangedListener);
+ mPipTransitionState.setState(customState, extra);
+ mPipTransitionState.setState(PipTransitionState.ENTERED_PIP);
+ mPipTransitionState.removePipTransitionStateChangedListener(mStateChangedListener);
+ }
+}
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 10e9e11e9004..e291c0e1a151 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
@@ -35,6 +35,7 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
@@ -45,22 +46,29 @@ import static java.lang.Integer.MAX_VALUE;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
+import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Rect;
import android.os.Bundle;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.view.SurfaceControl;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.window.flags.Flags;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.desktopmode.DesktopModeStatus;
import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
+import com.android.wm.shell.shared.DesktopModeStatus;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -68,10 +76,13 @@ import com.android.wm.shell.sysui.ShellSharedConstants;
import com.android.wm.shell.util.GroupedRecentTaskInfo;
import com.android.wm.shell.util.SplitBounds;
+import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.quality.Strictness;
import java.util.ArrayList;
import java.util.Arrays;
@@ -80,7 +91,9 @@ import java.util.Optional;
import java.util.function.Consumer;
/**
- * Tests for {@link RecentTasksController}.
+ * Tests for {@link RecentTasksController}
+ *
+ * Usage: atest WMShellUnitTests:RecentTasksControllerTest
*/
@RunWith(AndroidJUnit4.class)
@SmallTest
@@ -96,6 +109,15 @@ public class RecentTasksControllerTest extends ShellTestCase {
private DesktopModeTaskRepository mDesktopModeTaskRepository;
@Mock
private ActivityTaskManager mActivityTaskManager;
+ @Mock
+ private DisplayInsetsController mDisplayInsetsController;
+ @Mock
+ private IRecentTasksListener mRecentTasksListener;
+ @Mock
+ private TaskStackTransitionObserver mTaskStackTransitionObserver;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private ShellTaskOrganizer mShellTaskOrganizer;
private RecentTasksController mRecentTasksController;
@@ -103,17 +125,24 @@ public class RecentTasksControllerTest extends ShellTestCase {
private ShellInit mShellInit;
private ShellController mShellController;
private TestShellExecutor mMainExecutor;
+ private static StaticMockitoSession sMockitoSession;
@Before
public void setUp() {
+ sMockitoSession = mockitoSession().initMocks(this).strictness(Strictness.LENIENT)
+ .mockStatic(DesktopModeStatus.class).startMocking();
+ ExtendedMockito.doReturn(true)
+ .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
+
mMainExecutor = new TestShellExecutor();
when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
mShellInit = spy(new ShellInit(mMainExecutor));
mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
- mMainExecutor));
+ 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),
@@ -121,6 +150,11 @@ public class RecentTasksControllerTest extends ShellTestCase {
mShellInit.init();
}
+ @After
+ public void tearDown() {
+ sMockitoSession.finishMocking();
+ }
+
@Test
public void instantiateController_addInitCallback() {
verify(mShellInit, times(1)).addInitCallback(any(), isA(RecentTasksController.class));
@@ -260,10 +294,6 @@ public class RecentTasksControllerTest extends ShellTestCase {
@Test
public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_groupFreeformTasks() {
- StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
- DesktopModeStatus.class).startMocking();
- when(DesktopModeStatus.isEnabled()).thenReturn(true);
-
ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
@@ -294,15 +324,54 @@ public class RecentTasksControllerTest extends ShellTestCase {
// Check single entries
assertEquals(t2, singleGroup1.getTaskInfo1());
assertEquals(t4, singleGroup2.getTaskInfo1());
+ }
+
+ @Test
+ public void testGetRecentTasks_hasActiveDesktopTasks_proto2Enabled_freeformTaskOrder() {
+ ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
+ ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
+ ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
+ ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4);
+ ActivityManager.RecentTaskInfo t5 = makeTaskInfo(5);
+ setRawList(t1, t2, t3, t4, t5);
+
+ SplitBounds pair1Bounds =
+ new SplitBounds(new Rect(), new Rect(), 1, 2, SNAP_TO_50_50);
+ mRecentTasksController.addSplitPair(t1.taskId, t2.taskId, pair1Bounds);
+
+ when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true);
+ when(mDesktopModeTaskRepository.isActiveTask(5)).thenReturn(true);
+
+ ArrayList<GroupedRecentTaskInfo> recentTasks = mRecentTasksController.getRecentTasks(
+ MAX_VALUE, RECENT_IGNORE_UNAVAILABLE, 0);
+
+ // 2 split screen tasks grouped, 2 freeform tasks grouped, 3 total recents entries
+ assertEquals(3, recentTasks.size());
+ GroupedRecentTaskInfo splitGroup = recentTasks.get(0);
+ GroupedRecentTaskInfo freeformGroup = recentTasks.get(1);
+ GroupedRecentTaskInfo singleGroup = recentTasks.get(2);
+
+ // Check that groups have expected types
+ assertEquals(GroupedRecentTaskInfo.TYPE_SPLIT, splitGroup.getType());
+ assertEquals(GroupedRecentTaskInfo.TYPE_FREEFORM, freeformGroup.getType());
+ assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup.getType());
+
+ // Check freeform group entries
+ assertEquals(t3, freeformGroup.getTaskInfoList().get(0));
+ assertEquals(t5, freeformGroup.getTaskInfoList().get(1));
+
+ // Check split group entries
+ assertEquals(t1, splitGroup.getTaskInfoList().get(0));
+ assertEquals(t2, splitGroup.getTaskInfoList().get(1));
- mockitoSession.finishMocking();
+ // Check single entry
+ assertEquals(t4, singleGroup.getTaskInfo1());
}
@Test
public void testGetRecentTasks_hasActiveDesktopTasks_proto2Disabled_doNotGroupFreeformTasks() {
- StaticMockitoSession mockitoSession = mockitoSession().mockStatic(
- DesktopModeStatus.class).startMocking();
- when(DesktopModeStatus.isEnabled()).thenReturn(false);
+ ExtendedMockito.doReturn(false)
+ .when(() -> DesktopModeStatus.canEnterDesktopMode(any()));
ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
@@ -327,8 +396,45 @@ public class RecentTasksControllerTest extends ShellTestCase {
assertEquals(t2, recentTasks.get(1).getTaskInfo1());
assertEquals(t3, recentTasks.get(2).getTaskInfo1());
assertEquals(t4, recentTasks.get(3).getTaskInfo1());
+ }
+
+ @Test
+ public void testGetRecentTasks_proto2Enabled_ignoresMinimizedFreeformTasks() {
+ ActivityManager.RecentTaskInfo t1 = makeTaskInfo(1);
+ ActivityManager.RecentTaskInfo t2 = makeTaskInfo(2);
+ ActivityManager.RecentTaskInfo t3 = makeTaskInfo(3);
+ ActivityManager.RecentTaskInfo t4 = makeTaskInfo(4);
+ ActivityManager.RecentTaskInfo t5 = makeTaskInfo(5);
+ setRawList(t1, t2, t3, t4, t5);
+
+ when(mDesktopModeTaskRepository.isActiveTask(1)).thenReturn(true);
+ when(mDesktopModeTaskRepository.isActiveTask(3)).thenReturn(true);
+ when(mDesktopModeTaskRepository.isActiveTask(5)).thenReturn(true);
+ when(mDesktopModeTaskRepository.isMinimizedTask(3)).thenReturn(true);
+
+ 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
+ assertEquals(3, recentTasks.size());
+ GroupedRecentTaskInfo freeformGroup = recentTasks.get(0);
+ GroupedRecentTaskInfo singleGroup1 = recentTasks.get(1);
+ GroupedRecentTaskInfo singleGroup2 = recentTasks.get(2);
- mockitoSession.finishMocking();
+ // Check that groups have expected types
+ assertEquals(GroupedRecentTaskInfo.TYPE_FREEFORM, freeformGroup.getType());
+ assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup1.getType());
+ assertEquals(GroupedRecentTaskInfo.TYPE_SINGLE, singleGroup2.getType());
+
+ // Check freeform group entries
+ assertEquals(2, freeformGroup.getTaskInfoList().size());
+ assertEquals(t1, freeformGroup.getTaskInfoList().get(0));
+ assertEquals(t5, freeformGroup.getTaskInfoList().get(1));
+
+ // Check single entries
+ assertEquals(t2, singleGroup1.getTaskInfo1());
+ assertEquals(t4, singleGroup2.getTaskInfo1());
}
@Test
@@ -375,6 +481,109 @@ public class RecentTasksControllerTest extends ShellTestCase {
}
@Test
+ @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS})
+ public void onTaskAdded_desktopModeRunningAppsEnabled_triggersOnRunningTaskAppeared()
+ throws Exception {
+ mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+ ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+
+ mRecentTasksControllerReal.onTaskAdded(taskInfo);
+
+ verify(mRecentTasksListener).onRunningTaskAppeared(taskInfo);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS)
+ public void onTaskAdded_desktopModeRunningAppsDisabled_doesNotTriggerOnRunningTaskAppeared()
+ throws Exception {
+ mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+ ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+
+ mRecentTasksControllerReal.onTaskAdded(taskInfo);
+
+ verify(mRecentTasksListener, never()).onRunningTaskAppeared(any());
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS})
+ public void taskWindowingModeChanged_desktopRunningAppsEnabled_triggersOnRunningTaskChanged()
+ throws Exception {
+ mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+ ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+
+ mRecentTasksControllerReal.onTaskRunningInfoChanged(taskInfo);
+
+ verify(mRecentTasksListener).onRunningTaskChanged(taskInfo);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS)
+ public void
+ taskWindowingModeChanged_desktopRunningAppsDisabled_doesNotTriggerOnRunningTaskChanged()
+ throws Exception {
+ mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+ ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+
+ mRecentTasksControllerReal.onTaskRunningInfoChanged(taskInfo);
+
+ verify(mRecentTasksListener, never()).onRunningTaskChanged(any());
+ }
+
+ @Test
+ @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE,
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS})
+ public void onTaskRemoved_desktopModeRunningAppsEnabled_triggersOnRunningTaskVanished()
+ throws Exception {
+ mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+ ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+
+ mRecentTasksControllerReal.onTaskRemoved(taskInfo);
+
+ verify(mRecentTasksListener).onRunningTaskVanished(taskInfo);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS)
+ public void onTaskRemoved_desktopModeRunningAppsDisabled_doesNotTriggerOnRunningTaskVanished()
+ throws Exception {
+ mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+ ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+
+ mRecentTasksControllerReal.onTaskRemoved(taskInfo);
+
+ verify(mRecentTasksListener, never()).onRunningTaskVanished(any());
+ }
+
+ @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);
@@ -420,6 +629,7 @@ public class RecentTasksControllerTest extends ShellTestCase {
private ActivityManager.RunningTaskInfo makeRunningTaskInfo(int taskId) {
ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo();
info.taskId = taskId;
+ info.realActivity = new ComponentName("testPackage", "testClass");
return info;
}
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/animation/PhysicsAnimatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTest.kt
index e7274918fa2b..3fb66be2f91c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.animation
+package com.android.wm.shell.shared.animation
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
@@ -27,11 +27,11 @@ import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringForce
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.animation.PhysicsAnimator.EndListener
-import com.android.wm.shell.animation.PhysicsAnimator.UpdateListener
-import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.clearAnimationUpdateFrames
-import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.getAnimationUpdateFrames
-import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.verifyAnimationUpdateFrames
+import com.android.wm.shell.shared.animation.PhysicsAnimator.EndListener
+import com.android.wm.shell.shared.animation.PhysicsAnimator.UpdateListener
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.clearAnimationUpdateFrames
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.getAnimationUpdateFrames
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.verifyAnimationUpdateFrames
import org.junit.After
import org.junit.Assert
import org.junit.Assert.assertEquals
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index 315d97ed333b..3c387f0d7c34 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -123,7 +123,7 @@ public class SplitScreenControllerTests extends ShellTestCase {
assumeTrue(ActivityTaskManager.supportsSplitScreenMultiWindow(mContext));
MockitoAnnotations.initMocks(this);
mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
- mMainExecutor));
+ mDisplayInsetsController, mMainExecutor));
mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit,
mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
mRootTDAOrganizer, mDisplayController, mDisplayImeController,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
index befc702b01aa..34b2eebb15a1 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java
@@ -39,10 +39,13 @@ import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import android.annotation.NonNull;
import android.app.ActivityManager;
@@ -63,6 +66,7 @@ import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
+import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
@@ -105,6 +109,8 @@ public class SplitTransitionTests extends ShellTestCase {
@Mock private ShellExecutor mMainExecutor;
@Mock private LaunchAdjacentController mLaunchAdjacentController;
@Mock private DefaultMixedHandler mMixedHandler;
+ @Mock private SplitScreen.SplitInvocationListener mInvocationListener;
+ private final TestShellExecutor mTestShellExecutor = new TestShellExecutor();
private SplitLayout mSplitLayout;
private MainStage mMainStage;
private SideStage mSideStage;
@@ -147,6 +153,7 @@ public class SplitTransitionTests extends ShellTestCase {
.setParentTaskId(mSideStage.mRootTaskInfo.taskId).build();
doReturn(mock(SplitDecorManager.class)).when(mMainStage).getSplitDecorManager();
doReturn(mock(SplitDecorManager.class)).when(mSideStage).getSplitDecorManager();
+ mStageCoordinator.registerSplitAnimationListener(mInvocationListener, mTestShellExecutor);
}
@Test
@@ -452,6 +459,15 @@ public class SplitTransitionTests extends ShellTestCase {
mMainStage.activate(new WindowContainerTransaction(), true /* includingTopTask */);
}
+ @Test
+ @UiThreadTest
+ public void testSplitInvocationCallback() {
+ enterSplit();
+ mTestShellExecutor.flushAll();
+ verify(mInvocationListener, times(1))
+ .onSplitAnimationInvoked(eq(true));
+ }
+
private boolean containsSplitEnter(@NonNull WindowContainerTransaction wct) {
for (int i = 0; i < wct.getHierarchyOps().size(); ++i) {
WindowContainerTransaction.HierarchyOp op = wct.getHierarchyOps().get(i);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
index d819261ecba2..d18fec2f24ad 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java
@@ -16,10 +16,7 @@
package com.android.wm.shell.splitscreen;
-import static android.app.ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
-import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED;
-import static android.app.ComponentOptions.KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION;
import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
@@ -31,8 +28,9 @@ import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_DISMISS;
+import static com.google.common.truth.Truth.assertThat;
+
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.ArgumentMatchers.notNull;
@@ -40,10 +38,13 @@ import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.PendingIntent;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
@@ -51,7 +52,7 @@ import android.os.Handler;
import android.os.Looper;
import android.view.SurfaceControl;
import android.view.SurfaceSession;
-import android.window.WindowContainerToken;
+import android.window.RemoteTransition;
import android.window.WindowContainerTransaction;
import androidx.test.annotation.UiThreadTest;
@@ -74,6 +75,7 @@ import com.android.wm.shell.common.split.SplitLayout;
import com.android.wm.shell.splitscreen.SplitScreen.SplitScreenListener;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.DefaultMixedHandler;
import com.android.wm.shell.transition.HomeTransitionObserver;
import com.android.wm.shell.transition.Transitions;
@@ -111,6 +113,8 @@ public class StageCoordinatorTests extends ShellTestCase {
private TransactionPool mTransactionPool;
@Mock
private LaunchAdjacentController mLaunchAdjacentController;
+ @Mock
+ private DefaultMixedHandler mDefaultMixedHandler;
private final Rect mBounds1 = new Rect(10, 20, 30, 40);
private final Rect mBounds2 = new Rect(5, 10, 15, 20);
@@ -337,14 +341,14 @@ public class StageCoordinatorTests extends ShellTestCase {
@Test
public void testAddActivityOptions_addsBackgroundActivitiesFlags() {
- Bundle options = mStageCoordinator.resolveStartStage(STAGE_TYPE_MAIN,
+ Bundle bundle = mStageCoordinator.resolveStartStage(STAGE_TYPE_MAIN,
SPLIT_POSITION_UNDEFINED, null /* options */, null /* wct */);
+ ActivityOptions options = ActivityOptions.fromBundle(bundle);
- assertEquals(options.getParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, WindowContainerToken.class),
- mMainStage.mRootTaskInfo.token);
- assertTrue(options.getBoolean(KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED));
- assertTrue(options.getBoolean(
- KEY_PENDING_INTENT_BACKGROUND_ACTIVITY_ALLOWED_BY_PERMISSION));
+ assertThat(options.getLaunchRootTask()).isEqualTo(mMainStage.mRootTaskInfo.token);
+ assertThat(options.getPendingIntentBackgroundActivityStartMode())
+ .isEqualTo(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+ assertThat(options.isPendingIntentBackgroundActivityLaunchAllowedByPermission()).isTrue();
}
@Test
@@ -370,6 +374,96 @@ public class StageCoordinatorTests extends ShellTestCase {
}
}
+ @Test
+ public void testSplitIntentAndTaskWithPippedApp_launchFullscreen() {
+ int taskId = 9;
+ SplitScreenTransitions splitScreenTransitions =
+ spy(mStageCoordinator.getSplitTransitions());
+ mStageCoordinator.setSplitTransitions(splitScreenTransitions);
+ mStageCoordinator.setMixedHandler(mDefaultMixedHandler);
+ PendingIntent pendingIntent = mock(PendingIntent.class);
+ RemoteTransition remoteTransition = mock(RemoteTransition.class);
+ when(remoteTransition.getDebugName()).thenReturn("");
+ // Test launching second task full screen
+ when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(true);
+ mStageCoordinator.startIntentAndTask(
+ pendingIntent,
+ null /*fillInIntent*/,
+ null /*option1*/,
+ taskId,
+ null /*option2*/,
+ 0 /*splitPosition*/,
+ 1 /*snapPosition*/,
+ remoteTransition /*remoteTransition*/,
+ null /*instanceId*/);
+ verify(splitScreenTransitions, times(1))
+ .startFullscreenTransition(any(), any());
+
+ // Test launching first intent fullscreen
+ when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(false);
+ when(mDefaultMixedHandler.isTaskInPip(taskId, mTaskOrganizer)).thenReturn(true);
+ mStageCoordinator.startIntentAndTask(
+ pendingIntent,
+ null /*fillInIntent*/,
+ null /*option1*/,
+ taskId,
+ null /*option2*/,
+ 0 /*splitPosition*/,
+ 1 /*snapPosition*/,
+ remoteTransition /*remoteTransition*/,
+ null /*instanceId*/);
+ verify(splitScreenTransitions, times(2))
+ .startFullscreenTransition(any(), any());
+ }
+
+ @Test
+ public void testSplitIntentsWithPippedApp_launchFullscreen() {
+ SplitScreenTransitions splitScreenTransitions =
+ spy(mStageCoordinator.getSplitTransitions());
+ mStageCoordinator.setSplitTransitions(splitScreenTransitions);
+ mStageCoordinator.setMixedHandler(mDefaultMixedHandler);
+ PendingIntent pendingIntent = mock(PendingIntent.class);
+ PendingIntent pendingIntent2 = mock(PendingIntent.class);
+ RemoteTransition remoteTransition = mock(RemoteTransition.class);
+ when(remoteTransition.getDebugName()).thenReturn("");
+ // Test launching second task full screen
+ when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(true);
+ mStageCoordinator.startIntents(
+ pendingIntent,
+ null /*fillInIntent*/,
+ null /*shortcutInfo1*/,
+ new Bundle(),
+ pendingIntent2,
+ null /*fillInIntent2*/,
+ null /*shortcutInfo1*/,
+ new Bundle(),
+ 0 /*splitPosition*/,
+ 1 /*snapPosition*/,
+ remoteTransition /*remoteTransition*/,
+ null /*instanceId*/);
+ verify(splitScreenTransitions, times(1))
+ .startFullscreenTransition(any(), any());
+
+ // Test launching first intent fullscreen
+ when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(false);
+ when(mDefaultMixedHandler.isIntentInPip(pendingIntent2)).thenReturn(true);
+ mStageCoordinator.startIntents(
+ pendingIntent,
+ null /*fillInIntent*/,
+ null /*shortcutInfo1*/,
+ new Bundle(),
+ pendingIntent2,
+ null /*fillInIntent2*/,
+ null /*shortcutInfo1*/,
+ new Bundle(),
+ 0 /*splitPosition*/,
+ 1 /*snapPosition*/,
+ remoteTransition /*remoteTransition*/,
+ null /*instanceId*/);
+ verify(splitScreenTransitions, times(2))
+ .startFullscreenTransition(any(), any());
+ }
+
private Transitions createTestTransitions() {
ShellInit shellInit = new ShellInit(mMainExecutor);
final Transitions t = new Transitions(mContext, shellInit, mock(ShellController.class),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
index 012c40811811..ff76a2f13527 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
@@ -40,6 +40,7 @@ import com.android.internal.util.function.TriConsumer;
import com.android.launcher3.icons.IconProvider;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -65,6 +66,7 @@ public class StartingWindowControllerTests extends ShellTestCase {
private @Mock Context mContext;
private @Mock DisplayManager mDisplayManager;
+ private @Mock DisplayInsetsController mDisplayInsetsController;
private @Mock ShellCommandHandler mShellCommandHandler;
private @Mock ShellTaskOrganizer mTaskOrganizer;
private @Mock ShellExecutor mMainExecutor;
@@ -83,7 +85,7 @@ public class StartingWindowControllerTests extends ShellTestCase {
doReturn(super.mContext.getResources()).when(mContext).getResources();
mShellInit = spy(new ShellInit(mMainExecutor));
mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler,
- mMainExecutor));
+ mDisplayInsetsController, mMainExecutor));
mController = new StartingWindowController(mContext, mShellInit, mShellController,
mTaskOrganizer, mMainExecutor, mTypeAlgorithm, mIconProvider, mTransactionPool);
mShellInit.init();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
index 7c520c34b29d..6292018ba35d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
@@ -23,6 +23,7 @@ import static org.mockito.Mockito.mock;
import android.content.Context;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
+import android.graphics.Rect;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
@@ -35,8 +36,8 @@ import androidx.test.platform.app.InstrumentationRegistry;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.ExternalInterfaceBinder;
-import com.android.wm.shell.common.ShellExecutor;
import org.junit.After;
import org.junit.Before;
@@ -63,12 +64,15 @@ public class ShellControllerTest extends ShellTestCase {
private ShellCommandHandler mShellCommandHandler;
@Mock
private Context mTestUserContext;
+ @Mock
+ private DisplayInsetsController mDisplayInsetsController;
private TestShellExecutor mExecutor;
private ShellController mController;
private TestConfigurationChangeListener mConfigChangeListener;
private TestKeyguardChangeListener mKeyguardChangeListener;
private TestUserChangeListener mUserChangeListener;
+ private TestDisplayImeChangeListener mDisplayImeChangeListener;
@Before
@@ -77,8 +81,10 @@ public class ShellControllerTest extends ShellTestCase {
mKeyguardChangeListener = new TestKeyguardChangeListener();
mConfigChangeListener = new TestConfigurationChangeListener();
mUserChangeListener = new TestUserChangeListener();
+ mDisplayImeChangeListener = new TestDisplayImeChangeListener();
mExecutor = new TestShellExecutor();
- mController = new ShellController(mContext, mShellInit, mShellCommandHandler, mExecutor);
+ mController = new ShellController(mContext, mShellInit, mShellCommandHandler,
+ mDisplayInsetsController, mExecutor);
mController.onConfigurationChanged(getConfigurationCopy());
}
@@ -130,6 +136,45 @@ public class ShellControllerTest extends ShellTestCase {
}
@Test
+ public void testAddDisplayImeChangeListener_ensureCallback() {
+ mController.asShell().addDisplayImeChangeListener(
+ mDisplayImeChangeListener, mExecutor);
+
+ final Rect bounds = new Rect(10, 20, 30, 40);
+ mController.onImeBoundsChanged(bounds);
+ mController.onImeVisibilityChanged(true);
+ mExecutor.flushAll();
+
+ assertTrue(mDisplayImeChangeListener.boundsChanged == 1);
+ assertTrue(bounds.equals(mDisplayImeChangeListener.lastBounds));
+ assertTrue(mDisplayImeChangeListener.visibilityChanged == 1);
+ assertTrue(mDisplayImeChangeListener.lastVisibility);
+ }
+
+ @Test
+ public void testDoubleAddDisplayImeChangeListener_ensureSingleCallback() {
+ mController.asShell().addDisplayImeChangeListener(
+ mDisplayImeChangeListener, mExecutor);
+ mController.asShell().addDisplayImeChangeListener(
+ mDisplayImeChangeListener, mExecutor);
+
+ mController.onImeVisibilityChanged(true);
+ mExecutor.flushAll();
+ assertTrue(mDisplayImeChangeListener.visibilityChanged == 1);
+ }
+
+ @Test
+ public void testAddRemoveDisplayImeChangeListener_ensureNoCallback() {
+ mController.asShell().addDisplayImeChangeListener(
+ mDisplayImeChangeListener, mExecutor);
+ mController.asShell().removeDisplayImeChangeListener(mDisplayImeChangeListener);
+
+ mController.onImeVisibilityChanged(true);
+ mExecutor.flushAll();
+ assertTrue(mDisplayImeChangeListener.visibilityChanged == 0);
+ }
+
+ @Test
public void testAddUserChangeListener_ensureCallback() {
mController.addUserChangeListener(mUserChangeListener);
@@ -457,4 +502,23 @@ public class ShellControllerTest extends ShellTestCase {
lastUserProfiles = profiles;
}
}
+
+ private static class TestDisplayImeChangeListener implements DisplayImeChangeListener {
+ public int boundsChanged = 0;
+ public Rect lastBounds;
+ public int visibilityChanged = 0;
+ public boolean lastVisibility = false;
+
+ @Override
+ public void onImeBoundsChanged(int displayId, Rect bounds) {
+ boundsChanged++;
+ lastBounds = bounds;
+ }
+
+ @Override
+ public void onImeVisibilityChanged(int displayId, boolean isShowing) {
+ visibilityChanged++;
+ lastVisibility = isShowing;
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
index d7c46104b6b1..0434742c571b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java
@@ -44,7 +44,6 @@ import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.Region;
-import android.os.Handler;
import android.os.Looper;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -498,6 +497,31 @@ public class TaskViewTest extends ShellTestCase {
}
@Test
+ public void testStartRootTask_setsBoundsAndVisibility() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ TaskViewBase taskViewBase = mock(TaskViewBase.class);
+ Rect bounds = new Rect(0, 0, 100, 100);
+ when(taskViewBase.getCurrentBoundsOnScreen()).thenReturn(bounds);
+ mTaskViewTaskController.setTaskViewBase(taskViewBase);
+
+ // Surface created, but task not available so bounds / visibility isn't set
+ mTaskView.surfaceCreated(mock(SurfaceHolder.class));
+ verify(mTaskViewTransitions, never()).updateVisibilityState(
+ eq(mTaskViewTaskController), eq(true));
+
+ // Make the task available
+ WindowContainerTransaction wct = mock(WindowContainerTransaction.class);
+ mTaskViewTaskController.startRootTask(mTaskInfo, mLeash, wct);
+
+ // Bounds got set
+ verify(wct).setBounds(any(WindowContainerToken.class), eq(bounds));
+ // Visibility & bounds state got set
+ verify(mTaskViewTransitions).updateVisibilityState(eq(mTaskViewTaskController), eq(true));
+ verify(mTaskViewTransitions).updateBoundsState(eq(mTaskViewTaskController), eq(bounds));
+ }
+
+ @Test
public void testTaskViewPrepareOpenAnimationSetsBoundsAndVisibility() {
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java
index fbc0db9c2850..d3e40f21db23 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java
@@ -18,6 +18,7 @@ package com.android.wm.shell.taskview;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static com.google.common.truth.Truth.assertThat;
@@ -208,6 +209,48 @@ public class TaskViewTransitionsTest extends ShellTestCase {
}
@Test
+ public void testReorderTask_movedToFrontTransaction() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskViewTransitions.reorderTaskViewTask(mTaskViewTaskController, true);
+ // Consume the pending transaction from order change
+ TaskViewTransitions.PendingTransition pending =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT);
+ assertThat(pending).isNotNull();
+ mTaskViewTransitions.startAnimation(pending.mClaimed,
+ mock(TransitionInfo.class),
+ new SurfaceControl.Transaction(),
+ new SurfaceControl.Transaction(),
+ mock(Transitions.TransitionFinishCallback.class));
+
+ // Verify it was consumed
+ TaskViewTransitions.PendingTransition pending2 =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_FRONT);
+ assertThat(pending2).isNull();
+ }
+
+ @Test
+ public void testReorderTask_movedToBackTransaction() {
+ assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+
+ mTaskViewTransitions.reorderTaskViewTask(mTaskViewTaskController, false);
+ // Consume the pending transaction from order change
+ TaskViewTransitions.PendingTransition pending =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_BACK);
+ assertThat(pending).isNotNull();
+ mTaskViewTransitions.startAnimation(pending.mClaimed,
+ mock(TransitionInfo.class),
+ new SurfaceControl.Transaction(),
+ new SurfaceControl.Transaction(),
+ mock(Transitions.TransitionFinishCallback.class));
+
+ // Verify it was consumed
+ TaskViewTransitions.PendingTransition pending2 =
+ mTaskViewTransitions.findPending(mTaskViewTaskController, TRANSIT_TO_BACK);
+ assertThat(pending2).isNull();
+ }
+
+ @Test
public void test_startAnimation_setsTaskNotFound() {
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
index 66efa02de764..0db10ef65a74 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
@@ -24,6 +24,7 @@ import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mock;
@@ -51,6 +52,7 @@ import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.shared.IHomeTransitionListener;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
@@ -166,6 +168,25 @@ public class HomeTransitionObserverTest extends ShellTestCase {
}
@Test
+ public void testStartDragToDesktopDoesNotTriggerCallback() throws RemoteException {
+ TransitionInfo info = mock(TransitionInfo.class);
+ TransitionInfo.Change change = mock(TransitionInfo.Change.class);
+ ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class);
+ when(change.getTaskInfo()).thenReturn(taskInfo);
+ when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change)));
+ when(info.getType()).thenReturn(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP);
+
+ setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_OPEN, true);
+
+ mHomeTransitionObserver.onTransitionReady(mock(IBinder.class),
+ info,
+ mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class));
+
+ verify(mListener, times(0)).onHomeVisibilityChanged(anyBoolean());
+ }
+
+ @Test
public void testHomeActivityWithBackGestureNotifiesHomeIsVisible() throws RemoteException {
TransitionInfo info = mock(TransitionInfo.class);
TransitionInfo.Change change = mock(TransitionInfo.Change.class);
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 e9da25813510..964d86e8bd35 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
@@ -73,6 +73,7 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArraySet;
import android.util.Pair;
import android.view.IRecentsAnimationRunner;
@@ -83,9 +84,11 @@ import android.window.IRemoteTransition;
import android.window.IRemoteTransitionFinishedCallback;
import android.window.IWindowContainerToken;
import android.window.RemoteTransition;
+import android.window.RemoteTransitionStub;
import android.window.TransitionFilter;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
+import android.window.WindowAnimationState;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
@@ -97,6 +100,7 @@ import androidx.test.platform.app.InstrumentationRegistry;
import com.android.internal.R;
import com.android.internal.policy.TransitionAnimation;
+import com.android.systemui.shared.Flags;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
@@ -113,6 +117,7 @@ import com.android.wm.shell.sysui.ShellSharedConstants;
import com.android.wm.shell.util.StubTransaction;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
@@ -140,6 +145,9 @@ public class ShellTransitionTests extends ShellTestCase {
private final TestTransitionHandler mDefaultHandler = new TestTransitionHandler();
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+ @Rule
+ public final SetFlagsRule setFlagsRule = new SetFlagsRule();
+
@Before
public void setUp() {
doAnswer(invocation -> new Binder())
@@ -280,7 +288,7 @@ public class ShellTransitionTests extends ShellTestCase {
final boolean[] remoteCalled = new boolean[]{false};
final WindowContainerTransaction remoteFinishWCT = new WindowContainerTransaction();
- IRemoteTransition testRemote = new IRemoteTransition.Stub() {
+ IRemoteTransition testRemote = new RemoteTransitionStub() {
@Override
public void startAnimation(IBinder token, TransitionInfo info,
SurfaceControl.Transaction t,
@@ -288,16 +296,6 @@ public class ShellTransitionTests extends ShellTestCase {
remoteCalled[0] = true;
finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */);
}
-
- @Override
- public void mergeAnimation(IBinder token, TransitionInfo info,
- SurfaceControl.Transaction t, IBinder mergeTarget,
- IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
- }
-
- @Override
- public void onTransitionConsumed(IBinder iBinder, boolean b) throws RemoteException {
- }
};
IBinder transitToken = new Binder();
transitions.requestStartTransition(transitToken,
@@ -450,7 +448,7 @@ public class ShellTransitionTests extends ShellTestCase {
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
final boolean[] remoteCalled = new boolean[]{false};
- IRemoteTransition testRemote = new IRemoteTransition.Stub() {
+ IRemoteTransition testRemote = new RemoteTransitionStub() {
@Override
public void startAnimation(IBinder token, TransitionInfo info,
SurfaceControl.Transaction t,
@@ -458,15 +456,73 @@ public class ShellTransitionTests extends ShellTestCase {
remoteCalled[0] = true;
finishCallback.onTransitionFinished(null /* wct */, null /* sct */);
}
+ };
+
+ TransitionFilter filter = new TransitionFilter();
+ filter.mRequirements =
+ new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()};
+ filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
+
+ transitions.registerRemote(filter, new RemoteTransition(testRemote, "Test"));
+ mMainExecutor.flushAll();
+ IBinder transitToken = new Binder();
+ transitions.requestStartTransition(transitToken,
+ new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+ verify(mOrganizer, times(1)).startTransition(eq(transitToken), any());
+ TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ transitions.onTransitionReady(transitToken, info, new StubTransaction(),
+ new StubTransaction());
+ assertEquals(0, mDefaultHandler.activeCount());
+ assertTrue(remoteCalled[0]);
+ mDefaultHandler.finishAll();
+ mMainExecutor.flushAll();
+ verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any());
+ }
+
+ @Test
+ public void testRegisteredRemoteTransitionTakeover() {
+ Transitions transitions = createTestTransitions();
+ transitions.replaceDefaultHandlerForTest(mDefaultHandler);
+
+ IRemoteTransition testRemote = new RemoteTransitionStub() {
@Override
- public void mergeAnimation(IBinder token, TransitionInfo info,
- SurfaceControl.Transaction t, IBinder mergeTarget,
+ public void startAnimation(IBinder token, TransitionInfo info,
+ SurfaceControl.Transaction t,
IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
+ final Transitions.TransitionHandler takeoverHandler =
+ transitions.getHandlerForTakeover(token, info);
+
+ if (takeoverHandler == null) {
+ finishCallback.onTransitionFinished(null /* wct */, null /* sct */);
+ return;
+ }
+
+ takeoverHandler.takeOverAnimation(token, info, new SurfaceControl.Transaction(),
+ wct -> {
+ try {
+ finishCallback.onTransitionFinished(wct, null /* sct */);
+ } catch (RemoteException e) {
+ // Fail
+ }
+ }, new WindowAnimationState[info.getChanges().size()]);
}
+ };
+ final boolean[] takeoverRemoteCalled = new boolean[]{false};
+ IRemoteTransition testTakeoverRemote = new RemoteTransitionStub() {
+ @Override
+ public void startAnimation(IBinder token, TransitionInfo info,
+ SurfaceControl.Transaction t,
+ IRemoteTransitionFinishedCallback finishCallback) {}
@Override
- public void onTransitionConsumed(IBinder iBinder, boolean b) throws RemoteException {
+ public void takeOverAnimation(IBinder transition, TransitionInfo info,
+ SurfaceControl.Transaction startTransaction,
+ IRemoteTransitionFinishedCallback finishCallback, WindowAnimationState[] states)
+ throws RemoteException {
+ takeoverRemoteCalled[0] = true;
+ finishCallback.onTransitionFinished(null /* wct */, null /* sct */);
}
};
@@ -476,21 +532,38 @@ public class ShellTransitionTests extends ShellTestCase {
filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT};
transitions.registerRemote(filter, new RemoteTransition(testRemote, "Test"));
+ transitions.registerRemoteForTakeover(
+ filter, new RemoteTransition(testTakeoverRemote, "Test"));
mMainExecutor.flushAll();
+ // Takeover shouldn't happen when the flag is disabled.
+ setFlagsRule.disableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY);
IBinder transitToken = new Binder();
transitions.requestStartTransition(transitToken,
new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
- verify(mOrganizer, times(1)).startTransition(eq(transitToken), any());
TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN)
.addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
transitions.onTransitionReady(transitToken, info, new StubTransaction(),
new StubTransaction());
assertEquals(0, mDefaultHandler.activeCount());
- assertTrue(remoteCalled[0]);
+ assertFalse(takeoverRemoteCalled[0]);
mDefaultHandler.finishAll();
mMainExecutor.flushAll();
verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any());
+
+ // Takeover should happen when the flag is enabled.
+ setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LIBRARY);
+ transitions.requestStartTransition(transitToken,
+ new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */));
+ info = new TransitionInfoBuilder(TRANSIT_OPEN)
+ .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build();
+ transitions.onTransitionReady(transitToken, info, new StubTransaction(),
+ new StubTransaction());
+ assertEquals(0, mDefaultHandler.activeCount());
+ assertTrue(takeoverRemoteCalled[0]);
+ mDefaultHandler.finishAll();
+ mMainExecutor.flushAll();
+ verify(mOrganizer, times(2)).finishTransition(eq(transitToken), any());
}
@Test
@@ -499,8 +572,9 @@ public class ShellTransitionTests extends ShellTestCase {
transitions.replaceDefaultHandlerForTest(mDefaultHandler);
final boolean[] remoteCalled = new boolean[]{false};
+ final boolean[] takeoverRemoteCalled = new boolean[]{false};
final WindowContainerTransaction remoteFinishWCT = new WindowContainerTransaction();
- IRemoteTransition testRemote = new IRemoteTransition.Stub() {
+ IRemoteTransition testRemote = new RemoteTransitionStub() {
@Override
public void startAnimation(IBinder token, TransitionInfo info,
SurfaceControl.Transaction t,
@@ -510,13 +584,12 @@ public class ShellTransitionTests extends ShellTestCase {
}
@Override
- public void mergeAnimation(IBinder token, TransitionInfo info,
- SurfaceControl.Transaction t, IBinder mergeTarget,
- IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
- }
-
- @Override
- public void onTransitionConsumed(IBinder iBinder, boolean b) throws RemoteException {
+ public void takeOverAnimation(IBinder transition, TransitionInfo info,
+ SurfaceControl.Transaction startTransaction,
+ IRemoteTransitionFinishedCallback finishCallback, WindowAnimationState[] states)
+ throws RemoteException {
+ takeoverRemoteCalled[0] = true;
+ finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */);
}
};
@@ -524,6 +597,7 @@ public class ShellTransitionTests extends ShellTestCase {
OneShotRemoteHandler oneShot = new OneShotRemoteHandler(mMainExecutor,
new RemoteTransition(testRemote, "Test"));
+
// Verify that it responds to the remote but not other things.
IBinder transitToken = new Binder();
assertNotNull(oneShot.handleRequest(transitToken,
@@ -534,6 +608,7 @@ public class ShellTransitionTests extends ShellTestCase {
Transitions.TransitionFinishCallback testFinish =
mock(Transitions.TransitionFinishCallback.class);
+
// Verify that it responds to animation properly
oneShot.setTransition(transitToken);
IBinder anotherToken = new Binder();
@@ -543,6 +618,16 @@ public class ShellTransitionTests extends ShellTestCase {
assertTrue(oneShot.startAnimation(transitToken, new TransitionInfo(transitType, 0),
new StubTransaction(), new StubTransaction(),
testFinish));
+ assertTrue(remoteCalled[0]);
+
+ // Verify that it handles takeovers properly
+ IBinder newToken = new Binder();
+ oneShot.setTransition(newToken);
+ assertFalse(oneShot.takeOverAnimation(transitToken, new TransitionInfo(transitType, 0),
+ new StubTransaction(), testFinish, new WindowAnimationState[0]));
+ assertTrue(oneShot.takeOverAnimation(newToken, new TransitionInfo(transitType, 0),
+ new StubTransaction(), testFinish, new WindowAnimationState[0]));
+ assertTrue(takeoverRemoteCalled[0]);
}
@Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java
index 87330d2dc877..184e8955d08c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java
@@ -20,6 +20,7 @@ import android.os.RemoteException;
import android.view.SurfaceControl;
import android.window.IRemoteTransition;
import android.window.IRemoteTransitionFinishedCallback;
+import android.window.RemoteTransitionStub;
import android.window.TransitionInfo;
import android.window.WindowContainerTransaction;
@@ -29,7 +30,7 @@ import android.window.WindowContainerTransaction;
* {@link #startAnimation(IBinder, TransitionInfo, SurfaceControl.Transaction,
* IRemoteTransitionFinishedCallback)} being called.
*/
-public class TestRemoteTransition extends IRemoteTransition.Stub {
+public class TestRemoteTransition extends RemoteTransitionStub {
private boolean mCalled = false;
private boolean mConsumed = false;
final WindowContainerTransaction mRemoteFinishWCT = new WindowContainerTransaction();
@@ -44,12 +45,6 @@ public class TestRemoteTransition extends IRemoteTransition.Stub {
}
@Override
- public void mergeAnimation(IBinder transition, TransitionInfo info,
- SurfaceControl.Transaction t, IBinder mergeTarget,
- IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {
- }
-
- @Override
public void onTransitionConsumed(IBinder iBinder, boolean b) throws RemoteException {
mConsumed = true;
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java
index c5e229feaba7..acc0bce5cce9 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java
@@ -298,6 +298,32 @@ public class UnfoldTransitionHandlerTest {
}
@Test
+ public void fold_animationInProgress_finishesTransition() {
+ TransitionRequestInfo requestInfo = createUnfoldTransitionRequestInfo();
+ TransitionFinishCallback finishCallback = mock(TransitionFinishCallback.class);
+
+ // Unfold
+ mShellUnfoldProgressProvider.onFoldStateChanged(/* isFolded= */ false);
+ mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo);
+ mUnfoldTransitionHandler.startAnimation(
+ mTransition,
+ mock(TransitionInfo.class),
+ mock(SurfaceControl.Transaction.class),
+ mock(SurfaceControl.Transaction.class),
+ finishCallback
+ );
+
+ // Start animation but don't finish it
+ mShellUnfoldProgressProvider.onStateChangeStarted();
+ mShellUnfoldProgressProvider.onStateChangeProgress(0.5f);
+
+ // Fold
+ mShellUnfoldProgressProvider.onFoldStateChanged(/* isFolded= */ true);
+
+ verify(finishCallback).onTransitionFinished(any());
+ }
+
+ @Test
public void mergeAnimation_eatsDisplayOnlyTransitions() {
TransitionRequestInfo requestInfo = createUnfoldTransitionRequestInfo();
mUnfoldTransitionHandler.handleRequest(mTransition, requestInfo);
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 917fd715f71f..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,13 +22,22 @@ 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
+import android.hardware.input.InputManager
import android.os.Handler
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.platform.test.flag.junit.SetFlagsRule
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
+import android.util.SparseArray
import android.view.Choreographer
import android.view.Display.DEFAULT_DISPLAY
import android.view.IWindowManager
@@ -36,11 +45,18 @@ import android.view.InputChannel
import android.view.InputMonitor
import android.view.InsetsSource
import android.view.InsetsState
+import android.view.KeyEvent
import android.view.SurfaceControl
import android.view.SurfaceView
+import android.view.View
import android.view.WindowInsets.Type.navigationBars
import android.view.WindowInsets.Type.statusBars
import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.window.flags.Flags
+import com.android.wm.shell.R
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
@@ -51,16 +67,23 @@ import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.desktopmode.DesktopTasksController
+import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
+import com.android.wm.shell.shared.DesktopModeStatus
import com.android.wm.shell.sysui.KeyguardChangeListener
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.windowdecor.DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener
+import java.util.Optional
+import java.util.function.Supplier
+import org.junit.Assert.assertEquals
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
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
@@ -69,16 +92,26 @@ import org.mockito.Mockito.verify
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
+import org.mockito.kotlin.spy
import org.mockito.kotlin.whenever
-import java.util.Optional
-import java.util.function.Supplier
+import org.mockito.quality.Strictness
-
-/** Tests of [DesktopModeWindowDecorViewModel] */
+/**
+ * Tests of [DesktopModeWindowDecorViewModel]
+ * Usage: atest WMShellUnitTests:DesktopModeWindowDecorViewModelTests
+ */
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
+ @JvmField
+ @Rule
+ val setFlagsRule = SetFlagsRule()
+
+ @JvmField
+ @Rule
+ val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
@Mock private lateinit var mockDesktopModeWindowDecorFactory:
DesktopModeWindowDecoration.Factory
@Mock private lateinit var mockMainHandler: Handler
@@ -102,6 +135,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
private val transactionFactory = Supplier<SurfaceControl.Transaction> {
SurfaceControl.Transaction()
}
+ private val windowDecorByTaskIdSpy = spy(SparseArray<DesktopModeWindowDecoration>())
private lateinit var shellInit: ShellInit
private lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener
@@ -110,6 +144,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
@Before
fun setUp() {
shellInit = ShellInit(mockShellExecutor)
+ windowDecorByTaskIdSpy.clear()
desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel(
mContext,
mockShellExecutor,
@@ -128,7 +163,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
mockDesktopModeWindowDecorFactory,
mockInputMonitorFactory,
transactionFactory,
- mockRootTaskDisplayAreaOrganizer
+ mockRootTaskDisplayAreaOrganizer,
+ windowDecorByTaskIdSpy
)
whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
@@ -251,6 +287,41 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
}
@Test
+ fun testBackEventHasRightDisplayId() {
+ val secondaryDisplay = createVirtualDisplay() ?: return
+ val secondaryDisplayId = secondaryDisplay.display.displayId
+ val task = createTask(
+ displayId = secondaryDisplayId,
+ windowingMode = WINDOWING_MODE_FREEFORM
+ )
+ val windowDecor = setUpMockDecorationForTask(task)
+
+ onTaskOpening(task)
+ val onClickListenerCaptor = argumentCaptor<View.OnClickListener>()
+ verify(windowDecor).setCaptionListeners(
+ onClickListenerCaptor.capture(), any(), any(), any())
+
+ val onClickListener = onClickListenerCaptor.firstValue
+ val view = mock(View::class.java)
+ whenever(view.id).thenReturn(R.id.back_button)
+
+ val inputManager = mock(InputManager::class.java)
+ mContext.addMockSystemService(InputManager::class.java, inputManager)
+
+ val freeformTaskTransitionStarter = mock(FreeformTaskTransitionStarter::class.java)
+ desktopModeWindowDecorViewModel
+ .setFreeformTaskTransitionStarter(freeformTaskTransitionStarter)
+
+ onClickListener.onClick(view)
+
+ val eventCaptor = argumentCaptor<KeyEvent>()
+ verify(inputManager, times(2)).injectInputEvent(eventCaptor.capture(), anyInt())
+
+ assertEquals(secondaryDisplayId, eventCaptor.firstValue.displayId)
+ assertEquals(secondaryDisplayId, eventCaptor.secondValue.displayId)
+ }
+
+ @Test
fun testCaptionIsNotCreatedWhenKeyguardIsVisible() {
val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
val keyguardListenerCaptor = argumentCaptor<KeyguardChangeListener>()
@@ -272,6 +343,36 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
}
@Test
+ fun testDecorationIsNotCreatedForTopTranslucentActivities() {
+ setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply {
+ isTopActivityTransparent = true
+ numActivities = 1
+ }
+ onTaskOpening(task)
+
+ verify(mockDesktopModeWindowDecorFactory, never())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ }
+
+ @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)
val decoration = setUpMockDecorationForTask(task)
@@ -292,6 +393,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING)
fun testRelayoutDoesNotRunWhenNonStatusBarsInsetsSourceVisibilityChanges() {
val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true)
val decoration = setUpMockDecorationForTask(task)
@@ -312,6 +414,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING)
fun testRelayoutDoesNotRunWhenNonStatusBarsInsetSourceVisibilityDoesNotChange() {
val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM, focused = true)
val decoration = setUpMockDecorationForTask(task)
@@ -332,6 +435,89 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
verify(decoration, times(1)).relayout(task)
}
+ @Test
+ fun testDestroyWindowDecoration_closesBeforeCleanup() {
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
+ val decoration = setUpMockDecorationForTask(task)
+ val inOrder = Mockito.inOrder(decoration, windowDecorByTaskIdSpy)
+
+ onTaskOpening(task)
+ desktopModeWindowDecorViewModel.destroyWindowDecoration(task)
+
+ inOrder.verify(decoration).close()
+ inOrder.verify(windowDecorByTaskIdSpy).remove(task.taskId)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ fun testWindowDecor_desktopModeUnsupportedOnDevice_decorNotCreated() {
+ val mockitoSession: StaticMockitoSession = mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .spyStatic(DesktopModeStatus::class.java)
+ .startMocking()
+ try {
+ // Simulate default enforce device restrictions system property
+ whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
+
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+ // Simulate device that doesn't support desktop mode
+ doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+
+ onTaskOpening(task)
+ verify(mockDesktopModeWindowDecorFactory, never())
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ } finally {
+ mockitoSession.finishMocking()
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ fun testWindowDecor_desktopModeUnsupportedOnDevice_deviceRestrictionsOverridden_decorCreated() {
+ val mockitoSession: StaticMockitoSession = mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .spyStatic(DesktopModeStatus::class.java)
+ .startMocking()
+ try {
+ // Simulate enforce device restrictions system property overridden to false
+ whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false)
+ // Simulate device that doesn't support desktop mode
+ doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+ setUpMockDecorationsForTasks(task)
+
+ onTaskOpening(task)
+ verify(mockDesktopModeWindowDecorFactory)
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ } finally {
+ mockitoSession.finishMocking()
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE)
+ fun testWindowDecor_deviceSupportsDesktopMode_decorCreated() {
+ val mockitoSession: StaticMockitoSession = mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .spyStatic(DesktopModeStatus::class.java)
+ .startMocking()
+ try {
+ // Simulate default enforce device restrictions system property
+ whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
+
+ val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true)
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ setUpMockDecorationsForTasks(task)
+
+ onTaskOpening(task)
+ verify(mockDesktopModeWindowDecorFactory)
+ .create(any(), any(), any(), eq(task), any(), any(), any(), any(), any())
+ } finally {
+ mockitoSession.finishMocking()
+ }
+ }
+
private fun onTaskOpening(task: RunningTaskInfo, leash: SurfaceControl = SurfaceControl()) {
desktopModeWindowDecorViewModel.onTaskOpening(
task,
@@ -354,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)
@@ -362,13 +549,15 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
.setVisible(true)
.setActivityType(activityType)
.build().apply {
+ topActivityInfo = activityInfo
isFocused = focused
}
}
private fun setUpMockDecorationForTask(task: RunningTaskInfo): DesktopModeWindowDecoration {
val decoration = mock(DesktopModeWindowDecoration::class.java)
- whenever(mockDesktopModeWindowDecorFactory.create(
+ whenever(
+ mockDesktopModeWindowDecorFactory.create(
any(), any(), any(), eq(task), any(), any(), any(), any(), any())
).thenReturn(decoration)
decoration.mTaskInfo = task
@@ -387,7 +576,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
"testEventReceiversOnMultipleDisplays",
/*width=*/ 400,
/*height=*/ 400,
- /*densityDpi=*/320,
+ /*densityDpi=*/ 320,
surfaceView.holder.surface,
DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
)
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 9e62bd254ac5..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
@@ -18,13 +18,20 @@ package com.android.wm.shell.windowdecor;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
+import static android.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;
@@ -33,35 +40,52 @@ 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;
import android.os.SystemProperties;
+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.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;
import com.android.wm.shell.ShellTaskOrganizer;
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;
@@ -81,6 +105,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
private static final String USE_ROUNDED_CORNERS_SYSPROP_KEY =
"persist.wm.debug.desktop_use_rounded_corners";
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
+
@Mock
private DisplayController mMockDisplayController;
@Mock
@@ -96,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. */
@@ -121,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
@@ -173,10 +225,53 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
}
@Test
- public void updateRelayoutParams_freeformAndTransparent_allowsInputFallthrough() {
+ @EnableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY)
+ public void updateRelayoutParams_appHeader_usesTaskDensity() {
+ final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources()
+ .getConfiguration().densityDpi;
+ final int customTaskDensity = systemDensity + 300;
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
- taskInfo.taskDescription.setStatusBarAppearance(
+ taskInfo.configuration.densityDpi = customTaskDensity;
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false);
+
+ assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(customTaskDensity);
+ }
+
+ @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;
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ taskInfo.configuration.densityDpi = customTaskDensity;
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false);
+
+ assertThat(relayoutParams.mWindowDecorConfig.densityDpi).isEqualTo(systemDensity);
+ }
+
+ @Test
+ public void updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(
APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND);
final RelayoutParams relayoutParams = new RelayoutParams();
@@ -187,14 +282,14 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* applyStartTransactionOnDraw= */ true,
/* shouldSetTaskPositionAndCrop */ false);
- assertThat(relayoutParams.mAllowCaptionInputFallthrough).isTrue();
+ assertThat(relayoutParams.hasInputFeatureSpy()).isTrue();
}
@Test
- public void updateRelayoutParams_freeformButOpaque_disallowsInputFallthrough() {
+ public void updateRelayoutParams_freeformButOpaqueAppearance_disallowsInputFallthrough() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
- taskInfo.taskDescription.setStatusBarAppearance(0);
+ taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(0);
final RelayoutParams relayoutParams = new RelayoutParams();
DesktopModeWindowDecoration.updateRelayoutParams(
@@ -204,7 +299,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* applyStartTransactionOnDraw= */ true,
/* shouldSetTaskPositionAndCrop */ false);
- assertThat(relayoutParams.mAllowCaptionInputFallthrough).isFalse();
+ assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
}
@Test
@@ -220,7 +315,148 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* applyStartTransactionOnDraw= */ true,
/* shouldSetTaskPositionAndCrop */ false);
- assertThat(relayoutParams.mAllowCaptionInputFallthrough).isFalse();
+ assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
+ }
+
+ @Test
+ public void updateRelayoutParams_freeform_inputChannelNeeded() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false);
+
+ assertThat(hasNoInputChannelFeature(relayoutParams)).isFalse();
+ }
+
+ @Test
+ public void updateRelayoutParams_fullscreen_inputChannelNotNeeded() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false);
+
+ assertThat(hasNoInputChannelFeature(relayoutParams)).isTrue();
+ }
+
+ @Test
+ public void updateRelayoutParams_multiwindow_inputChannelNotNeeded() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+ final RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false);
+
+ 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) {
@@ -243,12 +479,16 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
private DesktopModeWindowDecoration createWindowDecoration(
ActivityManager.RunningTaskInfo taskInfo) {
- return new DesktopModeWindowDecoration(mContext, mMockDisplayController,
- mMockShellTaskOrganizer, taskInfo, mMockSurfaceControl, mConfiguration,
+ 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) {
@@ -268,4 +508,37 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
return taskInfo;
}
+
+ private static boolean hasNoInputChannelFeature(RelayoutParams params) {
+ 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 e60be7186b1e..f750e6b9a6fe 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,14 +16,20 @@
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.EnableFlags
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
@@ -33,8 +39,8 @@ import org.junit.Before
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
/**
@@ -57,6 +63,10 @@ class DragPositioningCallbackUtilityTest {
private lateinit var mockDisplayLayout: DisplayLayout
@Mock
private lateinit var mockDisplay: Display
+ @Mock
+ private lateinit var mockContext: Context
+ @Mock
+ private lateinit var mockResources: Resources
@Before
fun setup() {
@@ -69,16 +79,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 +102,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 +122,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 +136,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 +162,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 +181,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)
@@ -189,20 +198,20 @@ class DragPositioningCallbackUtilityTest {
DISPLAY_BOUNDS.right - 100,
DISPLAY_BOUNDS.bottom - 100)
- DragPositioningCallbackUtility.onDragEnd(repositionTaskBounds, STARTING_BOUNDS,
- startingPoint, startingPoint.x - 1000, (DISPLAY_BOUNDS.bottom + 1000).toFloat(),
+ DragPositioningCallbackUtility.updateTaskBounds(repositionTaskBounds, STARTING_BOUNDS,
+ startingPoint, startingPoint.x - 1000, (DISPLAY_BOUNDS.bottom + 1000).toFloat())
+ DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(repositionTaskBounds,
validDragArea)
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)
@@ -211,26 +220,127 @@ 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)
+ }
+
+ 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
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
new file mode 100644
index 000000000000..4dea5a75a0e8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
@@ -0,0 +1,390 @@
+/*
+ * 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 static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM;
+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.DragPositioningCallback.CTRL_TYPE_UNDEFINED;
+
+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.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.testing.AndroidTestingRunner;
+import android.util.Size;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.window.flags.Flags;
+
+import com.google.common.testing.EqualsTester;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@link DragResizeWindowGeometry}.
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:DragResizeWindowGeometryTests
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class DragResizeWindowGeometryTests {
+ private static final Size TASK_SIZE = new Size(500, 1000);
+ private static final int TASK_CORNER_RADIUS = 10;
+ private static final int EDGE_RESIZE_THICKNESS = 15;
+ private static final int EDGE_RESIZE_DEBUG_THICKNESS = EDGE_RESIZE_THICKNESS
+ + (DragResizeWindowGeometry.DEBUG ? DragResizeWindowGeometry.EDGE_DEBUG_BUFFER : 0);
+ private static final int FINE_CORNER_SIZE = EDGE_RESIZE_THICKNESS * 2 + 10;
+ private static final int LARGE_CORNER_SIZE = FINE_CORNER_SIZE + 10;
+ private static final DragResizeWindowGeometry GEOMETRY = new DragResizeWindowGeometry(
+ TASK_CORNER_RADIUS, TASK_SIZE, EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE,
+ LARGE_CORNER_SIZE);
+ // Points in the edge resize handle. Note that coordinates start from the top left.
+ private static final Point TOP_EDGE_POINT = new Point(TASK_SIZE.getWidth() / 2,
+ -EDGE_RESIZE_THICKNESS / 2);
+ private static final Point LEFT_EDGE_POINT = new Point(-EDGE_RESIZE_THICKNESS / 2,
+ TASK_SIZE.getHeight() / 2);
+ private static final Point RIGHT_EDGE_POINT = new Point(
+ TASK_SIZE.getWidth() + EDGE_RESIZE_THICKNESS / 2, TASK_SIZE.getHeight() / 2);
+ private static final Point BOTTOM_EDGE_POINT = new Point(TASK_SIZE.getWidth() / 2,
+ TASK_SIZE.getHeight() + EDGE_RESIZE_THICKNESS / 2);
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+ /**
+ * Check that both groups of objects satisfy equals/hashcode within each group, and that each
+ * group is distinct from the next.
+ */
+ @Test
+ public void testEqualsAndHash() {
+ new EqualsTester()
+ .addEqualityGroup(
+ GEOMETRY,
+ new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
+ EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, LARGE_CORNER_SIZE))
+ .addEqualityGroup(
+ new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
+ EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, LARGE_CORNER_SIZE),
+ new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
+ EDGE_RESIZE_THICKNESS + 10, FINE_CORNER_SIZE, LARGE_CORNER_SIZE))
+ .addEqualityGroup(new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
+ EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, LARGE_CORNER_SIZE + 5),
+ new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
+ EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE, LARGE_CORNER_SIZE + 5))
+ .addEqualityGroup(new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
+ EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE + 4, LARGE_CORNER_SIZE),
+ new DragResizeWindowGeometry(TASK_CORNER_RADIUS, TASK_SIZE,
+ EDGE_RESIZE_THICKNESS, FINE_CORNER_SIZE + 4, LARGE_CORNER_SIZE))
+ .testEquals();
+ }
+
+ @Test
+ public void testGetTaskSize() {
+ assertThat(GEOMETRY.getTaskSize()).isEqualTo(TASK_SIZE);
+ }
+
+ @Test
+ public void testRegionUnionContainsEdges() {
+ Region region = new Region();
+ GEOMETRY.union(region);
+ assertThat(region.isComplex()).isTrue();
+ // Region excludes task area. Note that coordinates start from top left.
+ assertThat(region.contains(TASK_SIZE.getWidth() / 2, TASK_SIZE.getHeight() / 2)).isFalse();
+ // Region includes edges outside the task window.
+ verifyVerticalEdge(region, LEFT_EDGE_POINT);
+ verifyHorizontalEdge(region, TOP_EDGE_POINT);
+ verifyVerticalEdge(region, RIGHT_EDGE_POINT);
+ verifyHorizontalEdge(region, BOTTOM_EDGE_POINT);
+ }
+
+ private static void verifyHorizontalEdge(@NonNull Region region, @NonNull Point point) {
+ assertThat(region.contains(point.x, point.y)).isTrue();
+ // Horizontally along the edge is still contained.
+ assertThat(region.contains(point.x + EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isTrue();
+ assertThat(region.contains(point.x - EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isTrue();
+ // Vertically along the edge is not contained.
+ assertThat(region.contains(point.x, point.y - EDGE_RESIZE_DEBUG_THICKNESS)).isFalse();
+ assertThat(region.contains(point.x, point.y + EDGE_RESIZE_DEBUG_THICKNESS)).isFalse();
+ }
+
+ private static void verifyVerticalEdge(@NonNull Region region, @NonNull Point point) {
+ assertThat(region.contains(point.x, point.y)).isTrue();
+ // Horizontally along the edge is not contained.
+ assertThat(region.contains(point.x + EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isFalse();
+ assertThat(region.contains(point.x - EDGE_RESIZE_DEBUG_THICKNESS, point.y)).isFalse();
+ // Vertically along the edge is contained.
+ assertThat(region.contains(point.x, point.y - EDGE_RESIZE_DEBUG_THICKNESS)).isTrue();
+ assertThat(region.contains(point.x, point.y + EDGE_RESIZE_DEBUG_THICKNESS)).isTrue();
+ }
+
+ /**
+ * Validate that with the flag enabled, the corner resize regions are the largest size, to
+ * 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
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ public void testRegionUnion_edgeDragResizeEnabled_containsLargeCorners() {
+ Region region = new Region();
+ GEOMETRY.union(region);
+ // Make sure we're choosing a point outside of any debug region buffer.
+ final int cornerRadius = DragResizeWindowGeometry.DEBUG
+ ? Math.max(LARGE_CORNER_SIZE / 2, EDGE_RESIZE_DEBUG_THICKNESS)
+ : LARGE_CORNER_SIZE / 2;
+
+ new TestPoints(TASK_SIZE, cornerRadius).validateRegion(region);
+ }
+
+ /**
+ * Validate that with the flag disabled, the corner resize regions are the original smaller
+ * size.
+ */
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ public void testRegionUnion_edgeDragResizeDisabled_containsFineCorners() {
+ Region region = new Region();
+ GEOMETRY.union(region);
+ final int cornerRadius = DragResizeWindowGeometry.DEBUG
+ ? Math.max(LARGE_CORNER_SIZE / 2, EDGE_RESIZE_DEBUG_THICKNESS)
+ : LARGE_CORNER_SIZE / 2;
+
+ new TestPoints(TASK_SIZE, cornerRadius).validateRegion(region);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ public void testCalculateControlType_edgeDragResizeEnabled_edges() {
+ // 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
+ @DisableFlags(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ public void testCalculateControlType_edgeDragResizeDisabled_edges() {
+ // 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 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
+ @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).
+ // 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, /* 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
+ @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 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, /* 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);
+ }
+
+ /**
+ * Class for creating points for testing the drag resize corners.
+ *
+ * <p>Creates points that are both just within the bounds of each corner, and just outside.
+ */
+ private static final class TestPoints {
+ private final Point mTopLeftPoint;
+ private final Point mTopLeftPointOutside;
+ private final Point mTopRightPoint;
+ private final Point mTopRightPointOutside;
+ private final Point mBottomLeftPoint;
+ private final Point mBottomLeftPointOutside;
+ private final Point mBottomRightPoint;
+ private final Point mBottomRightPointOutside;
+
+ TestPoints(@NonNull Size taskSize, int cornerRadius) {
+ // Point just inside corner square is included.
+ mTopLeftPoint = new Point(-cornerRadius + 1, -cornerRadius + 1);
+ // Point just outside corner square is excluded.
+ mTopLeftPointOutside = new Point(mTopLeftPoint.x - 5, mTopLeftPoint.y - 5);
+
+ mTopRightPoint = new Point(taskSize.getWidth() + cornerRadius - 1, -cornerRadius + 1);
+ mTopRightPointOutside = new Point(mTopRightPoint.x + 5, mTopRightPoint.y - 5);
+
+ mBottomLeftPoint = new Point(-cornerRadius + 1,
+ taskSize.getHeight() + cornerRadius - 1);
+ mBottomLeftPointOutside = new Point(mBottomLeftPoint.x - 5, mBottomLeftPoint.y + 5);
+
+ mBottomRightPoint = new Point(taskSize.getWidth() + cornerRadius - 1,
+ taskSize.getHeight() + cornerRadius - 1);
+ mBottomRightPointOutside = new Point(mBottomRightPoint.x + 5, mBottomRightPoint.y + 5);
+ }
+
+ /**
+ * Validates that all test points are either within or without the given region.
+ */
+ public void validateRegion(@NonNull Region region) {
+ // Point just inside corner square is included.
+ assertThat(region.contains(mTopLeftPoint.x, mTopLeftPoint.y)).isTrue();
+ // Point just outside corner square is excluded.
+ assertThat(region.contains(mTopLeftPointOutside.x, mTopLeftPointOutside.y)).isFalse();
+
+ assertThat(region.contains(mTopRightPoint.x, mTopRightPoint.y)).isTrue();
+ assertThat(
+ region.contains(mTopRightPointOutside.x, mTopRightPointOutside.y)).isFalse();
+
+ assertThat(region.contains(mBottomLeftPoint.x, mBottomLeftPoint.y)).isTrue();
+ assertThat(region.contains(mBottomLeftPointOutside.x,
+ mBottomLeftPointOutside.y)).isFalse();
+
+ assertThat(region.contains(mBottomRightPoint.x, mBottomRightPoint.y)).isTrue();
+ assertThat(region.contains(mBottomRightPointOutside.x,
+ mBottomRightPointOutside.y)).isFalse();
+ }
+
+ /**
+ * Validates that all test points within this drag corner size give the correct
+ * {@code @DragPositioningCallback.CtrlType}.
+ */
+ public void validateCtrlTypeForInnerPoints(@NonNull DragResizeWindowGeometry geometry,
+ 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(isTouchscreen, isEdgeResizePermitted,
+ mTopRightPoint.x, mTopRightPoint.y)).isEqualTo(
+ expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED);
+ assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ mBottomLeftPoint.x, mBottomLeftPoint.y)).isEqualTo(
+ expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM
+ : CTRL_TYPE_UNDEFINED);
+ assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ mBottomRightPoint.x, mBottomRightPoint.y)).isEqualTo(
+ expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM
+ : CTRL_TYPE_UNDEFINED);
+ }
+
+ /**
+ * Validates that all test points outside this drag corner size give the correct
+ * {@code @DragPositioningCallback.CtrlType}.
+ */
+ public void validateCtrlTypeForOutsidePoints(@NonNull DragResizeWindowGeometry geometry,
+ 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(isTouchscreen, isEdgeResizePermitted,
+ mTopRightPointOutside.x, mTopRightPointOutside.y)).isEqualTo(
+ expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED);
+ assertThat(geometry.calculateCtrlType(isTouchscreen, isEdgeResizePermitted,
+ mBottomLeftPointOutside.x, mBottomLeftPointOutside.y)).isEqualTo(
+ expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM
+ : CTRL_TYPE_UNDEFINED);
+ 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 de6903d9a06a..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,9 @@ 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
import android.testing.AndroidTestingRunner
@@ -11,10 +14,12 @@ import android.view.Surface.ROTATION_270
import android.view.Surface.ROTATION_90
import android.view.SurfaceControl
import android.view.WindowManager
+import android.window.TransitionInfo
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
@@ -41,6 +46,8 @@ import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.doReturn
import java.util.function.Supplier
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
import org.mockito.Mockito.`when` as whenever
/**
@@ -79,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
@@ -115,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)
@@ -125,8 +141,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
mockWindowDecoration,
mockDisplayController,
mockDragStartListener,
- mockTransactionFactory,
- DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT
+ mockTransactionFactory
)
}
@@ -577,28 +592,29 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
}
@Test
- fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() {
- taskPositioner.onDragPositioningStart(
- CTRL_TYPE_UNDEFINED, // drag
- STARTING_BOUNDS.right.toFloat(),
- STARTING_BOUNDS.top.toFloat()
- )
-
- val newX = STARTING_BOUNDS.right.toFloat() + 5
- val newY = DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT.toFloat() - 1
- taskPositioner.onDragPositioningMove(
- newX,
- newY
- )
-
- taskPositioner.onDragPositioningEnd(newX, newY)
-
- verify(mockTransitions, never()).startTransition(
- eq(WindowManager.TRANSIT_CHANGE), argThat { wct ->
- return@argThat wct.changes.any { (token, change) ->
- token == taskBinder &&
- ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0)
- }}, eq(taskPositioner))
+ fun testStartAnimation_useEndRelOffset() {
+ val mockTransitionInfo = mock(TransitionInfo::class.java)
+ val changeMock = mock(TransitionInfo.Change::class.java)
+ val startTransaction = mock(SurfaceControl.Transaction::class.java)
+ val finishTransaction = mock(SurfaceControl.Transaction::class.java)
+ val point = Point(10, 20)
+ val bounds = Rect(1, 2, 3, 4)
+ `when`(changeMock.endRelOffset).thenReturn(point)
+ `when`(changeMock.endAbsBounds).thenReturn(bounds)
+ `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock))
+ `when`(startTransaction.setWindowCrop(any(),
+ eq(bounds.width()),
+ eq(bounds.height()))).thenReturn(startTransaction)
+ `when`(finishTransaction.setWindowCrop(any(),
+ eq(bounds.width()),
+ eq(bounds.height()))).thenReturn(finishTransaction)
+
+ taskPositioner.startAnimation(mockTransitionBinder, mockTransitionInfo, startTransaction,
+ finishTransaction, { _ -> })
+
+ verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat()))
+ verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat()))
+ verify(changeMock).endRelOffset
}
private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean {
@@ -656,70 +672,6 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
}
@Test
- fun testDragResize_drag_taskPositionedInStableBounds() {
- taskPositioner.onDragPositioningStart(
- CTRL_TYPE_UNDEFINED, // drag
- STARTING_BOUNDS.left.toFloat(),
- STARTING_BOUNDS.top.toFloat()
- )
-
- val newX = STARTING_BOUNDS.left.toFloat()
- val newY = STABLE_BOUNDS_LANDSCAPE.top.toFloat() - 5
- taskPositioner.onDragPositioningMove(
- newX,
- newY
- )
- verify(mockTransaction).setPosition(any(), eq(newX), eq(newY))
-
- taskPositioner.onDragPositioningEnd(
- newX,
- newY
- )
- // Verify task's top bound is set to stable bounds top since dragged outside stable bounds
- // but not in disallowed end bounds area.
- verify(mockTransitions).startTransition(
- eq(WindowManager.TRANSIT_CHANGE), argThat { wct ->
- return@argThat wct.changes.any { (token, change) ->
- token == taskBinder &&
- (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
- change.configuration.windowConfiguration.bounds.top ==
- STABLE_BOUNDS_LANDSCAPE.top
- }}, eq(taskPositioner))
- }
-
- @Test
- fun testDragResize_drag_taskPositionedInValidDragArea() {
- taskPositioner.onDragPositioningStart(
- CTRL_TYPE_UNDEFINED, // drag
- STARTING_BOUNDS.left.toFloat(),
- STARTING_BOUNDS.top.toFloat()
- )
-
- val newX = VALID_DRAG_AREA.left - 500f
- val newY = VALID_DRAG_AREA.bottom + 500f
- taskPositioner.onDragPositioningMove(
- newX,
- newY
- )
- verify(mockTransaction).setPosition(any(), eq(newX), eq(newY))
-
- taskPositioner.onDragPositioningEnd(
- newX,
- newY
- )
- verify(mockTransitions).startTransition(
- eq(WindowManager.TRANSIT_CHANGE), argThat { wct ->
- return@argThat wct.changes.any { (token, change) ->
- token == taskBinder &&
- (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
- change.configuration.windowConfiguration.bounds.top ==
- VALID_DRAG_AREA.bottom &&
- change.configuration.windowConfiguration.bounds.left ==
- VALID_DRAG_AREA.left
- }}, eq(taskPositioner))
- }
-
- @Test
fun testDragResize_drag_updatesStableBoundsOnRotate() {
// Test landscape stable bounds
performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(),
@@ -848,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
new file mode 100644
index 000000000000..a07be79579eb
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
@@ -0,0 +1,228 @@
+/*
+ * 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.graphics.Bitmap
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.Display
+import android.view.SurfaceControl
+import android.view.SurfaceControlViewHost
+import android.view.WindowlessWindowManager
+import androidx.test.filters.SmallTest
+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.DisplayController.OnDisplaysChangedListener
+import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Spy
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.verifyZeroInteractions
+import org.mockito.kotlin.whenever
+
+
+/**
+ * Tests for [ResizeVeil].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:ResizeVeilTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class ResizeVeilTest : ShellTestCase() {
+
+ @Mock
+ private lateinit var mockDisplayController: DisplayController
+ @Mock
+ private lateinit var mockAppIcon: Bitmap
+ @Mock
+ private lateinit var mockDisplay: Display
+ @Mock
+ private lateinit var mockSurfaceControlViewHost: SurfaceControlViewHost
+ @Mock
+ private lateinit var mockSurfaceControlBuilderFactory: ResizeVeil.SurfaceControlBuilderFactory
+ @Mock
+ private lateinit var mockSurfaceControlViewHostFactory: SurfaceControlViewHostFactory
+ @Spy
+ private val spyResizeVeilSurfaceBuilder = SurfaceControl.Builder()
+ @Mock
+ private lateinit var mockResizeVeilSurface: SurfaceControl
+ @Spy
+ private val spyBackgroundSurfaceBuilder = SurfaceControl.Builder()
+ @Mock
+ private lateinit var mockBackgroundSurface: SurfaceControl
+ @Spy
+ private val spyIconSurfaceBuilder = SurfaceControl.Builder()
+ @Mock
+ private lateinit var mockIconSurface: SurfaceControl
+ @Mock
+ private lateinit var mockTransaction: SurfaceControl.Transaction
+
+ private val taskInfo = TestRunningTaskInfoBuilder().build()
+
+ @Before
+ fun setUp() {
+ whenever(mockSurfaceControlViewHostFactory.create(any(), any(), any(), any()))
+ .thenReturn(mockSurfaceControlViewHost)
+ whenever(mockSurfaceControlBuilderFactory
+ .create("Resize veil of Task=" + taskInfo.taskId))
+ .thenReturn(spyResizeVeilSurfaceBuilder)
+ doReturn(mockResizeVeilSurface).whenever(spyResizeVeilSurfaceBuilder).build()
+ whenever(mockSurfaceControlBuilderFactory
+ .create(eq("Resize veil background of Task=" + taskInfo.taskId), any()))
+ .thenReturn(spyBackgroundSurfaceBuilder)
+ doReturn(mockBackgroundSurface).whenever(spyBackgroundSurfaceBuilder).build()
+ whenever(mockSurfaceControlBuilderFactory
+ .create("Resize veil icon of Task=" + taskInfo.taskId))
+ .thenReturn(spyIconSurfaceBuilder)
+ doReturn(mockIconSurface).whenever(spyIconSurfaceBuilder).build()
+
+ doReturn(mockTransaction).whenever(mockTransaction).setLayer(any(), anyInt())
+ doReturn(mockTransaction).whenever(mockTransaction).setAlpha(any(), anyFloat())
+ doReturn(mockTransaction).whenever(mockTransaction).show(any())
+ doReturn(mockTransaction).whenever(mockTransaction).hide(any())
+ doReturn(mockTransaction).whenever(mockTransaction)
+ .setPosition(any(), anyFloat(), anyFloat())
+ doReturn(mockTransaction).whenever(mockTransaction).setWindowCrop(any(), anyInt(), anyInt())
+ }
+
+ @Test
+ fun init_displayAvailable_viewHostCreated() {
+ createResizeVeil(withDisplayAvailable = true)
+
+ verify(mockSurfaceControlViewHostFactory)
+ .create(any(), eq(mockDisplay), any(), eq("ResizeVeil"))
+ }
+
+ @Test
+ fun init_displayUnavailable_viewHostNotCreatedUntilDisplayAppears() {
+ createResizeVeil(withDisplayAvailable = false)
+
+ verify(mockSurfaceControlViewHostFactory, never())
+ .create(any(), eq(mockDisplay), any<WindowlessWindowManager>(), eq("ResizeVeil"))
+ val captor = ArgumentCaptor.forClass(OnDisplaysChangedListener::class.java)
+ verify(mockDisplayController).addDisplayWindowListener(captor.capture())
+
+ whenever(mockDisplayController.getDisplay(taskInfo.displayId)).thenReturn(mockDisplay)
+ captor.value.onDisplayAdded(taskInfo.displayId)
+
+ verify(mockSurfaceControlViewHostFactory)
+ .create(any(), eq(mockDisplay), any(), eq("ResizeVeil"))
+ verify(mockDisplayController).removeDisplayWindowListener(any())
+ }
+
+ @Test
+ fun dispose_removesDisplayWindowListener() {
+ createResizeVeil().dispose()
+
+ verify(mockDisplayController).removeDisplayWindowListener(any())
+ }
+
+ @Test
+ fun showVeil() {
+ val veil = createResizeVeil()
+
+ veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */)
+
+ verify(mockTransaction).show(mockResizeVeilSurface)
+ verify(mockTransaction).show(mockBackgroundSurface)
+ verify(mockTransaction).show(mockIconSurface)
+ verify(mockTransaction).apply()
+ }
+
+ @Test
+ fun showVeil_displayUnavailable_doesNotShow() {
+ val veil = createResizeVeil(withDisplayAvailable = false)
+
+ veil.showVeil(mockTransaction, mock(), Rect(0, 0, 100, 100), taskInfo, false /* fadeIn */)
+
+ verify(mockTransaction, never()).show(mockResizeVeilSurface)
+ verify(mockTransaction, never()).show(mockBackgroundSurface)
+ verify(mockTransaction, never()).show(mockIconSurface)
+ verify(mockTransaction).apply()
+ }
+
+ @Test
+ fun showVeil_alreadyVisible_doesNotShowAgain() {
+ val veil = createResizeVeil()
+
+ 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)
+ verify(mockTransaction, times(1)).show(mockIconSurface)
+ verify(mockTransaction, times(2)).apply()
+ }
+
+ @Test
+ fun showVeil_reparentsVeilToNewParent() {
+ val veil = createResizeVeil(parent = mock())
+
+ val newParent = mock<SurfaceControl>()
+ veil.showVeil(
+ mockTransaction,
+ newParent,
+ Rect(0, 0, 100, 100),
+ taskInfo,
+ false /* fadeIn */
+ )
+
+ verify(mockTransaction).reparent(mockResizeVeilSurface, newParent)
+ }
+
+ @Test
+ fun hideVeil_alreadyHidden_doesNothing() {
+ val veil = createResizeVeil()
+
+ veil.hideVeil()
+
+ verifyZeroInteractions(mockTransaction)
+ }
+
+ private fun createResizeVeil(
+ withDisplayAvailable: Boolean = true,
+ parent: SurfaceControl = mock()
+ ): ResizeVeil {
+ whenever(mockDisplayController.getDisplay(taskInfo.displayId))
+ .thenReturn(if (withDisplayAvailable) mockDisplay else null)
+ return ResizeVeil(
+ context,
+ mockDisplayController,
+ mockAppIcon,
+ parent,
+ { mockTransaction },
+ mockSurfaceControlBuilderFactory,
+ 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 86253f35a51d..48ac1e5717aa 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,7 @@ package com.android.wm.shell.windowdecor
import android.app.ActivityManager
import android.app.WindowConfiguration
+import android.graphics.Point
import android.graphics.Rect
import android.os.IBinder
import android.testing.AndroidTestingRunner
@@ -25,6 +26,7 @@ import android.view.Surface.ROTATION_0
import android.view.Surface.ROTATION_270
import android.view.Surface.ROTATION_90
import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_CHANGE
import android.window.TransitionInfo
import android.window.WindowContainerToken
@@ -39,6 +41,7 @@ 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.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED
+import java.util.function.Supplier
import junit.framework.Assert
import org.junit.Before
import org.junit.Test
@@ -47,13 +50,13 @@ import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.argThat
import org.mockito.Mockito.eq
+import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
-import org.mockito.MockitoAnnotations
-import java.util.function.Supplier
import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
/**
* Tests for [VeiledResizeTaskPositioner].
@@ -138,8 +141,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
mockDisplayController,
mockDragStartListener,
mockTransactionFactory,
- mockTransitions,
- DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT
+ mockTransitions
)
}
@@ -195,7 +197,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
rectAfterEnd.top += 20
rectAfterEnd.bottom += 20
- verify(mockDesktopWindowDecoration, never()).createResizeVeil()
+ verify(mockDesktopWindowDecoration, never()).showResizeVeil(any())
verify(mockDesktopWindowDecoration, never()).hideResizeVeil()
verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
@@ -355,68 +357,6 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
}
@Test
- fun testDragResize_drag_taskPositionedInStableBounds() {
- taskPositioner.onDragPositioningStart(
- CTRL_TYPE_UNDEFINED, // drag
- STARTING_BOUNDS.left.toFloat(),
- STARTING_BOUNDS.top.toFloat()
- )
-
- val newX = STARTING_BOUNDS.left.toFloat()
- val newY = STABLE_BOUNDS_LANDSCAPE.top.toFloat() - 5
- taskPositioner.onDragPositioningMove(
- newX,
- newY
- )
- verify(mockTransaction).setPosition(any(), eq(newX), eq(newY))
-
- taskPositioner.onDragPositioningEnd(
- newX,
- newY
- )
- // Verify task's top bound is set to stable bounds top since dragged outside stable bounds
- // but not in disallowed end bounds area.
- verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
- return@argThat wct.changes.any { (token, change) ->
- token == taskBinder &&
- (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
- change.configuration.windowConfiguration.bounds.top ==
- STABLE_BOUNDS_LANDSCAPE.top }},
- eq(taskPositioner))
- }
-
- @Test
- fun testDragResize_drag_taskPositionedInValidDragArea() {
- taskPositioner.onDragPositioningStart(
- CTRL_TYPE_UNDEFINED, // drag
- STARTING_BOUNDS.left.toFloat(),
- STARTING_BOUNDS.top.toFloat()
- )
-
- val newX = VALID_DRAG_AREA.left - 500f
- val newY = VALID_DRAG_AREA.bottom + 500f
- taskPositioner.onDragPositioningMove(
- newX,
- newY
- )
- verify(mockTransaction).setPosition(any(), eq(newX), eq(newY))
-
- taskPositioner.onDragPositioningEnd(
- newX,
- newY
- )
- verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct ->
- return@argThat wct.changes.any { (token, change) ->
- token == taskBinder &&
- (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
- change.configuration.windowConfiguration.bounds.top ==
- VALID_DRAG_AREA.bottom &&
- change.configuration.windowConfiguration.bounds.left ==
- VALID_DRAG_AREA.left }},
- eq(taskPositioner))
- }
-
- @Test
fun testDragResize_drag_updatesStableBoundsOnRotate() {
// Test landscape stable bounds
performDrag(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.bottom.toFloat(),
@@ -502,6 +442,40 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() {
Assert.assertFalse(taskPositioner.isResizingOrAnimating)
}
+ @Test
+ fun testStartAnimation_useEndRelOffset() {
+ val changeMock = mock(TransitionInfo.Change::class.java)
+ val startTransaction = mock(Transaction::class.java)
+ val finishTransaction = mock(Transaction::class.java)
+ val point = Point(10, 20)
+ val bounds = Rect(1, 2, 3, 4)
+ `when`(changeMock.endRelOffset).thenReturn(point)
+ `when`(changeMock.endAbsBounds).thenReturn(bounds)
+ `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock))
+ `when`(startTransaction.setWindowCrop(
+ any(),
+ eq(bounds.width()),
+ eq(bounds.height())
+ )).thenReturn(startTransaction)
+ `when`(finishTransaction.setWindowCrop(
+ any(),
+ eq(bounds.width()),
+ eq(bounds.height())
+ )).thenReturn(finishTransaction)
+
+ taskPositioner.startAnimation(
+ mockTransitionBinder,
+ mockTransitionInfo,
+ startTransaction,
+ finishTransaction,
+ mockFinishCallback
+ )
+
+ verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat()))
+ verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat()))
+ verify(changeMock).endRelOffset
+ }
+
private fun performDrag(
startX: Float,
startY: Float,
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 228b25ccb1ba..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;
@@ -42,13 +43,13 @@ import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.same;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.quality.Strictness.LENIENT;
import android.app.ActivityManager;
import android.content.Context;
-import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Point;
@@ -61,10 +62,10 @@ import android.view.InsetsState;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
import android.view.View;
-import android.view.ViewRootImpl;
import android.view.WindowInsets;
import android.view.WindowManager.LayoutParams;
import android.window.SurfaceSyncGroup;
+import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import androidx.test.filters.SmallTest;
@@ -74,8 +75,9 @@ import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestRunningTaskInfoBuilder;
import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.desktopmode.DesktopModeStatus;
+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;
@@ -133,7 +135,6 @@ public class WindowDecorationTests extends ShellTestCase {
private SurfaceControl.Transaction mMockSurfaceControlFinishT;
private SurfaceControl.Transaction mMockSurfaceControlAddWindowT;
private WindowDecoration.RelayoutParams mRelayoutParams = new WindowDecoration.RelayoutParams();
- private Configuration mWindowConfiguration = new Configuration();
private int mCaptionMenuWidthId;
@Before
@@ -252,16 +253,14 @@ public class WindowDecorationTests extends ShellTestCase {
argThat(lp -> lp.height == 64
&& lp.width == 300
&& (lp.flags & LayoutParams.FLAG_NOT_FOCUSABLE) != 0));
- if (ViewRootImpl.CAPTION_ON_SHELL) {
- verify(mMockView).setTaskFocusState(true);
- verify(mMockWindowContainerTransaction).addInsetsSource(
- eq(taskInfo.token),
- any(),
- eq(0 /* index */),
- eq(WindowInsets.Type.captionBar()),
- eq(new Rect(100, 300, 400, 364)),
- any());
- }
+ verify(mMockView).setTaskFocusState(true);
+ verify(mMockWindowContainerTransaction).addInsetsSource(
+ eq(taskInfo.token),
+ any(),
+ eq(0 /* index */),
+ eq(WindowInsets.Type.captionBar()),
+ eq(new Rect(100, 300, 400, 364)),
+ any());
verify(mMockSurfaceControlStartT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS);
verify(mMockSurfaceControlFinishT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS);
@@ -304,7 +303,6 @@ public class WindowDecorationTests extends ShellTestCase {
taskInfo.isFocused = true;
// Density is 2. Shadow radius is 10px. Caption height is 64px.
taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2;
- mWindowConfiguration.densityDpi = taskInfo.configuration.densityDpi;
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
@@ -315,14 +313,16 @@ public class WindowDecorationTests extends ShellTestCase {
verify(mMockWindowContainerTransaction, never())
.removeInsetsSource(eq(taskInfo.token), any(), anyInt(), anyInt());
+ final SurfaceControl.Transaction t2 = mock(SurfaceControl.Transaction.class);
+ mMockSurfaceControlTransactions.add(t2);
taskInfo.isVisible = false;
windowDecor.relayout(taskInfo);
- final InOrder releaseOrder = inOrder(t, mMockSurfaceControlViewHost);
+ final InOrder releaseOrder = inOrder(t2, mMockSurfaceControlViewHost);
releaseOrder.verify(mMockSurfaceControlViewHost).release();
- releaseOrder.verify(t).remove(captionContainerSurface);
- releaseOrder.verify(t).remove(decorContainerSurface);
- releaseOrder.verify(t).apply();
+ releaseOrder.verify(t2).remove(captionContainerSurface);
+ releaseOrder.verify(t2).remove(decorContainerSurface);
+ releaseOrder.verify(t2).apply();
// Expect to remove two insets sources, the caption insets and the mandatory gesture insets.
verify(mMockWindowContainerTransaction, Mockito.times(2))
.removeInsetsSource(eq(taskInfo.token), any(), anyInt(), anyInt());
@@ -373,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);
@@ -395,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)
@@ -409,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);
@@ -423,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
@@ -613,26 +608,86 @@ public class WindowDecorationTests extends ShellTestCase {
mockitoSession.finishMocking();
}
+ @Test
+ public void testRelayout_captionHidden_insetsRemoved() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setVisible(true)
+ .setBounds(new Rect(0, 0, 1000, 1000))
+ .build();
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+
+ // Run it once so that insets are added.
+ mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true);
+ windowDecor.relayout(taskInfo);
+
+ // Run it again so that insets are removed.
+ mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(false);
+ windowDecor.relayout(taskInfo);
+
+ verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
+ eq(0) /* index */, eq(captionBar()));
+ verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
+ eq(0) /* index */, eq(mandatorySystemGestures()));
+ }
@Test
- public void testInsetsRemovedWhenCaptionIsHidden() {
+ public void testRelayout_captionHidden_neverWasVisible_insetsNotRemoved() {
final Display defaultDisplay = mock(Display.class);
doReturn(defaultDisplay).when(mMockDisplayController)
.getDisplay(Display.DEFAULT_DISPLAY);
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setVisible(true)
+ .setBounds(new Rect(0, 0, 1000, 1000))
+ .build();
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+
+ // Hidden from the beginning, so no insets were ever added.
mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(false);
+ windowDecor.relayout(taskInfo);
+
+ // Never added.
+ verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(),
+ eq(0) /* index */, eq(captionBar()), any(), any());
+ verify(mMockWindowContainerTransaction, never()).addInsetsSource(eq(taskInfo.token), any(),
+ eq(0) /* index */, eq(mandatorySystemGestures()), any(), any());
+ // No need to remove them if they were never added.
+ verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token),
+ any(), eq(0) /* index */, eq(captionBar()));
+ verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token),
+ any(), eq(0) /* index */, eq(mandatorySystemGestures()));
+ }
+
+ @Test
+ public void testClose_withExistingInsets_insetsRemoved() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
- final ActivityManager.TaskDescription.Builder taskDescriptionBuilder =
- new ActivityManager.TaskDescription.Builder();
final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
.setDisplayId(Display.DEFAULT_DISPLAY)
- .setTaskDescriptionBuilder(taskDescriptionBuilder)
.setVisible(true)
+ .setBounds(new Rect(0, 0, 1000, 1000))
.build();
final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+ // Relayout will add insets.
+ mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true);
windowDecor.relayout(taskInfo);
+ verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
+ eq(0) /* index */, eq(captionBar()), any(), any());
+ verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
+ eq(0) /* index */, eq(mandatorySystemGestures()), any(), any());
+
+ windowDecor.close();
+ // Insets should be removed.
verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
eq(0) /* index */, eq(captionBar()));
verify(mMockWindowContainerTransaction).removeInsetsSource(eq(taskInfo.token), any(),
@@ -640,6 +695,82 @@ public class WindowDecorationTests extends ShellTestCase {
}
@Test
+ public void testClose_withoutExistingInsets_insetsNotRemoved() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+
+ final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setVisible(true)
+ .setBounds(new Rect(0, 0, 1000, 1000))
+ .build();
+ final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo);
+
+ windowDecor.close();
+
+ // No need to remove insets.
+ verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token),
+ any(), eq(0) /* index */, eq(captionBar()));
+ verify(mMockWindowContainerTransaction, never()).removeInsetsSource(eq(taskInfo.token),
+ any(), eq(0) /* index */, eq(mandatorySystemGestures()));
+ }
+
+ @Test
+ public void testRelayout_captionFrameChanged_insetsReapplied() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+ mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true);
+ final WindowContainerToken token = TestRunningTaskInfoBuilder.createMockWCToken();
+ final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setVisible(true);
+
+ // Relayout twice with different bounds.
+ final ActivityManager.RunningTaskInfo firstTaskInfo =
+ builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
+ final TestWindowDecoration windowDecor = createWindowDecoration(firstTaskInfo);
+ windowDecor.relayout(firstTaskInfo);
+ final ActivityManager.RunningTaskInfo secondTaskInfo =
+ builder.setToken(token).setBounds(new Rect(50, 50, 1000, 1000)).build();
+ windowDecor.relayout(secondTaskInfo);
+
+ // Insets should be applied twice.
+ verify(mMockWindowContainerTransaction, times(2)).addInsetsSource(eq(token), any(),
+ eq(0) /* index */, eq(captionBar()), any(), any());
+ verify(mMockWindowContainerTransaction, times(2)).addInsetsSource(eq(token), any(),
+ eq(0) /* index */, eq(mandatorySystemGestures()), any(), any());
+ }
+
+ @Test
+ public void testRelayout_captionFrameUnchanged_insetsNotApplied() {
+ final Display defaultDisplay = mock(Display.class);
+ doReturn(defaultDisplay).when(mMockDisplayController)
+ .getDisplay(Display.DEFAULT_DISPLAY);
+ mInsetsState.getOrCreateSource(STATUS_BAR_INSET_SOURCE_ID, captionBar()).setVisible(true);
+ final WindowContainerToken token = TestRunningTaskInfoBuilder.createMockWCToken();
+ final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder()
+ .setDisplayId(Display.DEFAULT_DISPLAY)
+ .setVisible(true);
+
+ // Relayout twice with the same bounds.
+ final ActivityManager.RunningTaskInfo firstTaskInfo =
+ builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
+ final TestWindowDecoration windowDecor = createWindowDecoration(firstTaskInfo);
+ windowDecor.relayout(firstTaskInfo);
+ final ActivityManager.RunningTaskInfo secondTaskInfo =
+ builder.setToken(token).setBounds(new Rect(0, 0, 1000, 1000)).build();
+ windowDecor.relayout(secondTaskInfo);
+
+ // Insets should only need to be applied once.
+ verify(mMockWindowContainerTransaction, times(1)).addInsetsSource(eq(token), any(),
+ eq(0) /* index */, eq(captionBar()), any(), any());
+ verify(mMockWindowContainerTransaction, times(1)).addInsetsSource(eq(token), any(),
+ eq(0) /* index */, eq(mandatorySystemGestures()), any(), any());
+ }
+
+ @Test
public void testTaskPositionAndCropNotSetWhenFalse() {
final Display defaultDisplay = mock(Display.class);
doReturn(defaultDisplay).when(mMockDisplayController)
@@ -698,10 +829,40 @@ 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,
- taskInfo, mMockTaskSurface, mWindowConfiguration,
+ taskInfo, mMockTaskSurface,
new MockObjectSupplier<>(mMockSurfaceControlBuilders,
() -> createMockSurfaceControlBuilder(mock(SurfaceControl.class))),
new MockObjectSupplier<>(mMockSurfaceControlTransactions,
@@ -742,16 +903,15 @@ public class WindowDecorationTests extends ShellTestCase {
TestWindowDecoration(Context context, DisplayController displayController,
ShellTaskOrganizer taskOrganizer, ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl taskSurface,
- Configuration windowConfiguration,
Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
Supplier<WindowContainerTransaction> windowContainerTransactionSupplier,
Supplier<SurfaceControl> surfaceControlSupplier,
SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
super(context, displayController, taskOrganizer, taskInfo, taskSurface,
- windowConfiguration, surfaceControlBuilderSupplier,
- surfaceControlTransactionSupplier, windowContainerTransactionSupplier,
- surfaceControlSupplier, surfaceControlViewHostFactory);
+ surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
+ windowContainerTransactionSupplier, surfaceControlSupplier,
+ surfaceControlViewHostFactory);
}
@Override
@@ -766,21 +926,22 @@ public class WindowDecorationTests extends ShellTestCase {
void relayout(ActivityManager.RunningTaskInfo taskInfo,
boolean applyStartTransactionOnDraw) {
+ mRelayoutParams.mRunningTaskInfo = taskInfo;
mRelayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
relayout(mRelayoutParams, mMockSurfaceControlStartT, mMockSurfaceControlFinishT,
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/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/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/dream/lowlight/tests/Android.bp b/libs/dream/lowlight/tests/Android.bp
index 4dafd0aa6df4..42547832133b 100644
--- a/libs/dream/lowlight/tests/Android.bp
+++ b/libs/dream/lowlight/tests/Android.bp
@@ -27,7 +27,7 @@ android_test {
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.ext.junit",
- "animationlib",
+ "//frameworks/libs/systemui:animationlib",
"frameworks-base-testutils",
"junit",
"kotlinx_coroutines_test",
diff --git a/libs/hostgraphics/ADisplay.cpp b/libs/hostgraphics/ADisplay.cpp
index 9cc1f40184e3..58fa08281a61 100644
--- a/libs/hostgraphics/ADisplay.cpp
+++ b/libs/hostgraphics/ADisplay.cpp
@@ -94,14 +94,14 @@ namespace android {
int ADisplay_acquirePhysicalDisplays(ADisplay*** outDisplays) {
// This is running on host, so there are no physical displays available.
// Create 1 fake display instead.
- DisplayImpl** const impls = reinterpret_cast<DisplayImpl**>(
- malloc(sizeof(DisplayImpl*) + sizeof(DisplayImpl)));
+ DisplayImpl** const impls =
+ reinterpret_cast<DisplayImpl**>(malloc(sizeof(DisplayImpl*) + sizeof(DisplayImpl)));
DisplayImpl* const displayData = reinterpret_cast<DisplayImpl*>(impls + 1);
- displayData[0] = DisplayImpl{ADisplayType::DISPLAY_TYPE_INTERNAL,
- ADataSpace::ADATASPACE_UNKNOWN,
- AHardwareBuffer_Format::AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM,
- DisplayConfigImpl()};
+ displayData[0] =
+ DisplayImpl{ADisplayType::DISPLAY_TYPE_INTERNAL, ADataSpace::ADATASPACE_UNKNOWN,
+ AHardwareBuffer_Format::AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM,
+ DisplayConfigImpl()};
impls[0] = displayData;
*outDisplays = reinterpret_cast<ADisplay**>(impls);
return 1;
diff --git a/libs/hostgraphics/ANativeWindow.cpp b/libs/hostgraphics/ANativeWindow.cpp
new file mode 100644
index 000000000000..fcfaf0235293
--- /dev/null
+++ b/libs/hostgraphics/ANativeWindow.cpp
@@ -0,0 +1,106 @@
+/*
+ * 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 <system/window.h>
+
+static int32_t query(ANativeWindow* window, int what) {
+ int value;
+ int res = window->query(window, what, &value);
+ return res < 0 ? res : value;
+}
+
+static int64_t query64(ANativeWindow* window, int what) {
+ int64_t value;
+ int res = window->perform(window, what, &value);
+ return res < 0 ? res : value;
+}
+
+int ANativeWindow_setCancelBufferInterceptor(ANativeWindow* window,
+ ANativeWindow_cancelBufferInterceptor interceptor,
+ void* data) {
+ return window->perform(window, NATIVE_WINDOW_SET_CANCEL_INTERCEPTOR, interceptor, data);
+}
+
+int ANativeWindow_setDequeueBufferInterceptor(ANativeWindow* window,
+ ANativeWindow_dequeueBufferInterceptor interceptor,
+ void* data) {
+ return window->perform(window, NATIVE_WINDOW_SET_DEQUEUE_INTERCEPTOR, interceptor, data);
+}
+
+int ANativeWindow_setQueueBufferInterceptor(ANativeWindow* window,
+ ANativeWindow_queueBufferInterceptor interceptor,
+ void* data) {
+ return window->perform(window, NATIVE_WINDOW_SET_QUEUE_INTERCEPTOR, interceptor, data);
+}
+
+int ANativeWindow_setPerformInterceptor(ANativeWindow* window,
+ ANativeWindow_performInterceptor interceptor, void* data) {
+ return window->perform(window, NATIVE_WINDOW_SET_PERFORM_INTERCEPTOR, interceptor, data);
+}
+
+int ANativeWindow_dequeueBuffer(ANativeWindow* window, ANativeWindowBuffer** buffer, int* fenceFd) {
+ return window->dequeueBuffer(window, buffer, fenceFd);
+}
+
+int ANativeWindow_cancelBuffer(ANativeWindow* window, ANativeWindowBuffer* buffer, int fenceFd) {
+ return window->cancelBuffer(window, buffer, fenceFd);
+}
+
+int ANativeWindow_setDequeueTimeout(ANativeWindow* window, int64_t timeout) {
+ return window->perform(window, NATIVE_WINDOW_SET_DEQUEUE_TIMEOUT, timeout);
+}
+
+// extern "C", so that it can be used outside libhostgraphics (in host hwui/.../CanvasContext.cpp)
+extern "C" void ANativeWindow_tryAllocateBuffers(ANativeWindow* window) {
+ if (!window || !query(window, NATIVE_WINDOW_IS_VALID)) {
+ return;
+ }
+ window->perform(window, NATIVE_WINDOW_ALLOCATE_BUFFERS);
+}
+
+int64_t ANativeWindow_getLastDequeueStartTime(ANativeWindow* window) {
+ return query64(window, NATIVE_WINDOW_GET_LAST_DEQUEUE_START);
+}
+
+int64_t ANativeWindow_getLastDequeueDuration(ANativeWindow* window) {
+ return query64(window, NATIVE_WINDOW_GET_LAST_DEQUEUE_DURATION);
+}
+
+int64_t ANativeWindow_getLastQueueDuration(ANativeWindow* window) {
+ return query64(window, NATIVE_WINDOW_GET_LAST_QUEUE_DURATION);
+}
+
+int32_t ANativeWindow_getWidth(ANativeWindow* window) {
+ return query(window, NATIVE_WINDOW_WIDTH);
+}
+
+int32_t ANativeWindow_getHeight(ANativeWindow* window) {
+ return query(window, NATIVE_WINDOW_HEIGHT);
+}
+
+int32_t ANativeWindow_getFormat(ANativeWindow* window) {
+ return query(window, NATIVE_WINDOW_FORMAT);
+}
+
+void ANativeWindow_acquire(ANativeWindow* window) {
+ // incStrong/decStrong token must be the same, doesn't matter what it is
+ window->incStrong((void*)ANativeWindow_acquire);
+}
+
+void ANativeWindow_release(ANativeWindow* window) {
+ // incStrong/decStrong token must be the same, doesn't matter what it is
+ window->decStrong((void*)ANativeWindow_acquire);
+}
diff --git a/libs/hostgraphics/Android.bp b/libs/hostgraphics/Android.bp
index 4407af68de99..09232b64616d 100644
--- a/libs/hostgraphics/Android.bp
+++ b/libs/hostgraphics/Android.bp
@@ -17,26 +17,18 @@ cc_library_host_static {
static_libs: [
"libbase",
"libmath",
+ "libui-types",
"libutils",
],
srcs: [
- ":libui_host_common",
"ADisplay.cpp",
+ "ANativeWindow.cpp",
"Fence.cpp",
"HostBufferQueue.cpp",
"PublicFormat.cpp",
],
- include_dirs: [
- // Here we override all the headers automatically included with frameworks/native/include.
- // When frameworks/native/include will be removed from the list of automatic includes.
- // We will have to copy necessary headers with a pre-build step (generated headers).
- ".",
- "frameworks/native/libs/arect/include",
- "frameworks/native/libs/ui/include_private",
- ],
-
header_libs: [
"libnativebase_headers",
"libnativedisplay_headers",
diff --git a/libs/hostgraphics/Fence.cpp b/libs/hostgraphics/Fence.cpp
index 9e54816651c4..4383bf02a00e 100644
--- a/libs/hostgraphics/Fence.cpp
+++ b/libs/hostgraphics/Fence.cpp
@@ -20,4 +20,4 @@ namespace android {
const sp<Fence> Fence::NO_FENCE = sp<Fence>(new Fence);
-} // namespace android \ No newline at end of file
+} // namespace android
diff --git a/libs/hostgraphics/HostBufferQueue.cpp b/libs/hostgraphics/HostBufferQueue.cpp
index ec304378c6c4..7e14b88a47fa 100644
--- a/libs/hostgraphics/HostBufferQueue.cpp
+++ b/libs/hostgraphics/HostBufferQueue.cpp
@@ -15,18 +15,26 @@
*/
#include <gui/BufferQueue.h>
+#include <system/window.h>
namespace android {
class HostBufferQueue : public IGraphicBufferProducer, public IGraphicBufferConsumer {
public:
- HostBufferQueue() : mWidth(0), mHeight(0) { }
+ HostBufferQueue() : mWidth(0), mHeight(0) {}
- virtual status_t setConsumerIsProtected(bool isProtected) { return OK; }
+ // Consumer
+ virtual status_t setConsumerIsProtected(bool isProtected) {
+ return OK;
+ }
- virtual status_t detachBuffer(int slot) { return OK; }
+ virtual status_t detachBuffer(int slot) {
+ return OK;
+ }
- virtual status_t getReleasedBuffers(uint64_t* slotMask) { return OK; }
+ virtual status_t getReleasedBuffers(uint64_t* slotMask) {
+ return OK;
+ }
virtual status_t setDefaultBufferSize(uint32_t w, uint32_t h) {
mWidth = w;
@@ -35,22 +43,54 @@ public:
return OK;
}
- virtual status_t setDefaultBufferFormat(PixelFormat defaultFormat) { return OK; }
+ virtual status_t setDefaultBufferFormat(PixelFormat defaultFormat) {
+ return OK;
+ }
- virtual status_t setDefaultBufferDataSpace(android_dataspace defaultDataSpace) { return OK; }
+ virtual status_t setDefaultBufferDataSpace(android_dataspace defaultDataSpace) {
+ return OK;
+ }
- virtual status_t discardFreeBuffers() { return OK; }
+ virtual status_t discardFreeBuffers() {
+ return OK;
+ }
virtual status_t acquireBuffer(BufferItem* buffer, nsecs_t presentWhen,
- uint64_t maxFrameNumber = 0) {
+ uint64_t maxFrameNumber = 0) {
buffer->mGraphicBuffer = mBuffer;
buffer->mSlot = 0;
return OK;
}
- virtual status_t setMaxAcquiredBufferCount(int maxAcquiredBuffers) { return OK; }
+ virtual status_t setMaxAcquiredBufferCount(int maxAcquiredBuffers) {
+ return OK;
+ }
+
+ virtual status_t setConsumerUsageBits(uint64_t usage) {
+ return OK;
+ }
+
+ // Producer
+ virtual int query(int what, int* value) {
+ switch (what) {
+ case NATIVE_WINDOW_WIDTH:
+ *value = mWidth;
+ break;
+ case NATIVE_WINDOW_HEIGHT:
+ *value = mHeight;
+ break;
+ default:
+ *value = 0;
+ break;
+ }
+ return OK;
+ }
+
+ virtual status_t requestBuffer(int slot, sp<GraphicBuffer>* buf) {
+ *buf = mBuffer;
+ return OK;
+ }
- virtual status_t setConsumerUsageBits(uint64_t usage) { return OK; }
private:
sp<GraphicBuffer> mBuffer;
uint32_t mWidth;
@@ -58,8 +98,7 @@ private:
};
void BufferQueue::createBufferQueue(sp<IGraphicBufferProducer>* outProducer,
- sp<IGraphicBufferConsumer>* outConsumer) {
-
+ sp<IGraphicBufferConsumer>* outConsumer) {
sp<HostBufferQueue> obj(new HostBufferQueue());
*outProducer = obj;
diff --git a/libs/hostgraphics/PublicFormat.cpp b/libs/hostgraphics/PublicFormat.cpp
index af6d2738c801..2a2eec63467c 100644
--- a/libs/hostgraphics/PublicFormat.cpp
+++ b/libs/hostgraphics/PublicFormat.cpp
@@ -30,4 +30,4 @@ PublicFormat mapHalFormatDataspaceToPublicFormat(int format, android_dataspace d
return static_cast<PublicFormat>(format);
}
-} // namespace android \ No newline at end of file
+} // namespace android
diff --git a/libs/hostgraphics/gui/BufferItem.h b/libs/hostgraphics/gui/BufferItem.h
index 01409e19c715..e95a9231dfaf 100644
--- a/libs/hostgraphics/gui/BufferItem.h
+++ b/libs/hostgraphics/gui/BufferItem.h
@@ -17,16 +17,15 @@
#ifndef ANDROID_GUI_BUFFERITEM_H
#define ANDROID_GUI_BUFFERITEM_H
+#include <system/graphics.h>
#include <ui/Fence.h>
#include <ui/Rect.h>
-
-#include <system/graphics.h>
-
#include <utils/StrongPointer.h>
namespace android {
class Fence;
+
class GraphicBuffer;
// The only thing we need here for layoutlib is mGraphicBuffer. The rest of the fields are added
@@ -37,6 +36,7 @@ public:
enum { INVALID_BUFFER_SLOT = -1 };
BufferItem() : mGraphicBuffer(nullptr), mFence(Fence::NO_FENCE) {}
+
~BufferItem() {}
sp<GraphicBuffer> mGraphicBuffer;
@@ -60,6 +60,6 @@ public:
bool mTransformToDisplayInverse;
};
-}
+} // namespace android
#endif // ANDROID_GUI_BUFFERITEM_H
diff --git a/libs/hostgraphics/gui/BufferItemConsumer.h b/libs/hostgraphics/gui/BufferItemConsumer.h
index 707b313eb102..c25941151800 100644
--- a/libs/hostgraphics/gui/BufferItemConsumer.h
+++ b/libs/hostgraphics/gui/BufferItemConsumer.h
@@ -17,32 +17,30 @@
#ifndef ANDROID_GUI_BUFFERITEMCONSUMER_H
#define ANDROID_GUI_BUFFERITEMCONSUMER_H
-#include <utils/RefBase.h>
-
#include <gui/ConsumerBase.h>
#include <gui/IGraphicBufferConsumer.h>
+#include <utils/RefBase.h>
namespace android {
class BufferItemConsumer : public ConsumerBase {
public:
- BufferItemConsumer(
- const sp<IGraphicBufferConsumer>& consumer,
- uint64_t consumerUsage,
- int bufferCount,
- bool controlledByApp) : mConsumer(consumer) {
- }
+ BufferItemConsumer(const sp<IGraphicBufferConsumer>& consumer, uint64_t consumerUsage,
+ int bufferCount, bool controlledByApp)
+ : mConsumer(consumer) {}
- status_t acquireBuffer(BufferItem *item, nsecs_t presentWhen, bool waitForFence = true) {
+ status_t acquireBuffer(BufferItem* item, nsecs_t presentWhen, bool waitForFence = true) {
return mConsumer->acquireBuffer(item, presentWhen, 0);
}
- status_t releaseBuffer(
- const BufferItem &item, const sp<Fence>& releaseFence = Fence::NO_FENCE) { return OK; }
+ status_t releaseBuffer(const BufferItem& item,
+ const sp<Fence>& releaseFence = Fence::NO_FENCE) {
+ return OK;
+ }
- void setName(const String8& name) { }
+ void setName(const String8& name) {}
- void setFrameAvailableListener(const wp<FrameAvailableListener>& listener) { }
+ void setFrameAvailableListener(const wp<FrameAvailableListener>& listener) {}
status_t setDefaultBufferSize(uint32_t width, uint32_t height) {
return mConsumer->setDefaultBufferSize(width, height);
@@ -56,16 +54,23 @@ public:
return mConsumer->setDefaultBufferDataSpace(defaultDataSpace);
}
- void abandon() { }
+ void abandon() {}
- status_t detachBuffer(int slot) { return OK; }
+ status_t detachBuffer(int slot) {
+ return OK;
+ }
+
+ status_t discardFreeBuffers() {
+ return OK;
+ }
- status_t discardFreeBuffers() { return OK; }
+ void freeBufferLocked(int slotIndex) {}
- void freeBufferLocked(int slotIndex) { }
+ status_t addReleaseFenceLocked(int slot, const sp<GraphicBuffer> graphicBuffer,
+ const sp<Fence>& fence) {
+ return OK;
+ }
- status_t addReleaseFenceLocked(
- int slot, const sp<GraphicBuffer> graphicBuffer, const sp<Fence>& fence) { return OK; }
private:
sp<IGraphicBufferConsumer> mConsumer;
};
diff --git a/libs/hostgraphics/gui/BufferQueue.h b/libs/hostgraphics/gui/BufferQueue.h
index aa3e7268e11c..67a8c00fd267 100644
--- a/libs/hostgraphics/gui/BufferQueue.h
+++ b/libs/hostgraphics/gui/BufferQueue.h
@@ -29,7 +29,7 @@ public:
enum { NO_BUFFER_AVAILABLE = IGraphicBufferConsumer::NO_BUFFER_AVAILABLE };
static void createBufferQueue(sp<IGraphicBufferProducer>* outProducer,
- sp<IGraphicBufferConsumer>* outConsumer);
+ sp<IGraphicBufferConsumer>* outConsumer);
};
} // namespace android
diff --git a/libs/hostgraphics/gui/ConsumerBase.h b/libs/hostgraphics/gui/ConsumerBase.h
index 9002953c0848..7f7309e8a3a8 100644
--- a/libs/hostgraphics/gui/ConsumerBase.h
+++ b/libs/hostgraphics/gui/ConsumerBase.h
@@ -18,7 +18,6 @@
#define ANDROID_GUI_CONSUMERBASE_H
#include <gui/BufferItem.h>
-
#include <utils/RefBase.h>
namespace android {
@@ -28,10 +27,11 @@ public:
struct FrameAvailableListener : public virtual RefBase {
// See IConsumerListener::onFrame{Available,Replaced}
virtual void onFrameAvailable(const BufferItem& item) = 0;
+
virtual void onFrameReplaced(const BufferItem& /* item */) {}
};
};
} // namespace android
-#endif // ANDROID_GUI_CONSUMERBASE_H \ No newline at end of file
+#endif // ANDROID_GUI_CONSUMERBASE_H
diff --git a/libs/hostgraphics/gui/IGraphicBufferConsumer.h b/libs/hostgraphics/gui/IGraphicBufferConsumer.h
index 9eb67b218800..14ac4fe71cc8 100644
--- a/libs/hostgraphics/gui/IGraphicBufferConsumer.h
+++ b/libs/hostgraphics/gui/IGraphicBufferConsumer.h
@@ -16,16 +16,16 @@
#pragma once
-#include <utils/RefBase.h>
-
#include <ui/PixelFormat.h>
-
#include <utils/Errors.h>
+#include <utils/RefBase.h>
namespace android {
class BufferItem;
+
class Fence;
+
class GraphicBuffer;
class IGraphicBufferConsumer : virtual public RefBase {
@@ -62,4 +62,4 @@ public:
virtual status_t discardFreeBuffers() = 0;
};
-} // namespace android \ No newline at end of file
+} // namespace android
diff --git a/libs/hostgraphics/gui/IGraphicBufferProducer.h b/libs/hostgraphics/gui/IGraphicBufferProducer.h
index a1efd0bcfa4c..8fd8590d10d7 100644
--- a/libs/hostgraphics/gui/IGraphicBufferProducer.h
+++ b/libs/hostgraphics/gui/IGraphicBufferProducer.h
@@ -17,9 +17,8 @@
#ifndef ANDROID_GUI_IGRAPHICBUFFERPRODUCER_H
#define ANDROID_GUI_IGRAPHICBUFFERPRODUCER_H
-#include <utils/RefBase.h>
-
#include <ui/GraphicBuffer.h>
+#include <utils/RefBase.h>
namespace android {
@@ -31,6 +30,10 @@ public:
// Disconnect any API originally connected from the process calling disconnect.
AllLocal
};
+
+ virtual int query(int what, int* value) = 0;
+
+ virtual status_t requestBuffer(int slot, sp<GraphicBuffer>* buf) = 0;
};
} // namespace android
diff --git a/libs/hostgraphics/gui/Surface.h b/libs/hostgraphics/gui/Surface.h
index 2573931c8543..2774f89cb54c 100644
--- a/libs/hostgraphics/gui/Surface.h
+++ b/libs/hostgraphics/gui/Surface.h
@@ -17,25 +17,36 @@
#ifndef ANDROID_GUI_SURFACE_H
#define ANDROID_GUI_SURFACE_H
-#include <gui/IGraphicBufferProducer.h>
+#include <system/window.h>
#include <ui/ANativeObjectBase.h>
#include <utils/RefBase.h>
-#include <system/window.h>
+
+#include "gui/IGraphicBufferProducer.h"
namespace android {
class Surface : public ANativeObjectBase<ANativeWindow, Surface, RefBase> {
public:
- explicit Surface(const sp<IGraphicBufferProducer>& bufferProducer,
- bool controlledByApp = false) {
+ explicit Surface(const sp<IGraphicBufferProducer>& bufferProducer, bool controlledByApp = false)
+ : mBufferProducer(bufferProducer) {
ANativeWindow::perform = hook_perform;
+ ANativeWindow::dequeueBuffer = hook_dequeueBuffer;
+ ANativeWindow::query = hook_query;
}
- static bool isValid(const sp<Surface>& surface) { return surface != nullptr; }
+
+ static bool isValid(const sp<Surface>& surface) {
+ return surface != nullptr;
+ }
+
void allocateBuffers() {}
- uint64_t getNextFrameNumber() const { return 0; }
+ uint64_t getNextFrameNumber() const {
+ return 0;
+ }
- int setScalingMode(int mode) { return 0; }
+ int setScalingMode(int mode) {
+ return 0;
+ }
virtual int disconnect(int api,
IGraphicBufferProducer::DisconnectMode mode =
@@ -47,22 +58,88 @@ public:
// TODO: implement this
return 0;
}
- virtual int unlockAndPost() { return 0; }
- virtual int query(int what, int* value) const { return 0; }
+
+ virtual int unlockAndPost() {
+ return 0;
+ }
+
+ virtual int query(int what, int* value) const {
+ return mBufferProducer->query(what, value);
+ }
+
+ status_t setDequeueTimeout(nsecs_t timeout) {
+ return OK;
+ }
+
+ nsecs_t getLastDequeueStartTime() const {
+ return 0;
+ }
virtual void destroy() {}
+ int getBuffersDataSpace() {
+ return 0;
+ }
+
protected:
virtual ~Surface() {}
- static int hook_perform(ANativeWindow* window, int operation, ...) { return 0; }
+ static int hook_perform(ANativeWindow* window, int operation, ...) {
+ va_list args;
+ va_start(args, operation);
+ Surface* c = getSelf(window);
+ int result = c->perform(operation, args);
+ va_end(args);
+ return result;
+ }
+
+ static int hook_query(const ANativeWindow* window, int what, int* value) {
+ const Surface* c = getSelf(window);
+ return c->query(what, value);
+ }
+
+ static int hook_dequeueBuffer(ANativeWindow* window, ANativeWindowBuffer** buffer,
+ int* fenceFd) {
+ Surface* c = getSelf(window);
+ return c->dequeueBuffer(buffer, fenceFd);
+ }
+
+ virtual int dequeueBuffer(ANativeWindowBuffer** buffer, int* fenceFd) {
+ mBufferProducer->requestBuffer(0, &mBuffer);
+ *buffer = mBuffer.get();
+ return OK;
+ }
+
+ virtual int cancelBuffer(ANativeWindowBuffer* buffer, int fenceFd) {
+ return 0;
+ }
+
+ virtual int queueBuffer(ANativeWindowBuffer* buffer, int fenceFd) {
+ return 0;
+ }
+
+ virtual int perform(int operation, va_list args) {
+ return 0;
+ }
+
+ virtual int setSwapInterval(int interval) {
+ return 0;
+ }
+
+ virtual int setBufferCount(int bufferCount) {
+ return 0;
+ }
private:
// can't be copied
Surface& operator=(const Surface& rhs);
+
Surface(const Surface& rhs);
+
+ const sp<IGraphicBufferProducer> mBufferProducer;
+ sp<GraphicBuffer> mBuffer;
};
} // namespace android
-#endif // ANDROID_GUI_SURFACE_H
+#endif // ANDROID_GUI_SURFACE_H
diff --git a/libs/hostgraphics/ui/Fence.h b/libs/hostgraphics/ui/Fence.h
index 04d535c3a211..187c3116f61c 100644
--- a/libs/hostgraphics/ui/Fence.h
+++ b/libs/hostgraphics/ui/Fence.h
@@ -17,8 +17,8 @@
#ifndef ANDROID_FENCE_H
#define ANDROID_FENCE_H
-#include <utils/String8.h>
#include <utils/RefBase.h>
+#include <utils/String8.h>
typedef int64_t nsecs_t;
@@ -26,11 +26,14 @@ namespace android {
class Fence : public LightRefBase<Fence> {
public:
- Fence() { }
- Fence(int) { }
+ Fence() {}
+
+ Fence(int) {}
+
static const sp<Fence> NO_FENCE;
static constexpr nsecs_t SIGNAL_TIME_PENDING = INT64_MAX;
static constexpr nsecs_t SIGNAL_TIME_INVALID = -1;
+
static sp<Fence> merge(const char* name, const sp<Fence>& f1, const sp<Fence>& f2) {
return NO_FENCE;
}
@@ -40,16 +43,22 @@ public:
}
enum class Status {
- Invalid, // Fence is invalid
- Unsignaled, // Fence is valid but has not yet signaled
- Signaled, // Fence is valid and has signaled
+ Invalid, // Fence is invalid
+ Unsignaled, // Fence is valid but has not yet signaled
+ Signaled, // Fence is valid and has signaled
};
- status_t wait(int timeout) { return OK; }
+ status_t wait(int timeout) {
+ return OK;
+ }
- status_t waitForever(const char* logname) { return OK; }
+ status_t waitForever(const char* logname) {
+ return OK;
+ }
- int dup() const { return 0; }
+ int dup() const {
+ return 0;
+ }
inline Status getStatus() {
// The sync_wait call underlying wait() has been measured to be
diff --git a/libs/hostgraphics/ui/GraphicBuffer.h b/libs/hostgraphics/ui/GraphicBuffer.h
index ac88e44dbc65..cda45e4660ca 100644
--- a/libs/hostgraphics/ui/GraphicBuffer.h
+++ b/libs/hostgraphics/ui/GraphicBuffer.h
@@ -19,31 +19,51 @@
#include <stdint.h>
#include <sys/types.h>
-
-#include <vector>
-
+#include <ui/ANativeObjectBase.h>
#include <ui/PixelFormat.h>
#include <ui/Rect.h>
-
#include <utils/RefBase.h>
+#include <vector>
+
namespace android {
-class GraphicBuffer : virtual public RefBase {
+class GraphicBuffer : public ANativeObjectBase<ANativeWindowBuffer, GraphicBuffer, RefBase> {
public:
- GraphicBuffer(uint32_t w, uint32_t h):width(w),height(h) {
- data.resize(w*h);
+ GraphicBuffer(uint32_t w, uint32_t h) {
+ data.resize(w * h);
+ reserved[0] = data.data();
+ width = w;
+ height = h;
+ }
+
+ uint32_t getWidth() const {
+ return static_cast<uint32_t>(width);
+ }
+
+ uint32_t getHeight() const {
+ return static_cast<uint32_t>(height);
+ }
+
+ uint32_t getStride() const {
+ return static_cast<uint32_t>(width);
+ }
+
+ uint64_t getUsage() const {
+ return 0;
}
- uint32_t getWidth() const { return static_cast<uint32_t>(width); }
- uint32_t getHeight() const { return static_cast<uint32_t>(height); }
- uint32_t getStride() const { return static_cast<uint32_t>(width); }
- uint64_t getUsage() const { return 0; }
- PixelFormat getPixelFormat() const { return PIXEL_FORMAT_RGBA_8888; }
- //uint32_t getLayerCount() const { return static_cast<uint32_t>(layerCount); }
- Rect getBounds() const { return Rect(width, height); }
- status_t lockAsyncYCbCr(uint32_t inUsage, const Rect& rect,
- android_ycbcr *ycbcr, int fenceFd) { return OK; }
+ PixelFormat getPixelFormat() const {
+ return PIXEL_FORMAT_RGBA_8888;
+ }
+
+ Rect getBounds() const {
+ return Rect(width, height);
+ }
+
+ status_t lockAsyncYCbCr(uint32_t inUsage, const Rect& rect, android_ycbcr* ycbcr, int fenceFd) {
+ return OK;
+ }
status_t lockAsync(uint32_t inUsage, const Rect& rect, void** vaddr, int fenceFd,
int32_t* outBytesPerPixel = nullptr, int32_t* outBytesPerStride = nullptr) {
@@ -51,11 +71,11 @@ public:
return OK;
}
- status_t unlockAsync(int *fenceFd) { return OK; }
+ status_t unlockAsync(int* fenceFd) {
+ return OK;
+ }
private:
- uint32_t width;
- uint32_t height;
std::vector<uint32_t> data;
};
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index 7e70a7a76971..341599e79662 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -1,4 +1,5 @@
package {
+ default_team: "trendy_team_android_core_graphics_stack",
default_applicable_licenses: ["frameworks_base_libs_hwui_license"],
}
@@ -79,13 +80,13 @@ cc_defaults {
include_dirs: [
"external/skia/include/private",
"external/skia/src/core",
+ "external/skia/src/utils",
],
target: {
android: {
include_dirs: [
"external/skia/src/image",
- "external/skia/src/utils",
"external/skia/src/gpu",
"external/skia/src/shaders",
],
@@ -93,9 +94,11 @@ cc_defaults {
host: {
include_dirs: [
"external/vulkan-headers/include",
+ "frameworks/av/media/ndk/include",
],
cflags: [
"-Wno-unused-variable",
+ "-D__INTRODUCED_IN(n)=",
],
},
},
@@ -112,6 +115,7 @@ cc_defaults {
"libharfbuzz_ng",
"libminikin",
"server_configurable_flags",
+ "libaconfig_storage_read_api_cc"
],
static_libs: [
@@ -141,7 +145,6 @@ cc_defaults {
"libsync",
"libui",
"aconfig_text_flags_c_lib",
- "server_configurable_flags",
],
static_libs: [
"libEGL_blobCache",
@@ -266,6 +269,7 @@ cc_defaults {
cppflags: ["-Wno-conversion-null"],
srcs: [
+ "apex/android_canvas.cpp",
"apex/android_matrix.cpp",
"apex/android_paint.cpp",
"apex/android_region.cpp",
@@ -278,7 +282,6 @@ cc_defaults {
android: {
srcs: [ // sources that depend on android only libraries
"apex/android_bitmap.cpp",
- "apex/android_canvas.cpp",
"apex/jni_runtime.cpp",
],
},
@@ -334,9 +337,12 @@ cc_defaults {
"jni/android_graphics_animation_NativeInterpolatorFactory.cpp",
"jni/android_graphics_animation_RenderNodeAnimator.cpp",
"jni/android_graphics_Canvas.cpp",
+ "jni/android_graphics_Color.cpp",
"jni/android_graphics_ColorSpace.cpp",
"jni/android_graphics_drawable_AnimatedVectorDrawable.cpp",
"jni/android_graphics_drawable_VectorDrawable.cpp",
+ "jni/android_graphics_HardwareRenderer.cpp",
+ "jni/android_graphics_HardwareBufferRenderer.cpp",
"jni/android_graphics_HardwareRendererObserver.cpp",
"jni/android_graphics_Matrix.cpp",
"jni/android_graphics_Picture.cpp",
@@ -346,6 +352,7 @@ cc_defaults {
"jni/android_nio_utils.cpp",
"jni/android_util_PathParser.cpp",
+ "jni/AnimatedImageDrawable.cpp",
"jni/Bitmap.cpp",
"jni/BitmapRegionDecoder.cpp",
"jni/BufferUtils.cpp",
@@ -419,17 +426,13 @@ cc_defaults {
target: {
android: {
srcs: [ // sources that depend on android only libraries
- "jni/AnimatedImageDrawable.cpp",
"jni/android_graphics_TextureLayer.cpp",
- "jni/android_graphics_HardwareRenderer.cpp",
- "jni/android_graphics_HardwareBufferRenderer.cpp",
"jni/GIFMovie.cpp",
"jni/GraphicsStatsService.cpp",
"jni/Movie.cpp",
"jni/MovieImpl.cpp",
"jni/pdf/PdfDocument.cpp",
"jni/pdf/PdfEditor.cpp",
- "jni/pdf/PdfRenderer.cpp",
"jni/pdf/PdfUtils.cpp",
],
shared_libs: [
@@ -448,6 +451,12 @@ cc_defaults {
"libstatssocket_lazy",
],
},
+ linux: {
+ srcs: ["platform/linux/utils/SharedLib.cpp"],
+ },
+ darwin: {
+ srcs: ["platform/darwin/utils/SharedLib.cpp"],
+ },
host: {
cflags: [
"-Wno-unused-const-variable",
@@ -531,16 +540,24 @@ cc_defaults {
"effects/GainmapRenderer.cpp",
"pipeline/skia/BackdropFilterDrawable.cpp",
"pipeline/skia/HolePunch.cpp",
+ "pipeline/skia/SkiaCpuPipeline.cpp",
"pipeline/skia/SkiaDisplayList.cpp",
+ "pipeline/skia/SkiaPipeline.cpp",
"pipeline/skia/SkiaRecordingCanvas.cpp",
"pipeline/skia/StretchMask.cpp",
"pipeline/skia/RenderNodeDrawable.cpp",
"pipeline/skia/ReorderBarrierDrawables.cpp",
"pipeline/skia/TransformCanvas.cpp",
+ "renderstate/RenderState.cpp",
+ "renderthread/CanvasContext.cpp",
+ "renderthread/DrawFrameTask.cpp",
"renderthread/Frame.cpp",
+ "renderthread/RenderEffectCapabilityQuery.cpp",
+ "renderthread/RenderProxy.cpp",
"renderthread/RenderTask.cpp",
"renderthread/TimeLord.cpp",
"hwui/AnimatedImageDrawable.cpp",
+ "hwui/AnimatedImageThread.cpp",
"hwui/Bitmap.cpp",
"hwui/BlurDrawLooper.cpp",
"hwui/Canvas.cpp",
@@ -549,6 +566,7 @@ cc_defaults {
"hwui/MinikinUtils.cpp",
"hwui/PaintImpl.cpp",
"hwui/Typeface.cpp",
+ "thread/CommonPool.cpp",
"utils/Blur.cpp",
"utils/Color.cpp",
"utils/LinearAllocator.cpp",
@@ -565,8 +583,11 @@ cc_defaults {
"FrameInfoVisualizer.cpp",
"FrameMetricsReporter.cpp",
"Gainmap.cpp",
+ "HWUIProperties.sysprop",
"Interpolator.cpp",
"JankTracker.cpp",
+ "Layer.cpp",
+ "LayerUpdateQueue.cpp",
"LightingInfo.cpp",
"Matrix.cpp",
"Mesh.cpp",
@@ -583,6 +604,7 @@ cc_defaults {
"SkiaCanvas.cpp",
"SkiaInterpolator.cpp",
"Tonemapper.cpp",
+ "TreeInfo.cpp",
"VectorDrawable.cpp",
],
@@ -599,43 +621,32 @@ cc_defaults {
local_include_dirs: ["platform/android"],
srcs: [
- "hwui/AnimatedImageThread.cpp",
"pipeline/skia/ATraceMemoryDump.cpp",
"pipeline/skia/GLFunctorDrawable.cpp",
"pipeline/skia/LayerDrawable.cpp",
"pipeline/skia/ShaderCache.cpp",
+ "pipeline/skia/SkiaGpuPipeline.cpp",
"pipeline/skia/SkiaMemoryTracer.cpp",
"pipeline/skia/SkiaOpenGLPipeline.cpp",
- "pipeline/skia/SkiaPipeline.cpp",
"pipeline/skia/SkiaProfileRenderer.cpp",
"pipeline/skia/SkiaVulkanPipeline.cpp",
"pipeline/skia/VkFunctorDrawable.cpp",
"pipeline/skia/VkInteropFunctorDrawable.cpp",
- "renderstate/RenderState.cpp",
"renderthread/CacheManager.cpp",
- "renderthread/CanvasContext.cpp",
- "renderthread/DrawFrameTask.cpp",
"renderthread/EglManager.cpp",
"renderthread/ReliableSurface.cpp",
- "renderthread/RenderEffectCapabilityQuery.cpp",
"renderthread/VulkanManager.cpp",
"renderthread/VulkanSurface.cpp",
- "renderthread/RenderProxy.cpp",
"renderthread/RenderThread.cpp",
"renderthread/HintSessionWrapper.cpp",
"service/GraphicsStatsService.cpp",
- "thread/CommonPool.cpp",
"utils/GLUtils.cpp",
"utils/NdkUtils.cpp",
"AutoBackendTextureRelease.cpp",
"DeferredLayerUpdater.cpp",
"HardwareBitmapUploader.cpp",
- "HWUIProperties.sysprop",
- "Layer.cpp",
- "LayerUpdateQueue.cpp",
"ProfileDataContainer.cpp",
"Readback.cpp",
- "TreeInfo.cpp",
"WebViewFunctorManager.cpp",
"protos/graphicsstats.proto",
],
@@ -653,6 +664,8 @@ cc_defaults {
srcs: [
"platform/host/renderthread/CacheManager.cpp",
+ "platform/host/renderthread/HintSessionWrapper.cpp",
+ "platform/host/renderthread/ReliableSurface.cpp",
"platform/host/renderthread/RenderThread.cpp",
"platform/host/ProfileDataContainer.cpp",
"platform/host/Readback.cpp",
diff --git a/libs/hwui/AnimatorManager.cpp b/libs/hwui/AnimatorManager.cpp
index 078041411a21..8645995e3df1 100644
--- a/libs/hwui/AnimatorManager.cpp
+++ b/libs/hwui/AnimatorManager.cpp
@@ -90,7 +90,13 @@ void AnimatorManager::pushStaging() {
}
mCancelAllAnimators = false;
} else {
- for (auto& animator : mAnimators) {
+ // create a copy of mAnimators as onAnimatorTargetChanged can erase mAnimators.
+ FatVector<sp<BaseRenderNodeAnimator>> animators;
+ animators.reserve(mAnimators.size());
+ for (const auto& animator : mAnimators) {
+ animators.push_back(animator);
+ }
+ for (auto& animator : animators) {
animator->pushStaging(mAnimationHandle->context());
}
}
diff --git a/libs/hwui/ColorFilter.h b/libs/hwui/ColorFilter.h
index 1a5b938d6eed..31c9db7ca4fb 100644
--- a/libs/hwui/ColorFilter.h
+++ b/libs/hwui/ColorFilter.h
@@ -23,17 +23,42 @@
#include "GraphicsJNI.h"
#include "SkColorFilter.h"
-#include "SkiaWrapper.h"
namespace android {
namespace uirenderer {
-class ColorFilter : public SkiaWrapper<SkColorFilter> {
+class ColorFilter : public VirtualLightRefBase {
public:
static ColorFilter* fromJava(jlong handle) { return reinterpret_cast<ColorFilter*>(handle); }
+ sk_sp<SkColorFilter> getInstance() {
+ if (mInstance != nullptr && shouldDiscardInstance()) {
+ mInstance = nullptr;
+ }
+
+ if (mInstance == nullptr) {
+ mInstance = createInstance();
+ if (mInstance) {
+ mInstance = mInstance->makeWithWorkingColorSpace(SkColorSpace::MakeSRGB());
+ }
+ mGenerationId++;
+ }
+ return mInstance;
+ }
+
+ virtual bool shouldDiscardInstance() const { return false; }
+
+ void discardInstance() { mInstance = nullptr; }
+
+ [[nodiscard]] int32_t getGenerationId() const { return mGenerationId; }
+
protected:
ColorFilter() = default;
+ virtual sk_sp<SkColorFilter> createInstance() = 0;
+
+private:
+ sk_sp<SkColorFilter> mInstance = nullptr;
+ int32_t mGenerationId = 0;
};
class BlendModeColorFilter : public ColorFilter {
diff --git a/libs/hwui/DeviceInfo.cpp b/libs/hwui/DeviceInfo.cpp
index 32bc122fdc58..af7a49653829 100644
--- a/libs/hwui/DeviceInfo.cpp
+++ b/libs/hwui/DeviceInfo.cpp
@@ -108,6 +108,10 @@ void DeviceInfo::setSupportFp16ForHdr(bool supportFp16ForHdr) {
get()->mSupportFp16ForHdr = supportFp16ForHdr;
}
+void DeviceInfo::setSupportRgba10101010ForHdr(bool supportRgba10101010ForHdr) {
+ get()->mSupportRgba10101010ForHdr = supportRgba10101010ForHdr;
+}
+
void DeviceInfo::setSupportMixedColorSpaces(bool supportMixedColorSpaces) {
get()->mSupportMixedColorSpaces = supportMixedColorSpaces;
}
diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h
index a5a841e07d7a..fb58a69747b3 100644
--- a/libs/hwui/DeviceInfo.h
+++ b/libs/hwui/DeviceInfo.h
@@ -69,6 +69,15 @@ public:
return get()->mSupportFp16ForHdr;
};
+ static void setSupportRgba10101010ForHdr(bool supportRgba10101010ForHdr);
+ static bool isSupportRgba10101010ForHdr() {
+ if (!Properties::hdr10bitPlus) {
+ return false;
+ }
+
+ return get()->mSupportRgba10101010ForHdr;
+ };
+
static void setSupportMixedColorSpaces(bool supportMixedColorSpaces);
static bool isSupportMixedColorSpaces() { return get()->mSupportMixedColorSpaces; };
@@ -102,6 +111,7 @@ private:
int mMaxTextureSize;
sk_sp<SkColorSpace> mWideColorSpace = SkColorSpace::MakeSRGB();
bool mSupportFp16ForHdr = false;
+ bool mSupportRgba10101010ForHdr = false;
bool mSupportMixedColorSpaces = false;
SkColorType mWideColorType = SkColorType::kN32_SkColorType;
int mDisplaysSize = 0;
diff --git a/libs/hwui/Mesh.cpp b/libs/hwui/Mesh.cpp
index 37a7d74330e9..5ef7acdaf0fa 100644
--- a/libs/hwui/Mesh.cpp
+++ b/libs/hwui/Mesh.cpp
@@ -21,6 +21,8 @@
#include "SafeMath.h"
+namespace android {
+
static size_t min_vcount_for_mode(SkMesh::Mode mode) {
switch (mode) {
case SkMesh::Mode::kTriangles:
@@ -28,6 +30,7 @@ static size_t min_vcount_for_mode(SkMesh::Mode mode) {
case SkMesh::Mode::kTriangleStrip:
return 3;
}
+ return 1;
}
// Re-implementation of SkMesh::validate to validate user side that their mesh is valid.
@@ -36,29 +39,30 @@ std::tuple<bool, SkString> Mesh::validate() {
if (!mMeshSpec) {
FAIL_MESH_VALIDATE("MeshSpecification is required.");
}
- if (mVertexBufferData.empty()) {
+ if (mBufferData->vertexData().empty()) {
FAIL_MESH_VALIDATE("VertexBuffer is required.");
}
- auto meshStride = mMeshSpec->stride();
- auto meshMode = SkMesh::Mode(mMode);
+ size_t vertexStride = mMeshSpec->stride();
+ size_t vertexCount = mBufferData->vertexCount();
+ size_t vertexOffset = mBufferData->vertexOffset();
SafeMath sm;
- size_t vsize = sm.mul(meshStride, mVertexCount);
- if (sm.add(vsize, mVertexOffset) > mVertexBufferData.size()) {
+ size_t vertexSize = sm.mul(vertexStride, vertexCount);
+ if (sm.add(vertexSize, vertexOffset) > mBufferData->vertexData().size()) {
FAIL_MESH_VALIDATE(
"The vertex buffer offset and vertex count reads beyond the end of the"
" vertex buffer.");
}
- if (mVertexOffset % meshStride != 0) {
+ if (vertexOffset % vertexStride != 0) {
FAIL_MESH_VALIDATE("The vertex offset (%zu) must be a multiple of the vertex stride (%zu).",
- mVertexOffset, meshStride);
+ vertexOffset, vertexStride);
}
if (size_t uniformSize = mMeshSpec->uniformSize()) {
- if (!mBuilder->fUniforms || mBuilder->fUniforms->size() < uniformSize) {
+ if (!mUniformBuilder.fUniforms || mUniformBuilder.fUniforms->size() < uniformSize) {
FAIL_MESH_VALIDATE("The uniform data is %zu bytes but must be at least %zu.",
- mBuilder->fUniforms->size(), uniformSize);
+ mUniformBuilder.fUniforms->size(), uniformSize);
}
}
@@ -69,29 +73,33 @@ std::tuple<bool, SkString> Mesh::validate() {
case SkMesh::Mode::kTriangleStrip:
return "triangle-strip";
}
+ return "unknown";
};
- if (!mIndexBufferData.empty()) {
- if (mIndexCount < min_vcount_for_mode(meshMode)) {
+
+ size_t indexCount = mBufferData->indexCount();
+ size_t indexOffset = mBufferData->indexOffset();
+ if (!mBufferData->indexData().empty()) {
+ if (indexCount < min_vcount_for_mode(mMode)) {
FAIL_MESH_VALIDATE("%s mode requires at least %zu indices but index count is %zu.",
- modeToStr(meshMode), min_vcount_for_mode(meshMode), mIndexCount);
+ modeToStr(mMode), min_vcount_for_mode(mMode), indexCount);
}
- size_t isize = sm.mul(sizeof(uint16_t), mIndexCount);
- if (sm.add(isize, mIndexOffset) > mIndexBufferData.size()) {
+ size_t isize = sm.mul(sizeof(uint16_t), indexCount);
+ if (sm.add(isize, indexOffset) > mBufferData->indexData().size()) {
FAIL_MESH_VALIDATE(
"The index buffer offset and index count reads beyond the end of the"
" index buffer.");
}
// If we allow 32 bit indices then this should enforce 4 byte alignment in that case.
- if (!SkIsAlign2(mIndexOffset)) {
+ if (!SkIsAlign2(indexOffset)) {
FAIL_MESH_VALIDATE("The index offset must be a multiple of 2.");
}
} else {
- if (mVertexCount < min_vcount_for_mode(meshMode)) {
+ if (vertexCount < min_vcount_for_mode(mMode)) {
FAIL_MESH_VALIDATE("%s mode requires at least %zu vertices but vertex count is %zu.",
- modeToStr(meshMode), min_vcount_for_mode(meshMode), mVertexCount);
+ modeToStr(mMode), min_vcount_for_mode(mMode), vertexCount);
}
- LOG_ALWAYS_FATAL_IF(mIndexCount != 0);
- LOG_ALWAYS_FATAL_IF(mIndexOffset != 0);
+ LOG_ALWAYS_FATAL_IF(indexCount != 0);
+ LOG_ALWAYS_FATAL_IF(indexOffset != 0);
}
if (!sm.ok()) {
@@ -100,3 +108,5 @@ std::tuple<bool, SkString> Mesh::validate() {
#undef FAIL_MESH_VALIDATE
return {true, {}};
}
+
+} // namespace android
diff --git a/libs/hwui/Mesh.h b/libs/hwui/Mesh.h
index 69fda34afc78..8c6ca9758479 100644
--- a/libs/hwui/Mesh.h
+++ b/libs/hwui/Mesh.h
@@ -25,6 +25,8 @@
#include <utility>
+namespace android {
+
class MeshUniformBuilder {
public:
struct MeshUniform {
@@ -103,111 +105,170 @@ private:
sk_sp<SkMeshSpecification> fMeshSpec;
};
-class Mesh {
+// Storage for CPU and GPU copies of the vertex and index data of a mesh.
+class MeshBufferData {
public:
- Mesh(const sk_sp<SkMeshSpecification>& meshSpec, int mode,
- std::vector<uint8_t>&& vertexBufferData, jint vertexCount, jint vertexOffset,
- std::unique_ptr<MeshUniformBuilder> builder, const SkRect& bounds)
- : mMeshSpec(meshSpec)
- , mMode(mode)
- , mVertexBufferData(std::move(vertexBufferData))
- , mVertexCount(vertexCount)
- , mVertexOffset(vertexOffset)
- , mBuilder(std::move(builder))
- , mBounds(bounds) {}
-
- Mesh(const sk_sp<SkMeshSpecification>& meshSpec, int mode,
- std::vector<uint8_t>&& vertexBufferData, jint vertexCount, jint vertexOffset,
- std::vector<uint8_t>&& indexBuffer, jint indexCount, jint indexOffset,
- std::unique_ptr<MeshUniformBuilder> builder, const SkRect& bounds)
- : mMeshSpec(meshSpec)
- , mMode(mode)
- , mVertexBufferData(std::move(vertexBufferData))
- , mVertexCount(vertexCount)
+ MeshBufferData(std::vector<uint8_t> vertexData, int32_t vertexCount, int32_t vertexOffset,
+ std::vector<uint8_t> indexData, int32_t indexCount, int32_t indexOffset)
+ : mVertexCount(vertexCount)
, mVertexOffset(vertexOffset)
- , mIndexBufferData(std::move(indexBuffer))
, mIndexCount(indexCount)
, mIndexOffset(indexOffset)
- , mBuilder(std::move(builder))
- , mBounds(bounds) {}
-
- Mesh(Mesh&&) = default;
+ , mVertexData(std::move(vertexData))
+ , mIndexData(std::move(indexData)) {}
- Mesh& operator=(Mesh&&) = default;
-
- [[nodiscard]] std::tuple<bool, SkString> validate();
-
- void updateSkMesh(GrDirectContext* context) const {
- GrDirectContext::DirectContextID genId = GrDirectContext::DirectContextID();
- if (context) {
- genId = context->directContextID();
+ void updateBuffers(GrDirectContext* context) const {
+ GrDirectContext::DirectContextID currentId = context == nullptr
+ ? GrDirectContext::DirectContextID()
+ : context->directContextID();
+ if (currentId == mSkiaBuffers.fGenerationId && mSkiaBuffers.fVertexBuffer != nullptr) {
+ // Nothing to update since the Android API does not support partial updates yet.
+ return;
}
- if (mIsDirty || genId != mGenerationId) {
- auto vertexData = reinterpret_cast<const void*>(mVertexBufferData.data());
+ mSkiaBuffers.fVertexBuffer =
#ifdef __ANDROID__
- auto vb = SkMeshes::MakeVertexBuffer(context,
- vertexData,
- mVertexBufferData.size());
+ SkMeshes::MakeVertexBuffer(context, mVertexData.data(), mVertexData.size());
#else
- auto vb = SkMeshes::MakeVertexBuffer(vertexData,
- mVertexBufferData.size());
+ SkMeshes::MakeVertexBuffer(mVertexData.data(), mVertexData.size());
#endif
- auto meshMode = SkMesh::Mode(mMode);
- if (!mIndexBufferData.empty()) {
- auto indexData = reinterpret_cast<const void*>(mIndexBufferData.data());
+ if (mIndexCount != 0) {
+ mSkiaBuffers.fIndexBuffer =
#ifdef __ANDROID__
- auto ib = SkMeshes::MakeIndexBuffer(context,
- indexData,
- mIndexBufferData.size());
+ SkMeshes::MakeIndexBuffer(context, mIndexData.data(), mIndexData.size());
#else
- auto ib = SkMeshes::MakeIndexBuffer(indexData,
- mIndexBufferData.size());
+ SkMeshes::MakeIndexBuffer(mIndexData.data(), mIndexData.size());
#endif
- mMesh = SkMesh::MakeIndexed(mMeshSpec, meshMode, vb, mVertexCount, mVertexOffset,
- ib, mIndexCount, mIndexOffset, mBuilder->fUniforms,
- SkSpan<SkRuntimeEffect::ChildPtr>(), mBounds)
- .mesh;
- } else {
- mMesh = SkMesh::Make(mMeshSpec, meshMode, vb, mVertexCount, mVertexOffset,
- mBuilder->fUniforms, SkSpan<SkRuntimeEffect::ChildPtr>(),
- mBounds)
- .mesh;
- }
- mIsDirty = false;
- mGenerationId = genId;
}
+ mSkiaBuffers.fGenerationId = currentId;
}
- SkMesh& getSkMesh() const {
- LOG_FATAL_IF(mIsDirty,
- "Attempt to obtain SkMesh when Mesh is dirty, did you "
- "forget to call updateSkMesh with a GrDirectContext? "
- "Defensively creating a CPU mesh");
- return mMesh;
- }
+ SkMesh::VertexBuffer* vertexBuffer() const { return mSkiaBuffers.fVertexBuffer.get(); }
+
+ sk_sp<SkMesh::VertexBuffer> refVertexBuffer() const { return mSkiaBuffers.fVertexBuffer; }
+ int32_t vertexCount() const { return mVertexCount; }
+ int32_t vertexOffset() const { return mVertexOffset; }
- void markDirty() { mIsDirty = true; }
+ sk_sp<SkMesh::IndexBuffer> refIndexBuffer() const { return mSkiaBuffers.fIndexBuffer; }
+ int32_t indexCount() const { return mIndexCount; }
+ int32_t indexOffset() const { return mIndexOffset; }
- MeshUniformBuilder* uniformBuilder() { return mBuilder.get(); }
+ const std::vector<uint8_t>& vertexData() const { return mVertexData; }
+ const std::vector<uint8_t>& indexData() const { return mIndexData; }
private:
- sk_sp<SkMeshSpecification> mMeshSpec;
- int mMode = 0;
+ struct CachedSkiaBuffers {
+ sk_sp<SkMesh::VertexBuffer> fVertexBuffer;
+ sk_sp<SkMesh::IndexBuffer> fIndexBuffer;
+ GrDirectContext::DirectContextID fGenerationId = GrDirectContext::DirectContextID();
+ };
+
+ mutable CachedSkiaBuffers mSkiaBuffers;
+ int32_t mVertexCount = 0;
+ int32_t mVertexOffset = 0;
+ int32_t mIndexCount = 0;
+ int32_t mIndexOffset = 0;
+ std::vector<uint8_t> mVertexData;
+ std::vector<uint8_t> mIndexData;
+};
- std::vector<uint8_t> mVertexBufferData;
- size_t mVertexCount = 0;
- size_t mVertexOffset = 0;
+class Mesh {
+public:
+ // A snapshot of the mesh for use by the render thread.
+ //
+ // After a snapshot is taken, future uniform changes to the original Mesh will not modify the
+ // uniforms returned by makeSkMesh.
+ class Snapshot {
+ public:
+ Snapshot() = delete;
+ Snapshot(const Snapshot&) = default;
+ Snapshot(Snapshot&&) = default;
+ Snapshot& operator=(const Snapshot&) = default;
+ Snapshot& operator=(Snapshot&&) = default;
+ ~Snapshot() = default;
- std::vector<uint8_t> mIndexBufferData;
- size_t mIndexCount = 0;
- size_t mIndexOffset = 0;
+ const SkMesh& getSkMesh() const {
+ SkMesh::VertexBuffer* vertexBuffer = mBufferData->vertexBuffer();
+ LOG_FATAL_IF(vertexBuffer == nullptr,
+ "Attempt to obtain SkMesh when vertexBuffer has not been created, did you "
+ "forget to call MeshBufferData::updateBuffers with a GrDirectContext?");
+ if (vertexBuffer != mMesh.vertexBuffer()) mMesh = makeSkMesh();
+ return mMesh;
+ }
- std::unique_ptr<MeshUniformBuilder> mBuilder;
- SkRect mBounds{};
+ private:
+ friend class Mesh;
- mutable SkMesh mMesh{};
- mutable bool mIsDirty = true;
- mutable GrDirectContext::DirectContextID mGenerationId = GrDirectContext::DirectContextID();
+ Snapshot(sk_sp<SkMeshSpecification> meshSpec, SkMesh::Mode mode,
+ std::shared_ptr<const MeshBufferData> bufferData, sk_sp<const SkData> uniforms,
+ const SkRect& bounds)
+ : mMeshSpec(std::move(meshSpec))
+ , mMode(mode)
+ , mBufferData(std::move(bufferData))
+ , mUniforms(std::move(uniforms))
+ , mBounds(bounds) {}
+
+ SkMesh makeSkMesh() const {
+ const MeshBufferData& d = *mBufferData;
+ if (d.indexCount() != 0) {
+ return SkMesh::MakeIndexed(mMeshSpec, mMode, d.refVertexBuffer(), d.vertexCount(),
+ d.vertexOffset(), d.refIndexBuffer(), d.indexCount(),
+ d.indexOffset(), mUniforms,
+ SkSpan<SkRuntimeEffect::ChildPtr>(), mBounds)
+ .mesh;
+ }
+ return SkMesh::Make(mMeshSpec, mMode, d.refVertexBuffer(), d.vertexCount(),
+ d.vertexOffset(), mUniforms, SkSpan<SkRuntimeEffect::ChildPtr>(),
+ mBounds)
+ .mesh;
+ }
+
+ mutable SkMesh mMesh;
+ sk_sp<SkMeshSpecification> mMeshSpec;
+ SkMesh::Mode mMode;
+ std::shared_ptr<const MeshBufferData> mBufferData;
+ sk_sp<const SkData> mUniforms;
+ SkRect mBounds;
+ };
+
+ Mesh(sk_sp<SkMeshSpecification> meshSpec, SkMesh::Mode mode, std::vector<uint8_t> vertexData,
+ int32_t vertexCount, int32_t vertexOffset, const SkRect& bounds)
+ : Mesh(std::move(meshSpec), mode, std::move(vertexData), vertexCount, vertexOffset,
+ /* indexData = */ {}, /* indexCount = */ 0, /* indexOffset = */ 0, bounds) {}
+
+ Mesh(sk_sp<SkMeshSpecification> meshSpec, SkMesh::Mode mode, std::vector<uint8_t> vertexData,
+ int32_t vertexCount, int32_t vertexOffset, std::vector<uint8_t> indexData,
+ int32_t indexCount, int32_t indexOffset, const SkRect& bounds)
+ : mMeshSpec(std::move(meshSpec))
+ , mMode(mode)
+ , mBufferData(std::make_shared<MeshBufferData>(std::move(vertexData), vertexCount,
+ vertexOffset, std::move(indexData),
+ indexCount, indexOffset))
+ , mUniformBuilder(mMeshSpec)
+ , mBounds(bounds) {}
+
+ Mesh(Mesh&&) = default;
+
+ Mesh& operator=(Mesh&&) = default;
+
+ [[nodiscard]] std::tuple<bool, SkString> validate();
+
+ std::shared_ptr<const MeshBufferData> refBufferData() const { return mBufferData; }
+
+ Snapshot takeSnapshot() const {
+ return Snapshot(mMeshSpec, mMode, mBufferData, mUniformBuilder.fUniforms, mBounds);
+ }
+
+ MeshUniformBuilder* uniformBuilder() { return &mUniformBuilder; }
+
+private:
+ sk_sp<SkMeshSpecification> mMeshSpec;
+ SkMesh::Mode mMode;
+ std::shared_ptr<MeshBufferData> mBufferData;
+ MeshUniformBuilder mUniformBuilder;
+ SkRect mBounds;
};
+
+} // namespace android
+
#endif // MESH_H_
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index 755332ff66fd..5d3bc89b40dd 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -16,10 +16,6 @@
#include "Properties.h"
-#include "Debug.h"
-#ifdef __ANDROID__
-#include "HWUIProperties.sysprop.h"
-#endif
#include <android-base/properties.h>
#include <cutils/compiler.h>
#include <log/log.h>
@@ -28,6 +24,8 @@
#include <cstdlib>
#include <optional>
+#include "Debug.h"
+#include "HWUIProperties.sysprop.h"
#include "src/core/SkTraceEventCommon.h"
#ifdef __ANDROID__
@@ -41,22 +39,15 @@ constexpr bool clip_surfaceviews() {
constexpr bool hdr_10bit_plus() {
return false;
}
+constexpr bool initialize_gl_always() {
+ return false;
+}
} // namespace hwui_flags
#endif
namespace android {
namespace uirenderer {
-#ifndef __ANDROID__ // Layoutlib does not compile HWUIProperties.sysprop as it depends on cutils properties
-std::optional<bool> use_vulkan() {
- return base::GetBoolProperty("ro.hwui.use_vulkan", true);
-}
-
-std::optional<std::int32_t> render_ahead() {
- return base::GetIntProperty("ro.hwui.render_ahead", 0);
-}
-#endif
-
bool Properties::debugLayersUpdates = false;
bool Properties::debugOverdraw = false;
bool Properties::debugTraceGpuResourceCategories = false;
@@ -269,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 ec53070f6cb8..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
///////////////////////////////////////////////////////////////////////////////
@@ -242,7 +247,7 @@ enum class ProfileType { None, Console, Bars };
enum class OverdrawColorSet { Default = 0, Deuteranomaly };
-enum class RenderPipelineType { SkiaGL, SkiaVulkan, NotInitialized = 128 };
+enum class RenderPipelineType { SkiaGL, SkiaVulkan, SkiaCpu, NotInitialized = 128 };
enum class StretchEffectBehavior {
ShaderHWUI, // Stretch shader in HWUI only, matrix scale in SF
@@ -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/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
index 54aef55f8b90..d0263798d2c2 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -573,9 +573,9 @@ struct DrawSkMesh final : Op {
struct DrawMesh final : Op {
static const auto kType = Type::DrawMesh;
DrawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const SkPaint& paint)
- : mesh(mesh), blender(std::move(blender)), paint(paint) {}
+ : mesh(mesh.takeSnapshot()), blender(std::move(blender)), paint(paint) {}
- const Mesh& mesh;
+ Mesh::Snapshot mesh;
sk_sp<SkBlender> blender;
SkPaint paint;
@@ -1296,14 +1296,5 @@ void RecordingCanvas::drawWebView(skiapipeline::FunctorDrawable* drawable) {
fDL->drawWebView(drawable);
}
-[[nodiscard]] const SkMesh& DrawMeshPayload::getSkMesh() const {
- LOG_FATAL_IF(!meshWrapper && !mesh, "One of Mesh or Mesh must be non-null");
- if (meshWrapper) {
- return meshWrapper->getSkMesh();
- } else {
- return *mesh;
- }
-}
-
} // namespace uirenderer
} // namespace android
diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h
index 965264f31119..f86785274224 100644
--- a/libs/hwui/RecordingCanvas.h
+++ b/libs/hwui/RecordingCanvas.h
@@ -41,11 +41,12 @@
enum class SkBlendMode;
class SkRRect;
-class Mesh;
namespace android {
-namespace uirenderer {
+class Mesh;
+
+namespace uirenderer {
namespace skiapipeline {
class FunctorDrawable;
}
@@ -68,18 +69,6 @@ struct DisplayListOp {
static_assert(sizeof(DisplayListOp) == 4);
-class DrawMeshPayload {
-public:
- explicit DrawMeshPayload(const SkMesh* mesh) : mesh(mesh) {}
- explicit DrawMeshPayload(const Mesh* meshWrapper) : meshWrapper(meshWrapper) {}
-
- [[nodiscard]] const SkMesh& getSkMesh() const;
-
-private:
- const SkMesh* mesh = nullptr;
- const Mesh* meshWrapper = nullptr;
-};
-
struct DrawImagePayload {
explicit DrawImagePayload(Bitmap& bitmap)
: image(bitmap.makeImage()), palette(bitmap.palette()) {
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index f526a280b113..589abb4d87f4 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -16,18 +16,6 @@
#include "RenderNode.h"
-#include "DamageAccumulator.h"
-#include "Debug.h"
-#include "Properties.h"
-#include "TreeInfo.h"
-#include "VectorDrawable.h"
-#include "private/hwui/WebViewFunctor.h"
-#ifdef __ANDROID__
-#include "renderthread/CanvasContext.h"
-#else
-#include "DamageAccumulator.h"
-#include "pipeline/skia/SkiaDisplayList.h"
-#endif
#include <SkPathOps.h>
#include <gui/TraceUtils.h>
#include <ui/FatVector.h>
@@ -37,6 +25,14 @@
#include <sstream>
#include <string>
+#include "DamageAccumulator.h"
+#include "Debug.h"
+#include "Properties.h"
+#include "TreeInfo.h"
+#include "VectorDrawable.h"
+#include "private/hwui/WebViewFunctor.h"
+#include "renderthread/CanvasContext.h"
+
#ifdef __ANDROID__
#include "include/gpu/ganesh/SkImageGanesh.h"
#endif
@@ -186,7 +182,6 @@ void RenderNode::prepareLayer(TreeInfo& info, uint32_t dirtyMask) {
}
void RenderNode::pushLayerUpdate(TreeInfo& info) {
-#ifdef __ANDROID__ // Layoutlib does not support CanvasContext and Layers
LayerType layerType = properties().effectiveLayerType();
// If we are not a layer OR we cannot be rendered (eg, view was detached)
// we need to destroy any Layers we may have had previously
@@ -218,7 +213,6 @@ void RenderNode::pushLayerUpdate(TreeInfo& info) {
// That might be us, so tell CanvasContext that this layer is in the
// tree and should not be destroyed.
info.canvasContext.markLayerInUse(this);
-#endif
}
/**
diff --git a/libs/hwui/RootRenderNode.cpp b/libs/hwui/RootRenderNode.cpp
index ddbbf58b3071..5174e27ae587 100644
--- a/libs/hwui/RootRenderNode.cpp
+++ b/libs/hwui/RootRenderNode.cpp
@@ -18,11 +18,12 @@
#ifdef __ANDROID__ // Layoutlib does not support Looper (windows)
#include <utils/Looper.h>
+#else
+#include "utils/MessageHandler.h"
#endif
namespace android::uirenderer {
-#ifdef __ANDROID__ // Layoutlib does not support Looper
class FinishAndInvokeListener : public MessageHandler {
public:
explicit FinishAndInvokeListener(PropertyValuesAnimatorSet* anim) : mAnimator(anim) {
@@ -237,9 +238,13 @@ void RootRenderNode::detachVectorDrawableAnimator(PropertyValuesAnimatorSet* ani
// user events, in which case the already posted listener's id will become stale, and
// the onFinished callback will then be ignored.
sp<FinishAndInvokeListener> message = new FinishAndInvokeListener(anim);
+#ifdef __ANDROID__ // Layoutlib does not support Looper
auto looper = Looper::getForThread();
LOG_ALWAYS_FATAL_IF(looper == nullptr, "Not on a looper thread?");
looper->sendMessageDelayed(ms2ns(remainingTimeInMs), message, 0);
+#else
+ message->handleMessage(0);
+#endif
anim->clearOneShotListener();
}
}
@@ -285,22 +290,5 @@ private:
AnimationContext* ContextFactoryImpl::createAnimationContext(renderthread::TimeLord& clock) {
return new AnimationContextBridge(clock, mRootNode);
}
-#else
-
-void RootRenderNode::prepareTree(TreeInfo& info) {
- info.errorHandler = mErrorHandler.get();
- info.updateWindowPositions = true;
- RenderNode::prepareTree(info);
- info.updateWindowPositions = false;
- info.errorHandler = nullptr;
-}
-
-void RootRenderNode::attachAnimatingNode(RenderNode* animatingNode) { }
-
-void RootRenderNode::destroy() { }
-
-void RootRenderNode::addVectorDrawableAnimator(PropertyValuesAnimatorSet* anim) { }
-
-#endif
} // namespace android::uirenderer
diff --git a/libs/hwui/RootRenderNode.h b/libs/hwui/RootRenderNode.h
index 1d3f5a8a51e0..7a5cda7041ed 100644
--- a/libs/hwui/RootRenderNode.h
+++ b/libs/hwui/RootRenderNode.h
@@ -74,7 +74,6 @@ private:
void detachVectorDrawableAnimator(PropertyValuesAnimatorSet* anim);
};
-#ifdef __ANDROID__ // Layoutlib does not support Animations
class ContextFactoryImpl : public IContextFactory {
public:
explicit ContextFactoryImpl(RootRenderNode* rootNode) : mRootNode(rootNode) {}
@@ -84,6 +83,5 @@ public:
private:
RootRenderNode* mRootNode;
};
-#endif
} // namespace android::uirenderer
diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp
index 0b739c361d64..72e83afbd96f 100644
--- a/libs/hwui/SkiaCanvas.cpp
+++ b/libs/hwui/SkiaCanvas.cpp
@@ -596,8 +596,8 @@ void SkiaCanvas::drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const Pain
if (recordingContext) {
context = recordingContext->asDirectContext();
}
- mesh.updateSkMesh(context);
- mCanvas->drawMesh(mesh.getSkMesh(), blender, paint);
+ mesh.refBufferData()->updateBuffers(context);
+ mCanvas->drawMesh(mesh.takeSnapshot().getSkMesh(), blender, paint);
}
// ----------------------------------------------------------------------------
diff --git a/libs/hwui/SkiaInterpolator.cpp b/libs/hwui/SkiaInterpolator.cpp
index c67b135855f7..5a45ad9085e7 100644
--- a/libs/hwui/SkiaInterpolator.cpp
+++ b/libs/hwui/SkiaInterpolator.cpp
@@ -20,6 +20,7 @@
#include "include/core/SkTypes.h"
#include <cstdlib>
+#include <cstring>
#include <log/log.h>
typedef int Dot14;
diff --git a/libs/hwui/SkiaWrapper.h b/libs/hwui/SkiaWrapper.h
deleted file mode 100644
index bd0e35aadbb4..000000000000
--- a/libs/hwui/SkiaWrapper.h
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef SKIA_WRAPPER_H_
-#define SKIA_WRAPPER_H_
-
-#include <SkRefCnt.h>
-#include <utils/RefBase.h>
-
-namespace android::uirenderer {
-
-template <typename T>
-class SkiaWrapper : public VirtualLightRefBase {
-public:
- sk_sp<T> getInstance() {
- if (mInstance != nullptr && shouldDiscardInstance()) {
- mInstance = nullptr;
- }
-
- if (mInstance == nullptr) {
- mInstance = createInstance();
- mGenerationId++;
- }
- return mInstance;
- }
-
- virtual bool shouldDiscardInstance() const { return false; }
-
- void discardInstance() { mInstance = nullptr; }
-
- [[nodiscard]] int32_t getGenerationId() const { return mGenerationId; }
-
-protected:
- virtual sk_sp<T> createInstance() = 0;
-
-private:
- sk_sp<T> mInstance = nullptr;
- int32_t mGenerationId = 0;
-};
-
-} // namespace android::uirenderer
-
-#endif // SKIA_WRAPPER_H_
diff --git a/libs/hwui/VectorDrawable.cpp b/libs/hwui/VectorDrawable.cpp
index 2ea4e3f21163..af169f4bc4cd 100644
--- a/libs/hwui/VectorDrawable.cpp
+++ b/libs/hwui/VectorDrawable.cpp
@@ -540,7 +540,7 @@ bool Tree::allocateBitmapIfNeeded(Cache& cache, int width, int height) {
}
bool Tree::canReuseBitmap(Bitmap* bitmap, int width, int height) {
- return bitmap && width <= bitmap->width() && height <= bitmap->height();
+ return bitmap && width == bitmap->width() && height == bitmap->height();
}
void Tree::onPropertyChanged(TreeProperties* prop) {
diff --git a/libs/hwui/WebViewFunctorManager.cpp b/libs/hwui/WebViewFunctorManager.cpp
index 6fc251dc815c..9d16ee86739e 100644
--- a/libs/hwui/WebViewFunctorManager.cpp
+++ b/libs/hwui/WebViewFunctorManager.cpp
@@ -16,15 +16,16 @@
#include "WebViewFunctorManager.h"
+#include <log/log.h>
#include <private/hwui/WebViewFunctor.h>
+#include <utils/Trace.h>
+
+#include <atomic>
+
#include "Properties.h"
#include "renderthread/CanvasContext.h"
#include "renderthread/RenderThread.h"
-#include <log/log.h>
-#include <utils/Trace.h>
-#include <atomic>
-
namespace android::uirenderer {
namespace {
@@ -86,6 +87,10 @@ void WebViewFunctor_release(int functor) {
WebViewFunctorManager::instance().releaseFunctor(functor);
}
+void WebViewFunctor_reportRenderingThreads(int functor, const pid_t* thread_ids, size_t size) {
+ WebViewFunctorManager::instance().reportRenderingThreads(functor, thread_ids, size);
+}
+
static std::atomic_int sNextId{1};
WebViewFunctor::WebViewFunctor(void* data, const WebViewFunctorCallbacks& callbacks,
@@ -260,6 +265,10 @@ void WebViewFunctor::reparentSurfaceControl(ASurfaceControl* parent) {
funcs.transactionDeleteFunc(transaction);
}
+void WebViewFunctor::reportRenderingThreads(const pid_t* thread_ids, size_t size) {
+ mRenderingThreads = std::vector<pid_t>(thread_ids, thread_ids + size);
+}
+
WebViewFunctorManager& WebViewFunctorManager::instance() {
static WebViewFunctorManager sInstance;
return sInstance;
@@ -346,6 +355,32 @@ void WebViewFunctorManager::destroyFunctor(int functor) {
}
}
+void WebViewFunctorManager::reportRenderingThreads(int functor, const pid_t* thread_ids,
+ size_t size) {
+ std::lock_guard _lock{mLock};
+ for (auto& iter : mFunctors) {
+ if (iter->id() == functor) {
+ iter->reportRenderingThreads(thread_ids, size);
+ break;
+ }
+ }
+}
+
+std::vector<pid_t> WebViewFunctorManager::getRenderingThreadsForActiveFunctors() {
+ std::vector<pid_t> renderingThreads;
+ std::lock_guard _lock{mLock};
+ for (const auto& iter : mActiveFunctors) {
+ const auto& functorThreads = iter->getRenderingThreads();
+ for (const auto& tid : functorThreads) {
+ if (std::find(renderingThreads.begin(), renderingThreads.end(), tid) ==
+ renderingThreads.end()) {
+ renderingThreads.push_back(tid);
+ }
+ }
+ }
+ return renderingThreads;
+}
+
sp<WebViewFunctor::Handle> WebViewFunctorManager::handleFor(int functor) {
std::lock_guard _lock{mLock};
for (auto& iter : mActiveFunctors) {
diff --git a/libs/hwui/WebViewFunctorManager.h b/libs/hwui/WebViewFunctorManager.h
index 0a02f2d4b720..ec17640f9b5e 100644
--- a/libs/hwui/WebViewFunctorManager.h
+++ b/libs/hwui/WebViewFunctorManager.h
@@ -17,13 +17,11 @@
#pragma once
#include <private/hwui/WebViewFunctor.h>
-#ifdef __ANDROID__ // Layoutlib does not support render thread
#include <renderthread/RenderProxy.h>
-#endif
-
#include <utils/LightRefBase.h>
#include <utils/Log.h>
#include <utils/StrongPointer.h>
+
#include <mutex>
#include <vector>
@@ -38,11 +36,7 @@ public:
class Handle : public LightRefBase<Handle> {
public:
- ~Handle() {
-#ifdef __ANDROID__ // Layoutlib does not support render thread
- renderthread::RenderProxy::destroyFunctor(id());
-#endif
- }
+ ~Handle() { renderthread::RenderProxy::destroyFunctor(id()); }
int id() const { return mReference.id(); }
@@ -60,6 +54,10 @@ public:
void onRemovedFromTree() { mReference.onRemovedFromTree(); }
+ const std::vector<pid_t>& getRenderingThreads() const {
+ return mReference.getRenderingThreads();
+ }
+
private:
friend class WebViewFunctor;
@@ -81,6 +79,9 @@ public:
ASurfaceControl* getSurfaceControl();
void mergeTransaction(ASurfaceTransaction* transaction);
+ void reportRenderingThreads(const pid_t* thread_ids, size_t size);
+ const std::vector<pid_t>& getRenderingThreads() const { return mRenderingThreads; }
+
sp<Handle> createHandle() {
LOG_ALWAYS_FATAL_IF(mCreatedHandle);
mCreatedHandle = true;
@@ -100,6 +101,7 @@ private:
bool mCreatedHandle = false;
int32_t mParentSurfaceControlGenerationId = 0;
ASurfaceControl* mSurfaceControl = nullptr;
+ std::vector<pid_t> mRenderingThreads;
};
class WebViewFunctorManager {
@@ -110,6 +112,8 @@ public:
void releaseFunctor(int functor);
void onContextDestroyed();
void destroyFunctor(int functor);
+ void reportRenderingThreads(int functor, const pid_t* thread_ids, size_t size);
+ std::vector<pid_t> getRenderingThreadsForActiveFunctors();
sp<WebViewFunctor::Handle> handleFor(int functor);
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index b40c14a08680..cd3ae5342f4e 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -3,6 +3,7 @@ container: "system"
flag {
name: "clip_shader"
+ is_exported: true
namespace: "core_graphics"
description: "API for canvas shader clipping operations"
bug: "280116960"
@@ -10,6 +11,7 @@ flag {
flag {
name: "matrix_44"
+ is_exported: true
namespace: "core_graphics"
description: "API for 4x4 matrix and related canvas functions"
bug: "280116960"
@@ -17,6 +19,7 @@ flag {
flag {
name: "limited_hdr"
+ is_exported: true
namespace: "core_graphics"
description: "API to enable apps to restrict the amount of HDR headroom that is used"
bug: "234181960"
@@ -45,6 +48,7 @@ flag {
flag {
name: "gainmap_animations"
+ is_exported: true
namespace: "core_graphics"
description: "APIs to help enable animations involving gainmaps"
bug: "296482289"
@@ -52,6 +56,7 @@ flag {
flag {
name: "gainmap_constructor_with_metadata"
+ is_exported: true
namespace: "core_graphics"
description: "APIs to create a new gainmap with a bitmap for metadata."
bug: "304478551"
@@ -66,6 +71,7 @@ flag {
flag {
name: "requested_formats_v"
+ is_exported: true
namespace: "core_graphics"
description: "Enable r_8, r_16_uint, rg_1616_uint, and rgba_10101010 in the SDK"
bug: "292545615"
@@ -77,3 +83,17 @@ flag {
description: "Automatically animate all changes in HDR headroom"
bug: "314810174"
}
+
+flag {
+ name: "draw_region"
+ namespace: "core_graphics"
+ 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/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp
index 770822a049b7..70a9ef04d6f3 100644
--- a/libs/hwui/apex/LayoutlibLoader.cpp
+++ b/libs/hwui/apex/LayoutlibLoader.cpp
@@ -46,6 +46,7 @@ namespace android {
extern int register_android_graphics_Canvas(JNIEnv* env);
extern int register_android_graphics_CanvasProperty(JNIEnv* env);
+extern int register_android_graphics_Color(JNIEnv* env);
extern int register_android_graphics_ColorFilter(JNIEnv* env);
extern int register_android_graphics_ColorSpace(JNIEnv* env);
extern int register_android_graphics_DrawFilter(JNIEnv* env);
@@ -87,6 +88,7 @@ static const std::unordered_map<std::string, RegJNIRec> gRegJNIMap = {
{"android.graphics.Camera", REG_JNI(register_android_graphics_Camera)},
{"android.graphics.Canvas", REG_JNI(register_android_graphics_Canvas)},
{"android.graphics.CanvasProperty", REG_JNI(register_android_graphics_CanvasProperty)},
+ {"android.graphics.Color", REG_JNI(register_android_graphics_Color)},
{"android.graphics.ColorFilter", REG_JNI(register_android_graphics_ColorFilter)},
{"android.graphics.ColorSpace", REG_JNI(register_android_graphics_ColorSpace)},
{"android.graphics.CreateJavaOutputStreamAdaptor",
@@ -164,8 +166,10 @@ static vector<string> parseCsv(JNIEnv* env, jstring csvJString) {
} // namespace android
using namespace android;
+using namespace android::uirenderer;
void init_android_graphics() {
+ Properties::overrideRenderPipelineType(RenderPipelineType::SkiaCpu);
SkGraphics::Init();
}
diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp
index 883f273b5d3d..15b2bac50c79 100644
--- a/libs/hwui/apex/jni_runtime.cpp
+++ b/libs/hwui/apex/jni_runtime.cpp
@@ -49,6 +49,7 @@ namespace android {
extern int register_android_graphics_Canvas(JNIEnv* env);
extern int register_android_graphics_CanvasProperty(JNIEnv* env);
extern int register_android_graphics_ColorFilter(JNIEnv* env);
+extern int register_android_graphics_Color(JNIEnv* env);
extern int register_android_graphics_ColorSpace(JNIEnv* env);
extern int register_android_graphics_DrawFilter(JNIEnv* env);
extern int register_android_graphics_FontFamily(JNIEnv* env);
@@ -70,7 +71,6 @@ extern int register_android_graphics_fonts_Font(JNIEnv* env);
extern int register_android_graphics_fonts_FontFamily(JNIEnv* env);
extern int register_android_graphics_pdf_PdfDocument(JNIEnv* env);
extern int register_android_graphics_pdf_PdfEditor(JNIEnv* env);
-extern int register_android_graphics_pdf_PdfRenderer(JNIEnv* env);
extern int register_android_graphics_text_MeasuredText(JNIEnv* env);
extern int register_android_graphics_text_LineBreaker(JNIEnv *env);
extern int register_android_graphics_text_TextShaper(JNIEnv *env);
@@ -99,6 +99,7 @@ extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env);
static const RegJNIRec gRegJNI[] = {
REG_JNI(register_android_graphics_Canvas),
+ REG_JNI(register_android_graphics_Color),
// This needs to be before register_android_graphics_Graphics, or the latter
// will not be able to find the jmethodID for ColorSpace.get().
REG_JNI(register_android_graphics_ColorSpace),
@@ -142,7 +143,6 @@ extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env);
REG_JNI(register_android_graphics_fonts_FontFamily),
REG_JNI(register_android_graphics_pdf_PdfDocument),
REG_JNI(register_android_graphics_pdf_PdfEditor),
- REG_JNI(register_android_graphics_pdf_PdfRenderer),
REG_JNI(register_android_graphics_text_MeasuredText),
REG_JNI(register_android_graphics_text_LineBreaker),
REG_JNI(register_android_graphics_text_TextShaper),
@@ -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 3ebf7d19202d..eac03609d72f 100644
--- a/libs/hwui/effects/GainmapRenderer.cpp
+++ b/libs/hwui/effects/GainmapRenderer.cpp
@@ -32,6 +32,8 @@
#include "src/core/SkColorFilterPriv.h"
#include "src/core/SkImageInfoPriv.h"
#include "src/core/SkRuntimeEffectPriv.h"
+
+#include <cmath>
#endif
namespace android::uirenderer {
@@ -94,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;
@@ -115,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);
@@ -184,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 =
@@ -199,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);
@@ -206,12 +216,12 @@ private:
void setupGenericUniforms(const sk_sp<const SkImage>& gainmapImage,
const SkGainmapInfo& gainmapInfo) {
- const SkColor4f logRatioMin({sk_float_log(gainmapInfo.fGainmapRatioMin.fR),
- sk_float_log(gainmapInfo.fGainmapRatioMin.fG),
- sk_float_log(gainmapInfo.fGainmapRatioMin.fB), 1.f});
- const SkColor4f logRatioMax({sk_float_log(gainmapInfo.fGainmapRatioMax.fR),
- sk_float_log(gainmapInfo.fGainmapRatioMax.fG),
- sk_float_log(gainmapInfo.fGainmapRatioMax.fB), 1.f});
+ const SkColor4f logRatioMin({std::log(gainmapInfo.fGainmapRatioMin.fR),
+ std::log(gainmapInfo.fGainmapRatioMin.fG),
+ std::log(gainmapInfo.fGainmapRatioMin.fB), 1.f});
+ const SkColor4f logRatioMax({std::log(gainmapInfo.fGainmapRatioMax.fR),
+ std::log(gainmapInfo.fGainmapRatioMax.fG),
+ std::log(gainmapInfo.fGainmapRatioMax.fB), 1.f});
const int noGamma = gainmapInfo.fGainmapGamma.fR == 1.f &&
gainmapInfo.fGainmapGamma.fG == 1.f &&
gainmapInfo.fGainmapGamma.fB == 1.f;
@@ -248,10 +258,10 @@ private:
float W = 0.f;
if (targetHdrSdrRatio > mGainmapInfo.fDisplayRatioSdr) {
if (targetHdrSdrRatio < mGainmapInfo.fDisplayRatioHdr) {
- W = (sk_float_log(targetHdrSdrRatio) -
- sk_float_log(mGainmapInfo.fDisplayRatioSdr)) /
- (sk_float_log(mGainmapInfo.fDisplayRatioHdr) -
- sk_float_log(mGainmapInfo.fDisplayRatioSdr));
+ W = (std::log(targetHdrSdrRatio) -
+ std::log(mGainmapInfo.fDisplayRatioSdr)) /
+ (std::log(mGainmapInfo.fDisplayRatioHdr) -
+ std::log(mGainmapInfo.fDisplayRatioSdr));
} else {
W = 1.f;
}
diff --git a/libs/hwui/hwui/AnimatedImageDrawable.cpp b/libs/hwui/hwui/AnimatedImageDrawable.cpp
index 27773a60355a..69613c7d17cb 100644
--- a/libs/hwui/hwui/AnimatedImageDrawable.cpp
+++ b/libs/hwui/hwui/AnimatedImageDrawable.cpp
@@ -15,18 +15,16 @@
*/
#include "AnimatedImageDrawable.h"
-#ifdef __ANDROID__ // Layoutlib does not support AnimatedImageThread
-#include "AnimatedImageThread.h"
-#endif
-
-#include <gui/TraceUtils.h>
-#include "pipeline/skia/SkiaUtils.h"
#include <SkPicture.h>
#include <SkRefCnt.h>
+#include <gui/TraceUtils.h>
#include <optional>
+#include "AnimatedImageThread.h"
+#include "pipeline/skia/SkiaUtils.h"
+
namespace android {
AnimatedImageDrawable::AnimatedImageDrawable(sk_sp<SkAnimatedImage> animatedImage, size_t bytesUsed,
@@ -185,10 +183,8 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) {
} else if (starting) {
// The image has animated, and now is being reset. Queue up the first
// frame, but keep showing the current frame until the first is ready.
-#ifdef __ANDROID__ // Layoutlib does not support AnimatedImageThread
auto& thread = uirenderer::AnimatedImageThread::getInstance();
mNextSnapshot = thread.reset(sk_ref_sp(this));
-#endif
}
bool finalFrame = false;
@@ -214,10 +210,8 @@ void AnimatedImageDrawable::onDraw(SkCanvas* canvas) {
}
if (mRunning && !mNextSnapshot.valid()) {
-#ifdef __ANDROID__ // Layoutlib does not support AnimatedImageThread
auto& thread = uirenderer::AnimatedImageThread::getInstance();
mNextSnapshot = thread.decodeNextFrame(sk_ref_sp(this));
-#endif
}
if (!drawDirectly) {
diff --git a/libs/hwui/hwui/AnimatedImageThread.cpp b/libs/hwui/hwui/AnimatedImageThread.cpp
index 825dd4cf2bf1..e39c8d57d31c 100644
--- a/libs/hwui/hwui/AnimatedImageThread.cpp
+++ b/libs/hwui/hwui/AnimatedImageThread.cpp
@@ -16,7 +16,9 @@
#include "AnimatedImageThread.h"
+#ifdef __ANDROID__
#include <sys/resource.h>
+#endif
namespace android {
namespace uirenderer {
@@ -31,7 +33,9 @@ AnimatedImageThread& AnimatedImageThread::getInstance() {
}
AnimatedImageThread::AnimatedImageThread() {
+#ifdef __ANDROID__
setpriority(PRIO_PROCESS, 0, PRIORITY_NORMAL + PRIORITY_MORE_FAVORABLE);
+#endif
}
std::future<AnimatedImageDrawable::Snapshot> AnimatedImageThread::decodeNextFrame(
diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h
index 14b4f584f0f3..4eb6918d7e9a 100644
--- a/libs/hwui/hwui/Canvas.h
+++ b/libs/hwui/hwui/Canvas.h
@@ -34,7 +34,6 @@ class SkCanvasState;
class SkRRect;
class SkRuntimeShaderBuilder;
class SkVertices;
-class Mesh;
namespace minikin {
class Font;
@@ -61,6 +60,7 @@ typedef std::function<void(uint16_t* text, float* positions)> ReadGlyphFunc;
class AnimatedImageDrawable;
class Bitmap;
+class Mesh;
class Paint;
struct Typeface;
diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h
index 1fcb6920db14..cfca48084d97 100644
--- a/libs/hwui/hwui/DrawTextFunctor.h
+++ b/libs/hwui/hwui/DrawTextFunctor.h
@@ -34,7 +34,9 @@ namespace flags = com::android::graphics::hwui::flags;
namespace android {
-inline constexpr int kHighContrastTextBorderWidth = 4;
+// These should match the constants in framework/base/core/java/android/text/Layout.java
+inline constexpr float kHighContrastTextBorderWidth = 4.0f;
+inline constexpr float kHighContrastTextBorderWidthFactor = 0.2f;
static inline void drawStroke(SkScalar left, SkScalar right, SkScalar top, SkScalar thickness,
const Paint& paint, Canvas* canvas) {
@@ -48,7 +50,16 @@ static void simplifyPaint(int color, Paint* paint) {
paint->setShader(nullptr);
paint->setColorFilter(nullptr);
paint->setLooper(nullptr);
- paint->setStrokeWidth(kHighContrastTextBorderWidth + 0.04 * paint->getSkFont().getSize());
+
+ if (flags::high_contrast_text_small_text_rect()) {
+ paint->setStrokeWidth(
+ std::max(kHighContrastTextBorderWidth,
+ kHighContrastTextBorderWidthFactor * paint->getSkFont().getSize()));
+ } else {
+ auto borderWidthFactor = 0.04f;
+ paint->setStrokeWidth(kHighContrastTextBorderWidth +
+ borderWidthFactor * paint->getSkFont().getSize());
+ }
paint->setStrokeJoin(SkPaint::kRound_Join);
paint->setLooper(nullptr);
}
@@ -106,36 +117,7 @@ public:
Paint outlinePaint(paint);
simplifyPaint(darken ? SK_ColorWHITE : SK_ColorBLACK, &outlinePaint);
outlinePaint.setStyle(SkPaint::kStrokeAndFill_Style);
- if (flags::high_contrast_text_small_text_rect()) {
- const SkFont& font = paint.getSkFont();
- auto padding = kHighContrastTextBorderWidth + 0.1f * font.getSize();
-
- // Draw the background only behind each glyph's bounds. We do this instead of using
- // the bounds of the entire layout, because the layout includes alignment whitespace
- // etc which can obscure other text from separate passes (e.g. emojis).
- // Merge all the glyph bounds into one rect for this line, since drawing a rect for
- // each glyph is expensive.
- SkRect glyphBounds;
- SkRect bgBounds;
- for (size_t i = start; i < end; i++) {
- auto glyph = layout.getGlyphId(i);
-
- font.getBounds(reinterpret_cast<const SkGlyphID*>(&glyph), 1, &glyphBounds,
- &paint);
- glyphBounds.offset(layout.getX(i), layout.getY(i));
-
- bgBounds.join(glyphBounds);
- }
-
- if (!bgBounds.isEmpty()) {
- bgBounds.offset(x, y);
- bgBounds.outset(padding, padding);
- canvas->drawRect(bgBounds.fLeft, bgBounds.fTop, bgBounds.fRight,
- bgBounds.fBottom, outlinePaint);
- }
- } else {
- canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, totalAdvance);
- }
+ canvas->drawGlyphs(glyphFunc, glyphCount, outlinePaint, x, y, totalAdvance);
// inner
gDrawTextBlobMode = DrawTextBlobMode::HctInner;
diff --git a/libs/hwui/jni/AnimatedImageDrawable.cpp b/libs/hwui/jni/AnimatedImageDrawable.cpp
index 90b1da846205..b01e38d014a9 100644
--- a/libs/hwui/jni/AnimatedImageDrawable.cpp
+++ b/libs/hwui/jni/AnimatedImageDrawable.cpp
@@ -25,7 +25,11 @@
#include <hwui/AnimatedImageDrawable.h>
#include <hwui/Canvas.h>
#include <hwui/ImageDecoder.h>
+#ifdef __ANDROID__
#include <utils/Looper.h>
+#else
+#include "utils/MessageHandler.h"
+#endif
#include "ColorFilter.h"
#include "GraphicsJNI.h"
@@ -204,6 +208,7 @@ private:
};
class JniAnimationEndListener : public OnAnimationEndListener {
+#ifdef __ANDROID__
public:
JniAnimationEndListener(sp<Looper>&& looper, JNIEnv* env, jobject javaObject) {
mListener = new InvokeListener(env, javaObject);
@@ -215,6 +220,17 @@ public:
private:
sp<InvokeListener> mListener;
sp<Looper> mLooper;
+#else
+public:
+ JniAnimationEndListener(JNIEnv* env, jobject javaObject) {
+ mListener = new InvokeListener(env, javaObject);
+ }
+
+ void onAnimationEnd() override { mListener->handleMessage(0); }
+
+private:
+ sp<InvokeListener> mListener;
+#endif
};
static void AnimatedImageDrawable_nSetOnAnimationEndListener(JNIEnv* env, jobject /*clazz*/,
@@ -223,6 +239,7 @@ static void AnimatedImageDrawable_nSetOnAnimationEndListener(JNIEnv* env, jobjec
if (!jdrawable) {
drawable->setOnAnimationEndListener(nullptr);
} else {
+#ifdef __ANDROID__
sp<Looper> looper = Looper::getForThread();
if (!looper.get()) {
doThrowISE(env,
@@ -233,6 +250,10 @@ static void AnimatedImageDrawable_nSetOnAnimationEndListener(JNIEnv* env, jobjec
drawable->setOnAnimationEndListener(
std::make_unique<JniAnimationEndListener>(std::move(looper), env, jdrawable));
+#else
+ drawable->setOnAnimationEndListener(
+ std::make_unique<JniAnimationEndListener>(env, jdrawable));
+#endif
}
}
diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp
index 9e21f860ce21..d4157008ca46 100644
--- a/libs/hwui/jni/Bitmap.cpp
+++ b/libs/hwui/jni/Bitmap.cpp
@@ -1,8 +1,14 @@
// #define LOG_NDEBUG 0
#include "Bitmap.h"
+#include <android-base/unique_fd.h>
#include <hwui/Bitmap.h>
#include <hwui/Paint.h>
+#include <inttypes.h>
+#include <renderthread/RenderProxy.h>
+#include <string.h>
+
+#include <memory>
#include "CreateJavaOutputStreamAdaptor.h"
#include "Gainmap.h"
@@ -24,16 +30,6 @@
#include "SkTypes.h"
#include "android_nio_utils.h"
-#ifdef __ANDROID__ // Layoutlib does not support graphic buffer, parcel or render thread
-#include <android-base/unique_fd.h>
-#include <renderthread/RenderProxy.h>
-#endif
-
-#include <inttypes.h>
-#include <string.h>
-
-#include <memory>
-
#define DEBUG_PARCEL 0
static jclass gBitmap_class;
@@ -1105,11 +1101,9 @@ static jboolean Bitmap_sameAs(JNIEnv* env, jobject, jlong bm0Handle, jlong bm1Ha
}
static void Bitmap_prepareToDraw(JNIEnv* env, jobject, jlong bitmapPtr) {
-#ifdef __ANDROID__ // Layoutlib does not support render thread
LocalScopedBitmap bitmapHandle(bitmapPtr);
if (!bitmapHandle.valid()) return;
android::uirenderer::renderthread::RenderProxy::prepareToDraw(bitmapHandle->bitmap());
-#endif
}
static jint Bitmap_getAllocationByteCount(JNIEnv* env, jobject, jlong bitmapPtr) {
diff --git a/libs/hwui/jni/BitmapFactory.cpp b/libs/hwui/jni/BitmapFactory.cpp
index 3d0a53440bfb..785aef312072 100644
--- a/libs/hwui/jni/BitmapFactory.cpp
+++ b/libs/hwui/jni/BitmapFactory.cpp
@@ -688,8 +688,8 @@ static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteA
static jobject nativeDecodeFileDescriptor(JNIEnv* env, jobject clazz, jobject fileDescriptor,
jobject padding, jobject bitmapFactoryOptions, jlong inBitmapHandle, jlong colorSpaceHandle) {
-#ifndef __ANDROID__ // LayoutLib for Windows does not support F_DUPFD_CLOEXEC
- return nullObjectReturn("Not supported on Windows");
+#ifdef _WIN32 // LayoutLib for Windows does not support F_DUPFD_CLOEXEC
+ return nullObjectReturn("Not supported on Windows");
#else
NPE_CHECK_RETURN_ZERO(env, fileDescriptor);
diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index 8315c4c0dd4d..a88139d6b5d6 100644
--- a/libs/hwui/jni/Graphics.cpp
+++ b/libs/hwui/jni/Graphics.cpp
@@ -211,11 +211,7 @@ static jclass gRegion_class;
static jfieldID gRegion_nativeInstanceID;
static jmethodID gRegion_constructorMethodID;
-static jclass gByte_class;
-static jobject gVMRuntime;
-static jclass gVMRuntime_class;
-static jmethodID gVMRuntime_newNonMovableArray;
-static jmethodID gVMRuntime_addressOf;
+static jclass gByte_class;
static jclass gColorSpace_class;
static jmethodID gColorSpace_getMethodID;
@@ -587,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],
@@ -789,13 +795,6 @@ int register_android_graphics_Graphics(JNIEnv* env)
gByte_class = (jclass) env->NewGlobalRef(
env->GetStaticObjectField(c, env->GetStaticFieldID(c, "TYPE", "Ljava/lang/Class;")));
- gVMRuntime_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "dalvik/system/VMRuntime"));
- m = env->GetStaticMethodID(gVMRuntime_class, "getRuntime", "()Ldalvik/system/VMRuntime;");
- gVMRuntime = env->NewGlobalRef(env->CallStaticObjectMethod(gVMRuntime_class, m));
- gVMRuntime_newNonMovableArray = GetMethodIDOrDie(env, gVMRuntime_class, "newNonMovableArray",
- "(Ljava/lang/Class;I)Ljava/lang/Object;");
- gVMRuntime_addressOf = GetMethodIDOrDie(env, gVMRuntime_class, "addressOf", "(Ljava/lang/Object;)J");
-
gColorSpace_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/ColorSpace"));
gColorSpace_getMethodID = GetStaticMethodIDOrDie(env, gColorSpace_class,
"get", "(Landroid/graphics/ColorSpace$Named;)Landroid/graphics/ColorSpace;");
diff --git a/libs/hwui/jni/HardwareBufferHelpers.cpp b/libs/hwui/jni/HardwareBufferHelpers.cpp
index 7e3f771b6b3d..d3b48d36b677 100644
--- a/libs/hwui/jni/HardwareBufferHelpers.cpp
+++ b/libs/hwui/jni/HardwareBufferHelpers.cpp
@@ -16,7 +16,9 @@
#include "HardwareBufferHelpers.h"
+#ifdef __ANDROID__
#include <dlfcn.h>
+#endif
#include <log/log.h>
#ifdef __ANDROID__
diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp
index a952be020855..2a057e7a4cdc 100644
--- a/libs/hwui/jni/Shader.cpp
+++ b/libs/hwui/jni/Shader.cpp
@@ -36,25 +36,6 @@ static const uint32_t sGradientShaderFlags = SkGradientShader::kInterpolateColor
return 0; \
}
-static void Color_RGBToHSV(JNIEnv* env, jobject, jint red, jint green, jint blue, jfloatArray hsvArray)
-{
- SkScalar hsv[3];
- SkRGBToHSV(red, green, blue, hsv);
-
- AutoJavaFloatArray autoHSV(env, hsvArray, 3);
- float* values = autoHSV.ptr();
- for (int i = 0; i < 3; i++) {
- values[i] = SkScalarToFloat(hsv[i]);
- }
-}
-
-static jint Color_HSVToColor(JNIEnv* env, jobject, jint alpha, jfloatArray hsvArray)
-{
- AutoJavaFloatArray autoHSV(env, hsvArray, 3);
- SkScalar* hsv = autoHSV.ptr();
- return static_cast<jint>(SkHSVToColor(alpha, hsv));
-}
-
///////////////////////////////////////////////////////////////////////////////////////////////
static void Shader_safeUnref(SkShader* shader) {
@@ -409,11 +390,6 @@ static void RuntimeShader_updateShader(JNIEnv* env, jobject, jlong shaderBuilder
///////////////////////////////////////////////////////////////////////////////////////////////
-static const JNINativeMethod gColorMethods[] = {
- { "nativeRGBToHSV", "(III[F)V", (void*)Color_RGBToHSV },
- { "nativeHSVToColor", "(I[F)I", (void*)Color_HSVToColor }
-};
-
static const JNINativeMethod gShaderMethods[] = {
{ "nativeGetFinalizer", "()J", (void*)Shader_getNativeFinalizer },
};
@@ -456,8 +432,6 @@ static const JNINativeMethod gRuntimeShaderMethods[] = {
int register_android_graphics_Shader(JNIEnv* env)
{
- android::RegisterMethodsOrDie(env, "android/graphics/Color", gColorMethods,
- NELEM(gColorMethods));
android::RegisterMethodsOrDie(env, "android/graphics/Shader", gShaderMethods,
NELEM(gShaderMethods));
android::RegisterMethodsOrDie(env, "android/graphics/BitmapShader", gBitmapShaderMethods,
diff --git a/libs/hwui/jni/android_graphics_Color.cpp b/libs/hwui/jni/android_graphics_Color.cpp
new file mode 100644
index 000000000000..c22b8b926373
--- /dev/null
+++ b/libs/hwui/jni/android_graphics_Color.cpp
@@ -0,0 +1,55 @@
+/*
+ * 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 "GraphicsJNI.h"
+
+#include "SkColor.h"
+
+using namespace android;
+
+static void Color_RGBToHSV(JNIEnv* env, jobject, jint red, jint green, jint blue,
+ jfloatArray hsvArray)
+{
+ SkScalar hsv[3];
+ SkRGBToHSV(red, green, blue, hsv);
+
+ AutoJavaFloatArray autoHSV(env, hsvArray, 3);
+ float* values = autoHSV.ptr();
+ for (int i = 0; i < 3; i++) {
+ values[i] = SkScalarToFloat(hsv[i]);
+ }
+}
+
+static jint Color_HSVToColor(JNIEnv* env, jobject, jint alpha, jfloatArray hsvArray)
+{
+ AutoJavaFloatArray autoHSV(env, hsvArray, 3);
+ SkScalar* hsv = autoHSV.ptr();
+ return static_cast<jint>(SkHSVToColor(alpha, hsv));
+}
+
+static const JNINativeMethod gColorMethods[] = {
+ { "nativeRGBToHSV", "(III[F)V", (void*)Color_RGBToHSV },
+ { "nativeHSVToColor", "(I[F)I", (void*)Color_HSVToColor }
+};
+
+namespace android {
+
+int register_android_graphics_Color(JNIEnv* env) {
+ return android::RegisterMethodsOrDie(env, "android/graphics/Color", gColorMethods,
+ NELEM(gColorMethods));
+}
+
+}; // namespace android
diff --git a/libs/hwui/jni/android_graphics_ColorSpace.cpp b/libs/hwui/jni/android_graphics_ColorSpace.cpp
index 63d3f83febd6..d06206be90d7 100644
--- a/libs/hwui/jni/android_graphics_ColorSpace.cpp
+++ b/libs/hwui/jni/android_graphics_ColorSpace.cpp
@@ -148,7 +148,7 @@ static const JNINativeMethod gColorSpaceRgbMethods[] = {
namespace android {
int register_android_graphics_ColorSpace(JNIEnv* env) {
- return android::RegisterMethodsOrDie(env, "android/graphics/ColorSpace$Rgb",
+ return android::RegisterMethodsOrDie(env, "android/graphics/ColorSpace$Rgb$Native",
gColorSpaceRgbMethods, NELEM(gColorSpaceRgbMethods));
}
diff --git a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp
index 426644ee6a4e..948362c30a31 100644
--- a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp
+++ b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp
@@ -16,22 +16,19 @@
#include "GraphicsJNI.h"
-#ifdef __ANDROID__ // Layoutlib does not support Looper and device properties
+#ifdef __ANDROID__ // Layoutlib does not support Looper
#include <utils/Looper.h>
#endif
-#include <SkRegion.h>
-#include <SkRuntimeEffect.h>
-
+#include <CanvasProperty.h>
#include <Rect.h>
#include <RenderNode.h>
-#include <CanvasProperty.h>
+#include <SkRegion.h>
+#include <SkRuntimeEffect.h>
#include <hwui/Canvas.h>
#include <hwui/Paint.h>
#include <minikin/Layout.h>
-#ifdef __ANDROID__ // Layoutlib does not support RenderThread
#include <renderthread/RenderProxy.h>
-#endif
namespace android {
@@ -85,11 +82,7 @@ static void android_view_DisplayListCanvas_resetDisplayListCanvas(CRITICAL_JNI_P
}
static jint android_view_DisplayListCanvas_getMaxTextureSize(JNIEnv*, jobject) {
-#ifdef __ANDROID__ // Layoutlib does not support RenderProxy (RenderThread)
return android::uirenderer::renderthread::RenderProxy::maxTextureSize();
-#else
- return 4096;
-#endif
}
static void android_view_DisplayListCanvas_enableZ(CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr,
diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
index d15b1680de94..df9f83036709 100644
--- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
+++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp
@@ -25,13 +25,16 @@
#include <SkColorSpace.h>
#include <SkData.h>
#include <SkImage.h>
+#ifdef __ANDROID__
#include <SkImageAndroid.h>
+#else
+#include <SkImagePriv.h>
+#endif
#include <SkPicture.h>
#include <SkPixmap.h>
#include <SkSerialProcs.h>
#include <SkStream.h>
#include <SkTypeface.h>
-#include <dlfcn.h>
#include <gui/TraceUtils.h>
#include <include/encode/SkPngEncoder.h>
#include <inttypes.h>
@@ -39,8 +42,10 @@
#include <media/NdkImage.h>
#include <media/NdkImageReader.h>
#include <nativehelper/JNIPlatformHelp.h>
+#ifdef __ANDROID__
#include <pipeline/skia/ShaderCache.h>
#include <private/EGL/cache.h>
+#endif
#include <renderthread/CanvasContext.h>
#include <renderthread/RenderProxy.h>
#include <renderthread/RenderTask.h>
@@ -59,6 +64,7 @@
#include "JvmErrorReporter.h"
#include "android_graphics_HardwareRendererObserver.h"
#include "utils/ForceDark.h"
+#include "utils/SharedLib.h"
namespace android {
@@ -498,7 +504,11 @@ public:
return sk_ref_sp(img);
}
bm.setImmutable();
+#ifdef __ANDROID__
return SkImages::PinnableRasterFromBitmap(bm);
+#else
+ return SkMakeImageFromRasterBitmap(bm, kNever_SkCopyPixelsMode);
+#endif
}
return sk_ref_sp(img);
}
@@ -713,6 +723,7 @@ public:
static jobject android_view_ThreadedRenderer_createHardwareBitmapFromRenderNode(JNIEnv* env,
jobject clazz, jlong renderNodePtr, jint jwidth, jint jheight) {
+#ifdef __ANDROID__
RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
if (jwidth <= 0 || jheight <= 0) {
ALOGW("Invalid width %d or height %d", jwidth, jheight);
@@ -796,6 +807,9 @@ static jobject android_view_ThreadedRenderer_createHardwareBitmapFromRenderNode(
sk_sp<Bitmap> bitmap = Bitmap::createFrom(buffer, cs);
return bitmap::createBitmap(env, bitmap.release(),
android::bitmap::kBitmapCreateFlag_Premultiplied);
+#else
+ return nullptr;
+#endif
}
static void android_view_ThreadedRenderer_disableVsync(JNIEnv*, jclass) {
@@ -860,7 +874,8 @@ static void android_view_ThreadedRenderer_setDisplayDensityDpi(JNIEnv*, jclass,
static void android_view_ThreadedRenderer_initDisplayInfo(
JNIEnv* env, jclass, jint physicalWidth, jint physicalHeight, jfloat refreshRate,
jint wideColorDataspace, jlong appVsyncOffsetNanos, jlong presentationDeadlineNanos,
- jboolean supportFp16ForHdr, jboolean supportMixedColorSpaces) {
+ jboolean supportFp16ForHdr, jboolean supportRgba10101010ForHdr,
+ jboolean supportMixedColorSpaces) {
DeviceInfo::setWidth(physicalWidth);
DeviceInfo::setHeight(physicalHeight);
DeviceInfo::setRefreshRate(refreshRate);
@@ -868,6 +883,7 @@ static void android_view_ThreadedRenderer_initDisplayInfo(
DeviceInfo::setAppVsyncOffsetNanos(appVsyncOffsetNanos);
DeviceInfo::setPresentationDeadlineNanos(presentationDeadlineNanos);
DeviceInfo::setSupportFp16ForHdr(supportFp16ForHdr);
+ DeviceInfo::setSupportRgba10101010ForHdr(supportRgba10101010ForHdr);
DeviceInfo::setSupportMixedColorSpaces(supportMixedColorSpaces);
}
@@ -907,6 +923,7 @@ static void android_view_ThreadedRenderer_removeObserver(JNIEnv* env, jclass cla
static void android_view_ThreadedRenderer_setupShadersDiskCache(JNIEnv* env, jobject clazz,
jstring diskCachePath, jstring skiaDiskCachePath) {
+#ifdef __ANDROID__
const char* cacheArray = env->GetStringUTFChars(diskCachePath, NULL);
android::egl_set_cache_filename(cacheArray);
env->ReleaseStringUTFChars(diskCachePath, cacheArray);
@@ -914,6 +931,7 @@ static void android_view_ThreadedRenderer_setupShadersDiskCache(JNIEnv* env, job
const char* skiaCacheArray = env->GetStringUTFChars(skiaDiskCachePath, NULL);
uirenderer::skiapipeline::ShaderCache::get().setFilename(skiaCacheArray);
env->ReleaseStringUTFChars(skiaDiskCachePath, skiaCacheArray);
+#endif
}
static jboolean android_view_ThreadedRenderer_isWebViewOverlaysEnabled(JNIEnv* env, jobject clazz) {
@@ -1020,7 +1038,7 @@ static const JNINativeMethod gMethods[] = {
{"nSetForceDark", "(JI)V", (void*)android_view_ThreadedRenderer_setForceDark},
{"nSetDisplayDensityDpi", "(I)V",
(void*)android_view_ThreadedRenderer_setDisplayDensityDpi},
- {"nInitDisplayInfo", "(IIFIJJZZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo},
+ {"nInitDisplayInfo", "(IIFIJJZZZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo},
{"preload", "()V", (void*)android_view_ThreadedRenderer_preload},
{"isWebViewOverlaysEnabled", "()Z",
(void*)android_view_ThreadedRenderer_isWebViewOverlaysEnabled},
@@ -1090,8 +1108,12 @@ int register_android_view_ThreadedRenderer(JNIEnv* env) {
gCopyRequest.getDestinationBitmap =
GetMethodIDOrDie(env, copyRequest, "getDestinationBitmap", "(II)J");
- void* handle_ = dlopen("libandroid.so", RTLD_NOW | RTLD_NODELETE);
- fromSurface = (ANW_fromSurface)dlsym(handle_, "ANativeWindow_fromSurface");
+#ifdef __ANDROID__
+ void* handle_ = SharedLib::openSharedLib("libandroid");
+#else
+ void* handle_ = SharedLib::openSharedLib("libandroid_runtime");
+#endif
+ fromSurface = (ANW_fromSurface)SharedLib::getSymbol(handle_, "ANativeWindow_fromSurface");
LOG_ALWAYS_FATAL_IF(fromSurface == nullptr,
"Failed to find required symbol ANativeWindow_fromSurface!");
diff --git a/libs/hwui/jni/android_graphics_Matrix.cpp b/libs/hwui/jni/android_graphics_Matrix.cpp
index ca667b0d09bc..eedc069ed01b 100644
--- a/libs/hwui/jni/android_graphics_Matrix.cpp
+++ b/libs/hwui/jni/android_graphics_Matrix.cpp
@@ -326,9 +326,6 @@ public:
};
static const JNINativeMethod methods[] = {
- {"nGetNativeFinalizer", "()J", (void*) SkMatrixGlue::getNativeFinalizer},
- {"nCreate","(J)J", (void*) SkMatrixGlue::create},
-
// ------- @FastNative below here ---------------
{"nMapPoints","(J[FI[FIIZ)V", (void*) SkMatrixGlue::mapPoints},
{"nMapRect","(JLandroid/graphics/RectF;Landroid/graphics/RectF;)Z",
@@ -376,11 +373,21 @@ static const JNINativeMethod methods[] = {
{"nEquals", "(JJ)Z", (void*) SkMatrixGlue::equals}
};
+static const JNINativeMethod extra_methods[] = {
+ {"nGetNativeFinalizer", "()J", (void*)SkMatrixGlue::getNativeFinalizer},
+ {"nCreate", "(J)J", (void*)SkMatrixGlue::create},
+};
+
static jclass sClazz;
static jfieldID sNativeInstanceField;
static jmethodID sCtor;
int register_android_graphics_Matrix(JNIEnv* env) {
+ // Methods only used on Ravenwood (for now). See the javadoc on Matrix$ExtraNativesx
+ // for why we need it.
+ RegisterMethodsOrDie(env, "android/graphics/Matrix$ExtraNatives", extra_methods,
+ NELEM(extra_methods));
+
int result = RegisterMethodsOrDie(env, "android/graphics/Matrix", methods, NELEM(methods));
jclass clazz = FindClassOrDie(env, "android/graphics/Matrix");
diff --git a/libs/hwui/jni/android_graphics_Mesh.cpp b/libs/hwui/jni/android_graphics_Mesh.cpp
index 5cb43e54e499..3109de5055ca 100644
--- a/libs/hwui/jni/android_graphics_Mesh.cpp
+++ b/libs/hwui/jni/android_graphics_Mesh.cpp
@@ -38,8 +38,8 @@ static jlong make(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobject verte
return 0;
}
auto skRect = SkRect::MakeLTRB(left, top, right, bottom);
- auto meshPtr = new Mesh(skMeshSpec, mode, std::move(buffer), vertexCount, vertexOffset,
- std::make_unique<MeshUniformBuilder>(skMeshSpec), skRect);
+ auto meshPtr = new Mesh(skMeshSpec, static_cast<SkMesh::Mode>(mode), std::move(buffer),
+ vertexCount, vertexOffset, skRect);
auto [valid, msg] = meshPtr->validate();
if (!valid) {
jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", msg.c_str());
@@ -63,9 +63,9 @@ static jlong makeIndexed(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobjec
return 0;
}
auto skRect = SkRect::MakeLTRB(left, top, right, bottom);
- auto meshPtr = new Mesh(skMeshSpec, mode, std::move(vBuf), vertexCount, vertexOffset,
- std::move(iBuf), indexCount, indexOffset,
- std::make_unique<MeshUniformBuilder>(skMeshSpec), skRect);
+ auto meshPtr =
+ new Mesh(skMeshSpec, static_cast<SkMesh::Mode>(mode), std::move(vBuf), vertexCount,
+ vertexOffset, std::move(iBuf), indexCount, indexOffset, skRect);
auto [valid, msg] = meshPtr->validate();
if (!valid) {
jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", msg.c_str());
@@ -133,7 +133,6 @@ static void updateFloatUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring
ScopedUtfChars name(env, uniformName);
const float values[4] = {value1, value2, value3, value4};
nativeUpdateFloatUniforms(env, wrapper->uniformBuilder(), name.c_str(), values, count, false);
- wrapper->markDirty();
}
static void updateFloatArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring jUniformName,
@@ -143,7 +142,6 @@ static void updateFloatArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, js
AutoJavaFloatArray autoValues(env, jvalues, 0, kRO_JNIAccess);
nativeUpdateFloatUniforms(env, wrapper->uniformBuilder(), name.c_str(), autoValues.ptr(),
autoValues.length(), isColor);
- wrapper->markDirty();
}
static void nativeUpdateIntUniforms(JNIEnv* env, MeshUniformBuilder* builder,
@@ -166,7 +164,6 @@ static void updateIntUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring u
ScopedUtfChars name(env, uniformName);
const int values[4] = {value1, value2, value3, value4};
nativeUpdateIntUniforms(env, wrapper->uniformBuilder(), name.c_str(), values, count);
- wrapper->markDirty();
}
static void updateIntArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName,
@@ -176,7 +173,6 @@ static void updateIntArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstr
AutoJavaIntArray autoValues(env, values, 0);
nativeUpdateIntUniforms(env, wrapper->uniformBuilder(), name.c_str(), autoValues.ptr(),
autoValues.length());
- wrapper->markDirty();
}
static void MeshWrapper_destroy(Mesh* wrapper) {
diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp
index a7d64231da80..6e03bbd0fa16 100644
--- a/libs/hwui/jni/android_graphics_RenderNode.cpp
+++ b/libs/hwui/jni/android_graphics_RenderNode.cpp
@@ -15,19 +15,17 @@
*/
#define ATRACE_TAG ATRACE_TAG_VIEW
-#include "GraphicsJNI.h"
-
#include <Animator.h>
#include <DamageAccumulator.h>
#include <Matrix.h>
#include <RenderNode.h>
-#ifdef __ANDROID__ // Layoutlib does not support CanvasContext
-#include <renderthread/CanvasContext.h>
-#endif
#include <TreeInfo.h>
#include <effects/StretchEffect.h>
#include <gui/TraceUtils.h>
#include <hwui/Paint.h>
+#include <renderthread/CanvasContext.h>
+
+#include "GraphicsJNI.h"
namespace android {
@@ -640,7 +638,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
ATRACE_NAME("Update SurfaceView position");
-#ifdef __ANDROID__ // Layoutlib does not support CanvasContext
JNIEnv* env = jnienv();
// Update the new position synchronously. We cannot defer this to
// a worker pool to process asynchronously because the UI thread
@@ -669,7 +666,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
env->DeleteGlobalRef(mListener);
mListener = nullptr;
}
-#endif
}
virtual void onPositionLost(RenderNode& node, const TreeInfo* info) override {
@@ -682,7 +678,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
ATRACE_NAME("SurfaceView position lost");
JNIEnv* env = jnienv();
-#ifdef __ANDROID__ // Layoutlib does not support CanvasContext
// Update the lost position synchronously. We cannot defer this to
// a worker pool to process asynchronously because the UI thread
// may be unblocked by the time a worker thread can process this,
@@ -698,7 +693,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
env->DeleteGlobalRef(mListener);
mListener = nullptr;
}
-#endif
}
private:
@@ -750,7 +744,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
StretchEffectBehavior::Shader) {
JNIEnv* env = jnienv();
-#ifdef __ANDROID__ // Layoutlib does not support CanvasContext
SkVector stretchDirection = effect->getStretchDirection();
jboolean keepListening = env->CallStaticBooleanMethod(
gPositionListener.clazz, gPositionListener.callApplyStretch, mListener,
@@ -762,7 +755,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject,
env->DeleteGlobalRef(mListener);
mListener = nullptr;
}
-#endif
}
}
diff --git a/libs/hwui/jni/pdf/PdfRenderer.cpp b/libs/hwui/jni/pdf/PdfRenderer.cpp
deleted file mode 100644
index cc1f96197c74..000000000000
--- a/libs/hwui/jni/pdf/PdfRenderer.cpp
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2014 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 "PdfUtils.h"
-
-#include "GraphicsJNI.h"
-#include "SkBitmap.h"
-#include "SkMatrix.h"
-#include "fpdfview.h"
-
-#include <vector>
-#include <utils/Log.h>
-#include <unistd.h>
-#include <sys/types.h>
-#include <unistd.h>
-
-namespace android {
-
-static const int RENDER_MODE_FOR_DISPLAY = 1;
-static const int RENDER_MODE_FOR_PRINT = 2;
-
-static struct {
- jfieldID x;
- jfieldID y;
-} gPointClassInfo;
-
-static jlong nativeOpenPageAndGetSize(JNIEnv* env, jclass thiz, jlong documentPtr,
- jint pageIndex, jobject outSize) {
- FPDF_DOCUMENT document = reinterpret_cast<FPDF_DOCUMENT>(documentPtr);
-
- FPDF_PAGE page = FPDF_LoadPage(document, pageIndex);
- if (!page) {
- jniThrowException(env, "java/lang/IllegalStateException",
- "cannot load page");
- return -1;
- }
-
- double width = 0;
- double height = 0;
-
- int result = FPDF_GetPageSizeByIndex(document, pageIndex, &width, &height);
- if (!result) {
- jniThrowException(env, "java/lang/IllegalStateException",
- "cannot get page size");
- return -1;
- }
-
- env->SetIntField(outSize, gPointClassInfo.x, width);
- env->SetIntField(outSize, gPointClassInfo.y, height);
-
- return reinterpret_cast<jlong>(page);
-}
-
-static void nativeClosePage(JNIEnv* env, jclass thiz, jlong pagePtr) {
- FPDF_PAGE page = reinterpret_cast<FPDF_PAGE>(pagePtr);
- FPDF_ClosePage(page);
-}
-
-static void nativeRenderPage(JNIEnv* env, jclass thiz, jlong documentPtr, jlong pagePtr,
- jlong bitmapPtr, jint clipLeft, jint clipTop, jint clipRight, jint clipBottom,
- jlong transformPtr, jint renderMode) {
- FPDF_PAGE page = reinterpret_cast<FPDF_PAGE>(pagePtr);
-
- SkBitmap skBitmap;
- bitmap::toBitmap(bitmapPtr).getSkBitmap(&skBitmap);
-
- const int stride = skBitmap.width() * 4;
-
- FPDF_BITMAP bitmap = FPDFBitmap_CreateEx(skBitmap.width(), skBitmap.height(),
- FPDFBitmap_BGRA, skBitmap.getPixels(), stride);
-
- int renderFlags = FPDF_REVERSE_BYTE_ORDER;
- if (renderMode == RENDER_MODE_FOR_DISPLAY) {
- renderFlags |= FPDF_LCD_TEXT;
- } else if (renderMode == RENDER_MODE_FOR_PRINT) {
- renderFlags |= FPDF_PRINTING;
- }
-
- SkMatrix matrix = *reinterpret_cast<SkMatrix*>(transformPtr);
- SkScalar transformValues[6];
- if (!matrix.asAffine(transformValues)) {
- jniThrowException(env, "java/lang/IllegalArgumentException",
- "transform matrix has perspective. Only affine matrices are allowed.");
- return;
- }
-
- FS_MATRIX transform = {transformValues[SkMatrix::kAScaleX], transformValues[SkMatrix::kASkewY],
- transformValues[SkMatrix::kASkewX], transformValues[SkMatrix::kAScaleY],
- transformValues[SkMatrix::kATransX],
- transformValues[SkMatrix::kATransY]};
-
- FS_RECTF clip = {(float) clipLeft, (float) clipTop, (float) clipRight, (float) clipBottom};
-
- FPDF_RenderPageBitmapWithMatrix(bitmap, page, &transform, &clip, renderFlags);
-
- skBitmap.notifyPixelsChanged();
-}
-
-static const JNINativeMethod gPdfRenderer_Methods[] = {
- {"nativeCreate", "(IJ)J", (void*) nativeOpen},
- {"nativeClose", "(J)V", (void*) nativeClose},
- {"nativeGetPageCount", "(J)I", (void*) nativeGetPageCount},
- {"nativeScaleForPrinting", "(J)Z", (void*) nativeScaleForPrinting},
- {"nativeRenderPage", "(JJJIIIIJI)V", (void*) nativeRenderPage},
- {"nativeOpenPageAndGetSize", "(JILandroid/graphics/Point;)J", (void*) nativeOpenPageAndGetSize},
- {"nativeClosePage", "(J)V", (void*) nativeClosePage}
-};
-
-int register_android_graphics_pdf_PdfRenderer(JNIEnv* env) {
- int result = RegisterMethodsOrDie(
- env, "android/graphics/pdf/PdfRenderer", gPdfRenderer_Methods,
- NELEM(gPdfRenderer_Methods));
-
- jclass clazz = FindClassOrDie(env, "android/graphics/Point");
- gPointClassInfo.x = GetFieldIDOrDie(env, clazz, "x", "I");
- gPointClassInfo.y = GetFieldIDOrDie(env, clazz, "y", "I");
-
- return result;
-};
-
-};
diff --git a/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp b/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp
new file mode 100644
index 000000000000..5bbbc1009541
--- /dev/null
+++ b/libs/hwui/pipeline/skia/SkiaCpuPipeline.cpp
@@ -0,0 +1,132 @@
+/*
+ * 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 "pipeline/skia/SkiaCpuPipeline.h"
+
+#include <system/window.h>
+
+#include "DeviceInfo.h"
+#include "LightingInfo.h"
+#include "renderthread/Frame.h"
+#include "utils/Color.h"
+
+using namespace android::uirenderer::renderthread;
+
+namespace android {
+namespace uirenderer {
+namespace skiapipeline {
+
+void SkiaCpuPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) {
+ // Render all layers that need to be updated, in order.
+ for (size_t i = 0; i < layers.entries().size(); i++) {
+ RenderNode* layerNode = layers.entries()[i].renderNode.get();
+ // only schedule repaint if node still on layer - possible it may have been
+ // removed during a dropped frame, but layers may still remain scheduled so
+ // as not to lose info on what portion is damaged
+ if (CC_UNLIKELY(layerNode->getLayerSurface() == nullptr)) {
+ continue;
+ }
+ bool rendered = renderLayerImpl(layerNode, layers.entries()[i].damage);
+ if (!rendered) {
+ return;
+ }
+ }
+}
+
+// If the given node didn't have a layer surface, or had one of the wrong size, this method
+// creates a new one and returns true. Otherwise does nothing and returns false.
+bool SkiaCpuPipeline::createOrUpdateLayer(RenderNode* node,
+ const DamageAccumulator& damageAccumulator,
+ ErrorHandler* errorHandler) {
+ // compute the size of the surface (i.e. texture) to be allocated for this layer
+ const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE;
+ const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE;
+
+ SkSurface* layer = node->getLayerSurface();
+ if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) {
+ SkImageInfo info;
+ info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(),
+ kPremul_SkAlphaType, getSurfaceColorSpace());
+ SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
+ node->setLayerSurface(SkSurfaces::Raster(info, &props));
+ if (node->getLayerSurface()) {
+ // update the transform in window of the layer to reset its origin wrt light source
+ // position
+ Matrix4 windowTransform;
+ damageAccumulator.computeCurrentTransform(&windowTransform);
+ node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform);
+ } else {
+ String8 cachesOutput;
+ mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput,
+ &mRenderThread.renderState());
+ ALOGE("%s", cachesOutput.c_str());
+ if (errorHandler) {
+ std::ostringstream err;
+ err << "Unable to create layer for " << node->getName();
+ const int maxTextureSize = DeviceInfo::get()->maxTextureSize();
+ err << ", size " << info.width() << "x" << info.height() << " max size "
+ << maxTextureSize << " color type " << (int)info.colorType() << " has context "
+ << (int)(mRenderThread.getGrContext() != nullptr);
+ errorHandler->onError(err.str());
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+MakeCurrentResult SkiaCpuPipeline::makeCurrent() {
+ return MakeCurrentResult::AlreadyCurrent;
+}
+
+Frame SkiaCpuPipeline::getFrame() {
+ return Frame(mSurface->width(), mSurface->height(), 0);
+}
+
+IRenderPipeline::DrawResult SkiaCpuPipeline::draw(
+ const Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
+ const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
+ const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
+ const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler,
+ const HardwareBufferRenderParams& bufferParams, std::mutex& profilerLock) {
+ LightingInfo::updateLighting(lightGeometry, lightInfo);
+ renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, mSurface,
+ SkMatrix::I());
+ return {true, IRenderPipeline::DrawResult::kUnknownTime, android::base::unique_fd{}};
+}
+
+bool SkiaCpuPipeline::setSurface(ANativeWindow* surface, SwapBehavior swapBehavior) {
+ if (surface) {
+ ANativeWindowBuffer* buffer;
+ surface->dequeueBuffer(surface, &buffer, nullptr);
+ int width, height;
+ surface->query(surface, NATIVE_WINDOW_WIDTH, &width);
+ surface->query(surface, NATIVE_WINDOW_HEIGHT, &height);
+ SkImageInfo imageInfo =
+ SkImageInfo::Make(width, height, mSurfaceColorType,
+ SkAlphaType::kPremul_SkAlphaType, mSurfaceColorSpace);
+ size_t widthBytes = width * imageInfo.bytesPerPixel();
+ void* pixels = buffer->reserved[0];
+ mSurface = SkSurfaces::WrapPixels(imageInfo, pixels, widthBytes);
+ } else {
+ mSurface = sk_sp<SkSurface>();
+ }
+ return true;
+}
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/pipeline/skia/SkiaCpuPipeline.h b/libs/hwui/pipeline/skia/SkiaCpuPipeline.h
new file mode 100644
index 000000000000..5a1014c2c2de
--- /dev/null
+++ b/libs/hwui/pipeline/skia/SkiaCpuPipeline.h
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include "pipeline/skia/SkiaPipeline.h"
+
+namespace android {
+
+namespace uirenderer {
+namespace skiapipeline {
+
+class SkiaCpuPipeline : public SkiaPipeline {
+public:
+ SkiaCpuPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) {}
+ ~SkiaCpuPipeline() {}
+
+ bool pinImages(std::vector<SkImage*>& mutableImages) override { return false; }
+ bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; }
+ void unpinImages() override {}
+
+ // If the given node didn't have a layer surface, or had one of the wrong size, this method
+ // creates a new one and returns true. Otherwise does nothing and returns false.
+ bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator,
+ ErrorHandler* errorHandler) override;
+ void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override;
+ void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override {}
+ bool hasHardwareBuffer() override { return false; }
+
+ renderthread::MakeCurrentResult makeCurrent() override;
+ renderthread::Frame getFrame() override;
+ renderthread::IRenderPipeline::DrawResult draw(
+ const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
+ const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
+ const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
+ const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler,
+ const renderthread::HardwareBufferRenderParams& bufferParams,
+ std::mutex& profilerLock) override;
+ bool swapBuffers(const renderthread::Frame& frame, IRenderPipeline::DrawResult& drawResult,
+ const SkRect& screenDirty, FrameInfo* currentFrameInfo,
+ bool* requireSwap) override {
+ return false;
+ }
+ DeferredLayerUpdater* createTextureLayer() override { return nullptr; }
+ bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override;
+ [[nodiscard]] android::base::unique_fd flush() override {
+ return android::base::unique_fd(-1);
+ };
+ void onStop() override {}
+ bool isSurfaceReady() override { return mSurface.get() != nullptr; }
+ bool isContextReady() override { return true; }
+
+ const SkM44& getPixelSnapMatrix() const override {
+ static const SkM44 sSnapMatrix = SkM44();
+ return sSnapMatrix;
+ }
+
+private:
+ sk_sp<SkSurface> mSurface;
+};
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
index 5c8285a8e1e9..36dc933aa7b0 100644
--- a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
+++ b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp
@@ -15,22 +15,18 @@
*/
#include "SkiaDisplayList.h"
-#include "FunctorDrawable.h"
+#include <SkImagePriv.h>
+#include <SkPathOps.h>
+
+// clang-format off
+#include "FunctorDrawable.h" // Must be included before DumpOpsCanvas.h
#include "DumpOpsCanvas.h"
-#ifdef __ANDROID__ // Layoutlib does not support SkiaPipeline
+// clang-format on
#include "SkiaPipeline.h"
-#else
-#include "DamageAccumulator.h"
-#endif
#include "TreeInfo.h"
#include "VectorDrawable.h"
-#ifdef __ANDROID__
#include "renderthread/CanvasContext.h"
-#endif
-
-#include <SkImagePriv.h>
-#include <SkPathOps.h>
namespace android {
namespace uirenderer {
@@ -101,7 +97,6 @@ bool SkiaDisplayList::prepareListAndChildren(
// If the prepare tree is triggered by the UI thread and no previous call to
// pinImages has failed then we must pin all mutable images in the GPU cache
// until the next UI thread draw.
-#ifdef __ANDROID__ // Layoutlib does not support CanvasContext
if (info.prepareTextures && !info.canvasContext.pinImages(mMutableImages)) {
// In the event that pinning failed we prevent future pinImage calls for the
// remainder of this tree traversal and also unpin any currently pinned images
@@ -110,11 +105,11 @@ bool SkiaDisplayList::prepareListAndChildren(
info.canvasContext.unpinImages();
}
+#ifdef __ANDROID__
auto grContext = info.canvasContext.getGrContext();
- for (auto mesh : mMeshes) {
- mesh->updateSkMesh(grContext);
+ for (const auto& bufferData : mMeshBufferData) {
+ bufferData->updateBuffers(grContext);
}
-
#endif
bool hasBackwardProjectedNodesHere = false;
@@ -181,7 +176,7 @@ void SkiaDisplayList::reset() {
mDisplayList.reset();
- mMeshes.clear();
+ mMeshBufferData.clear();
mMutableImages.clear();
mVectorDrawables.clear();
mAnimatedImages.clear();
diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.h b/libs/hwui/pipeline/skia/SkiaDisplayList.h
index b9dc1c49f09e..071a4e8caaff 100644
--- a/libs/hwui/pipeline/skia/SkiaDisplayList.h
+++ b/libs/hwui/pipeline/skia/SkiaDisplayList.h
@@ -17,6 +17,7 @@
#pragma once
#include <deque>
+#include <memory>
#include "Mesh.h"
#include "RecordingCanvas.h"
@@ -172,7 +173,7 @@ public:
std::deque<RenderNodeDrawable> mChildNodes;
std::deque<FunctorDrawable*> mChildFunctors;
std::vector<SkImage*> mMutableImages;
- std::vector<const Mesh*> mMeshes;
+ std::vector<std::shared_ptr<const MeshBufferData>> mMeshBufferData;
private:
std::vector<Pair<VectorDrawableRoot*, SkMatrix>> mVectorDrawables;
diff --git a/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp b/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp
new file mode 100644
index 000000000000..7bfbfdc4b96b
--- /dev/null
+++ b/libs/hwui/pipeline/skia/SkiaGpuPipeline.cpp
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "pipeline/skia/SkiaGpuPipeline.h"
+
+#include <SkImageAndroid.h>
+#include <gui/TraceUtils.h>
+#include <include/android/SkSurfaceAndroid.h>
+#include <include/gpu/ganesh/SkSurfaceGanesh.h>
+
+using namespace android::uirenderer::renderthread;
+
+namespace android {
+namespace uirenderer {
+namespace skiapipeline {
+
+SkiaGpuPipeline::SkiaGpuPipeline(RenderThread& thread) : SkiaPipeline(thread) {}
+
+SkiaGpuPipeline::~SkiaGpuPipeline() {
+ unpinImages();
+}
+
+void SkiaGpuPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) {
+ sk_sp<GrDirectContext> cachedContext;
+
+ // Render all layers that need to be updated, in order.
+ for (size_t i = 0; i < layers.entries().size(); i++) {
+ RenderNode* layerNode = layers.entries()[i].renderNode.get();
+ // only schedule repaint if node still on layer - possible it may have been
+ // removed during a dropped frame, but layers may still remain scheduled so
+ // as not to lose info on what portion is damaged
+ if (CC_UNLIKELY(layerNode->getLayerSurface() == nullptr)) {
+ continue;
+ }
+ bool rendered = renderLayerImpl(layerNode, layers.entries()[i].damage);
+ if (!rendered) {
+ return;
+ }
+ // cache the current context so that we can defer flushing it until
+ // either all the layers have been rendered or the context changes
+ GrDirectContext* currentContext =
+ GrAsDirectContext(layerNode->getLayerSurface()->getCanvas()->recordingContext());
+ if (cachedContext.get() != currentContext) {
+ if (cachedContext.get()) {
+ ATRACE_NAME("flush layers (context changed)");
+ cachedContext->flushAndSubmit();
+ }
+ cachedContext.reset(SkSafeRef(currentContext));
+ }
+ }
+ if (cachedContext.get()) {
+ ATRACE_NAME("flush layers");
+ cachedContext->flushAndSubmit();
+ }
+}
+
+// If the given node didn't have a layer surface, or had one of the wrong size, this method
+// creates a new one and returns true. Otherwise does nothing and returns false.
+bool SkiaGpuPipeline::createOrUpdateLayer(RenderNode* node,
+ const DamageAccumulator& damageAccumulator,
+ ErrorHandler* errorHandler) {
+ // compute the size of the surface (i.e. texture) to be allocated for this layer
+ const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE;
+ const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE;
+
+ SkSurface* layer = node->getLayerSurface();
+ if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) {
+ SkImageInfo info;
+ info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(),
+ kPremul_SkAlphaType, getSurfaceColorSpace());
+ SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
+ SkASSERT(mRenderThread.getGrContext() != nullptr);
+ node->setLayerSurface(SkSurfaces::RenderTarget(mRenderThread.getGrContext(),
+ skgpu::Budgeted::kYes, info, 0,
+ this->getSurfaceOrigin(), &props));
+ if (node->getLayerSurface()) {
+ // update the transform in window of the layer to reset its origin wrt light source
+ // position
+ Matrix4 windowTransform;
+ damageAccumulator.computeCurrentTransform(&windowTransform);
+ node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform);
+ } else {
+ String8 cachesOutput;
+ mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput,
+ &mRenderThread.renderState());
+ ALOGE("%s", cachesOutput.c_str());
+ if (errorHandler) {
+ std::ostringstream err;
+ err << "Unable to create layer for " << node->getName();
+ const int maxTextureSize = DeviceInfo::get()->maxTextureSize();
+ err << ", size " << info.width() << "x" << info.height() << " max size "
+ << maxTextureSize << " color type " << (int)info.colorType() << " has context "
+ << (int)(mRenderThread.getGrContext() != nullptr);
+ errorHandler->onError(err.str());
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+bool SkiaGpuPipeline::pinImages(std::vector<SkImage*>& mutableImages) {
+ if (!mRenderThread.getGrContext()) {
+ ALOGD("Trying to pin an image with an invalid GrContext");
+ return false;
+ }
+ for (SkImage* image : mutableImages) {
+ if (skgpu::ganesh::PinAsTexture(mRenderThread.getGrContext(), image)) {
+ mPinnedImages.emplace_back(sk_ref_sp(image));
+ } else {
+ return false;
+ }
+ }
+ return true;
+}
+
+void SkiaGpuPipeline::unpinImages() {
+ for (auto& image : mPinnedImages) {
+ skgpu::ganesh::UnpinTexture(mRenderThread.getGrContext(), image.get());
+ }
+ mPinnedImages.clear();
+}
+
+void SkiaGpuPipeline::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) {
+ GrDirectContext* context = thread.getGrContext();
+ if (context && !bitmap->isHardware()) {
+ ATRACE_FORMAT("Bitmap#prepareToDraw %dx%d", bitmap->width(), bitmap->height());
+ auto image = bitmap->makeImage();
+ if (image.get()) {
+ skgpu::ganesh::PinAsTexture(context, image.get());
+ skgpu::ganesh::UnpinTexture(context, image.get());
+ // A submit is necessary as there may not be a frame coming soon, so without a call
+ // to submit these texture uploads can just sit in the queue building up until
+ // we run out of RAM
+ context->flushAndSubmit();
+ }
+ }
+}
+
+sk_sp<SkSurface> SkiaGpuPipeline::getBufferSkSurface(
+ const renderthread::HardwareBufferRenderParams& bufferParams) {
+ auto bufferColorSpace = bufferParams.getColorSpace();
+ if (mBufferSurface == nullptr || mBufferColorSpace == nullptr ||
+ !SkColorSpace::Equals(mBufferColorSpace.get(), bufferColorSpace.get())) {
+ mBufferSurface = SkSurfaces::WrapAndroidHardwareBuffer(
+ mRenderThread.getGrContext(), mHardwareBuffer, kTopLeft_GrSurfaceOrigin,
+ bufferColorSpace, nullptr, true);
+ mBufferColorSpace = bufferColorSpace;
+ }
+ return mBufferSurface;
+}
+
+void SkiaGpuPipeline::dumpResourceCacheUsage() const {
+ int resources;
+ size_t bytes;
+ mRenderThread.getGrContext()->getResourceCacheUsage(&resources, &bytes);
+ size_t maxBytes = mRenderThread.getGrContext()->getResourceCacheLimit();
+
+ SkString log("Resource Cache Usage:\n");
+ log.appendf("%8d items\n", resources);
+ log.appendf("%8zu bytes (%.2f MB) out of %.2f MB maximum\n", bytes,
+ bytes * (1.0f / (1024.0f * 1024.0f)), maxBytes * (1.0f / (1024.0f * 1024.0f)));
+
+ ALOGD("%s", log.c_str());
+}
+
+void SkiaGpuPipeline::setHardwareBuffer(AHardwareBuffer* buffer) {
+ if (mHardwareBuffer) {
+ AHardwareBuffer_release(mHardwareBuffer);
+ mHardwareBuffer = nullptr;
+ }
+
+ if (buffer) {
+ AHardwareBuffer_acquire(buffer);
+ mHardwareBuffer = buffer;
+ }
+}
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
index c8d598702a7c..e4b1f916b4d6 100644
--- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.cpp
@@ -14,25 +14,25 @@
* limitations under the License.
*/
-#include "SkiaOpenGLPipeline.h"
+#include "pipeline/skia/SkiaOpenGLPipeline.h"
-#include <include/gpu/ganesh/SkSurfaceGanesh.h>
-#include <include/gpu/ganesh/gl/GrGLBackendSurface.h>
-#include <include/gpu/gl/GrGLTypes.h>
#include <GrBackendSurface.h>
#include <SkBlendMode.h>
#include <SkImageInfo.h>
#include <cutils/properties.h>
#include <gui/TraceUtils.h>
+#include <include/gpu/ganesh/SkSurfaceGanesh.h>
+#include <include/gpu/ganesh/gl/GrGLBackendSurface.h>
+#include <include/gpu/gl/GrGLTypes.h>
#include <strings.h>
#include "DeferredLayerUpdater.h"
#include "FrameInfo.h"
-#include "LayerDrawable.h"
#include "LightingInfo.h"
-#include "SkiaPipeline.h"
-#include "SkiaProfileRenderer.h"
#include "hwui/Bitmap.h"
+#include "pipeline/skia/LayerDrawable.h"
+#include "pipeline/skia/SkiaGpuPipeline.h"
+#include "pipeline/skia/SkiaProfileRenderer.h"
#include "private/hwui/DrawGlInfo.h"
#include "renderstate/RenderState.h"
#include "renderthread/EglManager.h"
@@ -47,7 +47,7 @@ namespace uirenderer {
namespace skiapipeline {
SkiaOpenGLPipeline::SkiaOpenGLPipeline(RenderThread& thread)
- : SkiaPipeline(thread), mEglManager(thread.eglManager()) {
+ : SkiaGpuPipeline(thread), mEglManager(thread.eglManager()) {
thread.renderState().registerContextCallback(this);
}
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
index 326b6ed77fe0..dc669a5eca73 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp
@@ -14,11 +14,8 @@
* limitations under the License.
*/
-#include "SkiaPipeline.h"
+#include "pipeline/skia/SkiaPipeline.h"
-#include <include/android/SkSurfaceAndroid.h>
-#include <include/gpu/ganesh/SkSurfaceGanesh.h>
-#include <include/encode/SkPngEncoder.h>
#include <SkCanvas.h>
#include <SkColor.h>
#include <SkColorSpace.h>
@@ -27,7 +24,6 @@
#include <SkImageAndroid.h>
#include <SkImageInfo.h>
#include <SkMatrix.h>
-#include <SkMultiPictureDocument.h>
#include <SkOverdrawCanvas.h>
#include <SkOverdrawColorFilter.h>
#include <SkPicture.h>
@@ -40,6 +36,10 @@
#include <SkTypeface.h>
#include <android-base/properties.h>
#include <gui/TraceUtils.h>
+#include <include/android/SkSurfaceAndroid.h>
+#include <include/docs/SkMultiPictureDocument.h>
+#include <include/encode/SkPngEncoder.h>
+#include <include/gpu/ganesh/SkSurfaceGanesh.h>
#include <unistd.h>
#include <sstream>
@@ -62,37 +62,13 @@ SkiaPipeline::SkiaPipeline(RenderThread& thread) : mRenderThread(thread) {
setSurfaceColorProperties(mColorMode);
}
-SkiaPipeline::~SkiaPipeline() {
- unpinImages();
-}
+SkiaPipeline::~SkiaPipeline() {}
void SkiaPipeline::onDestroyHardwareResources() {
unpinImages();
mRenderThread.cacheManager().trimStaleResources();
}
-bool SkiaPipeline::pinImages(std::vector<SkImage*>& mutableImages) {
- if (!mRenderThread.getGrContext()) {
- ALOGD("Trying to pin an image with an invalid GrContext");
- return false;
- }
- for (SkImage* image : mutableImages) {
- if (skgpu::ganesh::PinAsTexture(mRenderThread.getGrContext(), image)) {
- mPinnedImages.emplace_back(sk_ref_sp(image));
- } else {
- return false;
- }
- }
- return true;
-}
-
-void SkiaPipeline::unpinImages() {
- for (auto& image : mPinnedImages) {
- skgpu::ganesh::UnpinTexture(mRenderThread.getGrContext(), image.get());
- }
- mPinnedImages.clear();
-}
-
void SkiaPipeline::renderLayers(const LightGeometry& lightGeometry,
LayerUpdateQueue* layerUpdateQueue, bool opaque,
const LightInfo& lightInfo) {
@@ -102,136 +78,48 @@ void SkiaPipeline::renderLayers(const LightGeometry& lightGeometry,
layerUpdateQueue->clear();
}
-void SkiaPipeline::renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) {
- sk_sp<GrDirectContext> cachedContext;
-
- // Render all layers that need to be updated, in order.
- for (size_t i = 0; i < layers.entries().size(); i++) {
- RenderNode* layerNode = layers.entries()[i].renderNode.get();
- // only schedule repaint if node still on layer - possible it may have been
- // removed during a dropped frame, but layers may still remain scheduled so
- // as not to lose info on what portion is damaged
- if (CC_UNLIKELY(layerNode->getLayerSurface() == nullptr)) {
- continue;
- }
- SkASSERT(layerNode->getLayerSurface());
- SkiaDisplayList* displayList = layerNode->getDisplayList().asSkiaDl();
- if (!displayList || displayList->isEmpty()) {
- ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName());
- return;
- }
-
- const Rect& layerDamage = layers.entries()[i].damage;
-
- SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas();
-
- int saveCount = layerCanvas->save();
- SkASSERT(saveCount == 1);
-
- layerCanvas->androidFramework_setDeviceClipRestriction(layerDamage.toSkIRect());
-
- // TODO: put localized light center calculation and storage to a drawable related code.
- // It does not seem right to store something localized in a global state
- // fix here and in recordLayers
- const Vector3 savedLightCenter(LightingInfo::getLightCenterRaw());
- Vector3 transformedLightCenter(savedLightCenter);
- // map current light center into RenderNode's coordinate space
- layerNode->getSkiaLayer()->inverseTransformInWindow.mapPoint3d(transformedLightCenter);
- LightingInfo::setLightCenterRaw(transformedLightCenter);
-
- const RenderProperties& properties = layerNode->properties();
- const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight());
- if (properties.getClipToBounds() && layerCanvas->quickReject(bounds)) {
- return;
- }
-
- ATRACE_FORMAT("drawLayer [%s] %.1f x %.1f", layerNode->getName(), bounds.width(),
- bounds.height());
+bool SkiaPipeline::renderLayerImpl(RenderNode* layerNode, const Rect& layerDamage) {
+ SkASSERT(layerNode->getLayerSurface());
+ SkiaDisplayList* displayList = layerNode->getDisplayList().asSkiaDl();
+ if (!displayList || displayList->isEmpty()) {
+ ALOGE("%p drawLayers(%s) : missing drawable", layerNode, layerNode->getName());
+ return false;
+ }
- layerNode->getSkiaLayer()->hasRenderedSinceRepaint = false;
- layerCanvas->clear(SK_ColorTRANSPARENT);
+ SkCanvas* layerCanvas = layerNode->getLayerSurface()->getCanvas();
- RenderNodeDrawable root(layerNode, layerCanvas, false);
- root.forceDraw(layerCanvas);
- layerCanvas->restoreToCount(saveCount);
+ int saveCount = layerCanvas->save();
+ SkASSERT(saveCount == 1);
- LightingInfo::setLightCenterRaw(savedLightCenter);
+ layerCanvas->androidFramework_setDeviceClipRestriction(layerDamage.toSkIRect());
- // cache the current context so that we can defer flushing it until
- // either all the layers have been rendered or the context changes
- GrDirectContext* currentContext =
- GrAsDirectContext(layerNode->getLayerSurface()->getCanvas()->recordingContext());
- if (cachedContext.get() != currentContext) {
- if (cachedContext.get()) {
- ATRACE_NAME("flush layers (context changed)");
- cachedContext->flushAndSubmit();
- }
- cachedContext.reset(SkSafeRef(currentContext));
- }
+ // TODO: put localized light center calculation and storage to a drawable related code.
+ // It does not seem right to store something localized in a global state
+ // fix here and in recordLayers
+ const Vector3 savedLightCenter(LightingInfo::getLightCenterRaw());
+ Vector3 transformedLightCenter(savedLightCenter);
+ // map current light center into RenderNode's coordinate space
+ layerNode->getSkiaLayer()->inverseTransformInWindow.mapPoint3d(transformedLightCenter);
+ LightingInfo::setLightCenterRaw(transformedLightCenter);
+
+ const RenderProperties& properties = layerNode->properties();
+ const SkRect bounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight());
+ if (properties.getClipToBounds() && layerCanvas->quickReject(bounds)) {
+ return false;
}
- if (cachedContext.get()) {
- ATRACE_NAME("flush layers");
- cachedContext->flushAndSubmit();
- }
-}
+ ATRACE_FORMAT("drawLayer [%s] %.1f x %.1f", layerNode->getName(), bounds.width(),
+ bounds.height());
-bool SkiaPipeline::createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator,
- ErrorHandler* errorHandler) {
- // compute the size of the surface (i.e. texture) to be allocated for this layer
- const int surfaceWidth = ceilf(node->getWidth() / float(LAYER_SIZE)) * LAYER_SIZE;
- const int surfaceHeight = ceilf(node->getHeight() / float(LAYER_SIZE)) * LAYER_SIZE;
-
- SkSurface* layer = node->getLayerSurface();
- if (!layer || layer->width() != surfaceWidth || layer->height() != surfaceHeight) {
- SkImageInfo info;
- info = SkImageInfo::Make(surfaceWidth, surfaceHeight, getSurfaceColorType(),
- kPremul_SkAlphaType, getSurfaceColorSpace());
- SkSurfaceProps props(0, kUnknown_SkPixelGeometry);
- SkASSERT(mRenderThread.getGrContext() != nullptr);
- node->setLayerSurface(SkSurfaces::RenderTarget(mRenderThread.getGrContext(),
- skgpu::Budgeted::kYes, info, 0,
- this->getSurfaceOrigin(), &props));
- if (node->getLayerSurface()) {
- // update the transform in window of the layer to reset its origin wrt light source
- // position
- Matrix4 windowTransform;
- damageAccumulator.computeCurrentTransform(&windowTransform);
- node->getSkiaLayer()->inverseTransformInWindow.loadInverse(windowTransform);
- } else {
- String8 cachesOutput;
- mRenderThread.cacheManager().dumpMemoryUsage(cachesOutput,
- &mRenderThread.renderState());
- ALOGE("%s", cachesOutput.c_str());
- if (errorHandler) {
- std::ostringstream err;
- err << "Unable to create layer for " << node->getName();
- const int maxTextureSize = DeviceInfo::get()->maxTextureSize();
- err << ", size " << info.width() << "x" << info.height() << " max size "
- << maxTextureSize << " color type " << (int)info.colorType() << " has context "
- << (int)(mRenderThread.getGrContext() != nullptr);
- errorHandler->onError(err.str());
- }
- }
- return true;
- }
- return false;
-}
+ layerNode->getSkiaLayer()->hasRenderedSinceRepaint = false;
+ layerCanvas->clear(SK_ColorTRANSPARENT);
-void SkiaPipeline::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) {
- GrDirectContext* context = thread.getGrContext();
- if (context && !bitmap->isHardware()) {
- ATRACE_FORMAT("Bitmap#prepareToDraw %dx%d", bitmap->width(), bitmap->height());
- auto image = bitmap->makeImage();
- if (image.get()) {
- skgpu::ganesh::PinAsTexture(context, image.get());
- skgpu::ganesh::UnpinTexture(context, image.get());
- // A submit is necessary as there may not be a frame coming soon, so without a call
- // to submit these texture uploads can just sit in the queue building up until
- // we run out of RAM
- context->flushAndSubmit();
- }
- }
+ RenderNodeDrawable root(layerNode, layerCanvas, false);
+ root.forceDraw(layerCanvas);
+ layerCanvas->restoreToCount(saveCount);
+
+ LightingInfo::setLightCenterRaw(savedLightCenter);
+ return true;
}
static void savePictureAsync(const sk_sp<SkData>& data, const std::string& filename) {
@@ -297,7 +185,7 @@ bool SkiaPipeline::setupMultiFrameCapture() {
// we need to keep it until after mMultiPic.close()
// procs is passed as a pointer, but just as a method of having an optional default.
// procs doesn't need to outlive this Make call.
- mMultiPic = SkMakeMultiPictureDocument(mOpenMultiPicStream.get(), &procs,
+ mMultiPic = SkMultiPictureDocument::Make(mOpenMultiPicStream.get(), &procs,
[sharingCtx = mSerialContext.get()](const SkPicture* pic) {
SkSharingSerialContext::collectNonTextureImagesFromPicture(pic, sharingCtx);
});
@@ -599,45 +487,6 @@ void SkiaPipeline::renderFrameImpl(const SkRect& clip,
}
}
-void SkiaPipeline::dumpResourceCacheUsage() const {
- int resources;
- size_t bytes;
- mRenderThread.getGrContext()->getResourceCacheUsage(&resources, &bytes);
- size_t maxBytes = mRenderThread.getGrContext()->getResourceCacheLimit();
-
- SkString log("Resource Cache Usage:\n");
- log.appendf("%8d items\n", resources);
- log.appendf("%8zu bytes (%.2f MB) out of %.2f MB maximum\n", bytes,
- bytes * (1.0f / (1024.0f * 1024.0f)), maxBytes * (1.0f / (1024.0f * 1024.0f)));
-
- ALOGD("%s", log.c_str());
-}
-
-void SkiaPipeline::setHardwareBuffer(AHardwareBuffer* buffer) {
- if (mHardwareBuffer) {
- AHardwareBuffer_release(mHardwareBuffer);
- mHardwareBuffer = nullptr;
- }
-
- if (buffer) {
- AHardwareBuffer_acquire(buffer);
- mHardwareBuffer = buffer;
- }
-}
-
-sk_sp<SkSurface> SkiaPipeline::getBufferSkSurface(
- const renderthread::HardwareBufferRenderParams& bufferParams) {
- auto bufferColorSpace = bufferParams.getColorSpace();
- if (mBufferSurface == nullptr || mBufferColorSpace == nullptr ||
- !SkColorSpace::Equals(mBufferColorSpace.get(), bufferColorSpace.get())) {
- mBufferSurface = SkSurfaces::WrapAndroidHardwareBuffer(
- mRenderThread.getGrContext(), mHardwareBuffer, kTopLeft_GrSurfaceOrigin,
- bufferColorSpace, nullptr, true);
- mBufferColorSpace = bufferColorSpace;
- }
- return mBufferSurface;
-}
-
void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) {
mColorMode = colorMode;
switch (colorMode) {
@@ -650,7 +499,11 @@ void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) {
mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace();
break;
case ColorMode::Hdr:
- if (DeviceInfo::get()->isSupportFp16ForHdr()) {
+ if (DeviceInfo::get()->isSupportRgba10101010ForHdr()) {
+ mSurfaceColorType = SkColorType::kRGBA_10x6_SkColorType;
+ mSurfaceColorSpace = SkColorSpace::MakeRGB(
+ GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3);
+ } else if (DeviceInfo::get()->isSupportFp16ForHdr()) {
mSurfaceColorType = SkColorType::kRGBA_F16_SkColorType;
mSurfaceColorSpace = SkColorSpace::MakeSRGB();
} else {
@@ -675,7 +528,8 @@ void SkiaPipeline::setTargetSdrHdrRatio(float ratio) {
if (mColorMode == ColorMode::Hdr || mColorMode == ColorMode::Hdr10) {
mTargetSdrHdrRatio = ratio;
- if (mColorMode == ColorMode::Hdr && DeviceInfo::get()->isSupportFp16ForHdr()) {
+ if (mColorMode == ColorMode::Hdr && DeviceInfo::get()->isSupportFp16ForHdr() &&
+ !DeviceInfo::get()->isSupportRgba10101010ForHdr()) {
mSurfaceColorSpace = SkColorSpace::MakeSRGB();
} else {
mSurfaceColorSpace = SkColorSpace::MakeRGB(
diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h
index befee8989383..823b209017a5 100644
--- a/libs/hwui/pipeline/skia/SkiaPipeline.h
+++ b/libs/hwui/pipeline/skia/SkiaPipeline.h
@@ -18,7 +18,6 @@
#include <SkColorSpace.h>
#include <SkDocument.h>
-#include <SkMultiPictureDocument.h>
#include <SkSurface.h>
#include "Lighting.h"
@@ -42,18 +41,9 @@ public:
void onDestroyHardwareResources() override;
- bool pinImages(std::vector<SkImage*>& mutableImages) override;
- bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; }
- void unpinImages() override;
-
void renderLayers(const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
bool opaque, const LightInfo& lightInfo) override;
- // If the given node didn't have a layer surface, or had one of the wrong size, this method
- // creates a new one and returns true. Otherwise does nothing and returns false.
- bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator,
- ErrorHandler* errorHandler) override;
-
void setSurfaceColorProperties(ColorMode colorMode) override;
SkColorType getSurfaceColorType() const override { return mSurfaceColorType; }
sk_sp<SkColorSpace> getSurfaceColorSpace() override { return mSurfaceColorSpace; }
@@ -63,9 +53,8 @@ public:
const Rect& contentDrawBounds, sk_sp<SkSurface> surface,
const SkMatrix& preTransform);
- static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap);
-
- void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque);
+ bool renderLayerImpl(RenderNode* layerNode, const Rect& layerDamage);
+ virtual void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) = 0;
// Sets the recording callback to the provided function and the recording mode
// to CallbackAPI
@@ -75,19 +64,11 @@ public:
mCaptureMode = callback ? CaptureMode::CallbackAPI : CaptureMode::None;
}
- virtual void setHardwareBuffer(AHardwareBuffer* buffer) override;
- bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; }
-
void setTargetSdrHdrRatio(float ratio) override;
protected:
- sk_sp<SkSurface> getBufferSkSurface(
- const renderthread::HardwareBufferRenderParams& bufferParams);
- void dumpResourceCacheUsage() const;
-
renderthread::RenderThread& mRenderThread;
- AHardwareBuffer* mHardwareBuffer = nullptr;
sk_sp<SkSurface> mBufferSurface = nullptr;
sk_sp<SkColorSpace> mBufferColorSpace = nullptr;
@@ -125,8 +106,6 @@ private:
// Set up a multi frame capture.
bool setupMultiFrameCapture();
- std::vector<sk_sp<SkImage>> mPinnedImages;
-
// Block of properties used only for debugging to record a SkPicture and save it in a file.
// There are three possible ways of recording drawing commands.
enum class CaptureMode {
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
index e917f9a66917..45bfe1c4957f 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
@@ -342,7 +342,7 @@ double SkiaRecordingCanvas::drawAnimatedImage(AnimatedImageDrawable* animatedIma
}
void SkiaRecordingCanvas::drawMesh(const Mesh& mesh, sk_sp<SkBlender> blender, const Paint& paint) {
- mDisplayList->mMeshes.push_back(&mesh);
+ mDisplayList->mMeshBufferData.push_back(mesh.refBufferData());
mRecorder.drawMesh(mesh, blender, paint);
}
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
index fd0a8e06f39c..d06dba05ee88 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
+++ b/libs/hwui/pipeline/skia/SkiaVulkanPipeline.cpp
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-#include "SkiaVulkanPipeline.h"
+#include "pipeline/skia/SkiaVulkanPipeline.h"
#include <GrDirectContext.h>
#include <GrTypes.h>
@@ -28,10 +28,10 @@
#include "DeferredLayerUpdater.h"
#include "LightingInfo.h"
#include "Readback.h"
-#include "ShaderCache.h"
-#include "SkiaPipeline.h"
-#include "SkiaProfileRenderer.h"
-#include "VkInteropFunctorDrawable.h"
+#include "pipeline/skia/ShaderCache.h"
+#include "pipeline/skia/SkiaGpuPipeline.h"
+#include "pipeline/skia/SkiaProfileRenderer.h"
+#include "pipeline/skia/VkInteropFunctorDrawable.h"
#include "renderstate/RenderState.h"
#include "renderthread/Frame.h"
#include "renderthread/IRenderPipeline.h"
@@ -42,7 +42,8 @@ namespace android {
namespace uirenderer {
namespace skiapipeline {
-SkiaVulkanPipeline::SkiaVulkanPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) {
+SkiaVulkanPipeline::SkiaVulkanPipeline(renderthread::RenderThread& thread)
+ : SkiaGpuPipeline(thread) {
thread.renderState().registerContextCallback(this);
}
diff --git a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp
index b62711f50c94..21fe6ff14f56 100644
--- a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp
+++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp
@@ -16,10 +16,10 @@
#include "VkFunctorDrawable.h"
-#include <GrBackendDrawableInfo.h>
#include <SkAndroidFrameworkUtils.h>
#include <SkImage.h>
#include <SkM44.h>
+#include <include/gpu/ganesh/vk/GrBackendDrawableInfo.h>
#include <gui/TraceUtils.h>
#include <private/hwui/DrawVkInfo.h>
#include <utils/Color.h>
diff --git a/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h
new file mode 100644
index 000000000000..9159eae46065
--- /dev/null
+++ b/libs/hwui/platform/android/pipeline/skia/SkiaGpuPipeline.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "pipeline/skia/SkiaPipeline.h"
+
+namespace android {
+namespace uirenderer {
+namespace skiapipeline {
+
+class SkiaGpuPipeline : public SkiaPipeline {
+public:
+ SkiaGpuPipeline(renderthread::RenderThread& thread);
+ virtual ~SkiaGpuPipeline();
+
+ virtual GrSurfaceOrigin getSurfaceOrigin() = 0;
+
+ // If the given node didn't have a layer surface, or had one of the wrong size, this method
+ // creates a new one and returns true. Otherwise does nothing and returns false.
+ bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator,
+ ErrorHandler* errorHandler) override;
+
+ bool pinImages(std::vector<SkImage*>& mutableImages) override;
+ bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; }
+ void unpinImages() override;
+ void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override;
+ void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override;
+ bool hasHardwareBuffer() override { return mHardwareBuffer != nullptr; }
+
+ static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap);
+
+protected:
+ sk_sp<SkSurface> getBufferSkSurface(
+ const renderthread::HardwareBufferRenderParams& bufferParams);
+ void dumpResourceCacheUsage() const;
+
+ AHardwareBuffer* mHardwareBuffer = nullptr;
+
+private:
+ std::vector<sk_sp<SkImage>> mPinnedImages;
+};
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaOpenGLPipeline.h
index ebe8b6e15d44..6e7478288777 100644
--- a/libs/hwui/pipeline/skia/SkiaOpenGLPipeline.h
+++ b/libs/hwui/platform/android/pipeline/skia/SkiaOpenGLPipeline.h
@@ -19,7 +19,7 @@
#include <EGL/egl.h>
#include <system/window.h>
-#include "SkiaPipeline.h"
+#include "pipeline/skia/SkiaGpuPipeline.h"
#include "renderstate/RenderState.h"
#include "renderthread/HardwareBufferRenderParams.h"
@@ -30,7 +30,7 @@ class Bitmap;
namespace uirenderer {
namespace skiapipeline {
-class SkiaOpenGLPipeline : public SkiaPipeline, public IGpuContextCallback {
+class SkiaOpenGLPipeline : public SkiaGpuPipeline, public IGpuContextCallback {
public:
SkiaOpenGLPipeline(renderthread::RenderThread& thread);
virtual ~SkiaOpenGLPipeline();
diff --git a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/platform/android/pipeline/skia/SkiaVulkanPipeline.h
index 624eaa51a584..0d30df48baee 100644
--- a/libs/hwui/pipeline/skia/SkiaVulkanPipeline.h
+++ b/libs/hwui/platform/android/pipeline/skia/SkiaVulkanPipeline.h
@@ -17,7 +17,7 @@
#pragma once
#include "SkRefCnt.h"
-#include "SkiaPipeline.h"
+#include "pipeline/skia/SkiaGpuPipeline.h"
#include "renderstate/RenderState.h"
#include "renderthread/HardwareBufferRenderParams.h"
#include "renderthread/VulkanManager.h"
@@ -30,7 +30,7 @@ namespace android {
namespace uirenderer {
namespace skiapipeline {
-class SkiaVulkanPipeline : public SkiaPipeline, public IGpuContextCallback {
+class SkiaVulkanPipeline : public SkiaGpuPipeline, public IGpuContextCallback {
public:
explicit SkiaVulkanPipeline(renderthread::RenderThread& thread);
virtual ~SkiaVulkanPipeline();
diff --git a/libs/hwui/platform/android/thread/CommonPoolBase.h b/libs/hwui/platform/android/thread/CommonPoolBase.h
new file mode 100644
index 000000000000..8f836b612440
--- /dev/null
+++ b/libs/hwui/platform/android/thread/CommonPoolBase.h
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+#ifndef FRAMEWORKS_BASE_COMMONPOOLBASE_H
+#define FRAMEWORKS_BASE_COMMONPOOLBASE_H
+
+#include <sys/resource.h>
+
+#include "renderthread/RenderThread.h"
+
+namespace android {
+namespace uirenderer {
+
+class CommonPoolBase {
+ PREVENT_COPY_AND_ASSIGN(CommonPoolBase);
+
+protected:
+ CommonPoolBase() {}
+
+ void setupThread(int i, std::mutex& mLock, std::vector<int>& tids,
+ std::vector<std::condition_variable>& tidConditionVars) {
+ std::array<char, 20> name{"hwuiTask"};
+ snprintf(name.data(), name.size(), "hwuiTask%d", i);
+ auto self = pthread_self();
+ pthread_setname_np(self, name.data());
+ {
+ std::unique_lock lock(mLock);
+ tids[i] = pthread_gettid_np(self);
+ tidConditionVars[i].notify_one();
+ }
+ setpriority(PRIO_PROCESS, 0, PRIORITY_FOREGROUND);
+ auto startHook = renderthread::RenderThread::getOnStartHook();
+ if (startHook) {
+ startHook(name.data());
+ }
+ }
+
+ bool supportsTid() { return true; }
+};
+
+} // namespace uirenderer
+} // namespace android
+
+#endif // FRAMEWORKS_BASE_COMMONPOOLBASE_H
diff --git a/libs/hwui/platform/darwin/utils/SharedLib.cpp b/libs/hwui/platform/darwin/utils/SharedLib.cpp
new file mode 100644
index 000000000000..6e9f0b486513
--- /dev/null
+++ b/libs/hwui/platform/darwin/utils/SharedLib.cpp
@@ -0,0 +1,33 @@
+/*
+ * 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 "utils/SharedLib.h"
+
+#include <dlfcn.h>
+
+namespace android {
+namespace uirenderer {
+
+void* SharedLib::openSharedLib(std::string filename) {
+ return dlopen((filename + ".dylib").c_str(), RTLD_NOW | RTLD_NODELETE);
+}
+
+void* SharedLib::getSymbol(void* library, const char* symbol) {
+ return dlsym(library, symbol);
+}
+
+} // namespace uirenderer
+} // namespace android
diff --git a/libs/hwui/platform/host/WebViewFunctorManager.cpp b/libs/hwui/platform/host/WebViewFunctorManager.cpp
index 1d16655bf73c..4ba206b41b39 100644
--- a/libs/hwui/platform/host/WebViewFunctorManager.cpp
+++ b/libs/hwui/platform/host/WebViewFunctorManager.cpp
@@ -50,6 +50,8 @@ ASurfaceControl* WebViewFunctor::getSurfaceControl() {
void WebViewFunctor::mergeTransaction(ASurfaceTransaction* transaction) {}
+void WebViewFunctor::reportRenderingThreads(const pid_t* thread_ids, size_t size) {}
+
void WebViewFunctor::reparentSurfaceControl(ASurfaceControl* parent) {}
WebViewFunctorManager& WebViewFunctorManager::instance() {
@@ -68,6 +70,13 @@ void WebViewFunctorManager::onContextDestroyed() {}
void WebViewFunctorManager::destroyFunctor(int functor) {}
+void WebViewFunctorManager::reportRenderingThreads(int functor, const pid_t* thread_ids,
+ size_t size) {}
+
+std::vector<pid_t> WebViewFunctorManager::getRenderingThreadsForActiveFunctors() {
+ return {};
+}
+
sp<WebViewFunctor::Handle> WebViewFunctorManager::handleFor(int functor) {
return nullptr;
}
diff --git a/libs/hwui/platform/host/android/api-level.h b/libs/hwui/platform/host/android/api-level.h
new file mode 120000
index 000000000000..4fb4784f9f60
--- /dev/null
+++ b/libs/hwui/platform/host/android/api-level.h
@@ -0,0 +1 @@
+../../../../../../../bionic/libc/include/android/api-level.h \ No newline at end of file
diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h
new file mode 100644
index 000000000000..a71726585081
--- /dev/null
+++ b/libs/hwui/platform/host/pipeline/skia/SkiaGpuPipeline.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "pipeline/skia/SkiaPipeline.h"
+#include "renderthread/Frame.h"
+
+namespace android {
+namespace uirenderer {
+namespace skiapipeline {
+
+class SkiaGpuPipeline : public SkiaPipeline {
+public:
+ SkiaGpuPipeline(renderthread::RenderThread& thread) : SkiaPipeline(thread) {}
+ ~SkiaGpuPipeline() {}
+
+ bool pinImages(std::vector<SkImage*>& mutableImages) override { return false; }
+ bool pinImages(LsaVector<sk_sp<Bitmap>>& images) override { return false; }
+ void unpinImages() override {}
+
+ // If the given node didn't have a layer surface, or had one of the wrong size, this method
+ // creates a new one and returns true. Otherwise does nothing and returns false.
+ bool createOrUpdateLayer(RenderNode* node, const DamageAccumulator& damageAccumulator,
+ ErrorHandler* errorHandler) override {
+ return false;
+ }
+ void renderLayersImpl(const LayerUpdateQueue& layers, bool opaque) override {}
+ void setHardwareBuffer(AHardwareBuffer* hardwareBuffer) override {}
+ bool hasHardwareBuffer() override { return false; }
+
+ renderthread::MakeCurrentResult makeCurrent() override {
+ return renderthread::MakeCurrentResult::Failed;
+ }
+ renderthread::Frame getFrame() override { return renderthread::Frame(0, 0, 0); }
+ renderthread::IRenderPipeline::DrawResult draw(
+ const renderthread::Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
+ const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
+ const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
+ const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler,
+ const renderthread::HardwareBufferRenderParams& bufferParams,
+ std::mutex& profilerLock) override {
+ return {false, IRenderPipeline::DrawResult::kUnknownTime, android::base::unique_fd(-1)};
+ }
+ bool swapBuffers(const renderthread::Frame& frame, IRenderPipeline::DrawResult& drawResult,
+ const SkRect& screenDirty, FrameInfo* currentFrameInfo,
+ bool* requireSwap) override {
+ return false;
+ }
+ DeferredLayerUpdater* createTextureLayer() override { return nullptr; }
+ bool setSurface(ANativeWindow* surface, renderthread::SwapBehavior swapBehavior) override {
+ return false;
+ }
+ [[nodiscard]] android::base::unique_fd flush() override {
+ return android::base::unique_fd(-1);
+ };
+ void onStop() override {}
+ bool isSurfaceReady() override { return false; }
+ bool isContextReady() override { return false; }
+
+ const SkM44& getPixelSnapMatrix() const override {
+ static const SkM44 sSnapMatrix = SkM44();
+ return sSnapMatrix;
+ }
+ static void prepareToDraw(const renderthread::RenderThread& thread, Bitmap* bitmap) {}
+};
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h
new file mode 100644
index 000000000000..4fafbcc4748d
--- /dev/null
+++ b/libs/hwui/platform/host/pipeline/skia/SkiaOpenGLPipeline.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "pipeline/skia/SkiaGpuPipeline.h"
+
+namespace android {
+
+namespace uirenderer {
+namespace skiapipeline {
+
+class SkiaOpenGLPipeline : public SkiaGpuPipeline {
+public:
+ SkiaOpenGLPipeline(renderthread::RenderThread& thread) : SkiaGpuPipeline(thread) {}
+
+ static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor) {}
+};
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h b/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h
new file mode 100644
index 000000000000..d54caef45bb5
--- /dev/null
+++ b/libs/hwui/platform/host/pipeline/skia/SkiaVulkanPipeline.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "pipeline/skia/SkiaGpuPipeline.h"
+
+namespace android {
+
+namespace uirenderer {
+namespace skiapipeline {
+
+class SkiaVulkanPipeline : public SkiaGpuPipeline {
+public:
+ SkiaVulkanPipeline(renderthread::RenderThread& thread) : SkiaGpuPipeline(thread) {}
+
+ static void invokeFunctor(const renderthread::RenderThread& thread, Functor* functor) {}
+};
+
+} /* namespace skiapipeline */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/platform/host/renderthread/HintSessionWrapper.cpp b/libs/hwui/platform/host/renderthread/HintSessionWrapper.cpp
new file mode 100644
index 000000000000..b1b1d5830834
--- /dev/null
+++ b/libs/hwui/platform/host/renderthread/HintSessionWrapper.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 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 "renderthread/HintSessionWrapper.h"
+
+namespace android {
+namespace uirenderer {
+namespace renderthread {
+
+void HintSessionWrapper::HintSessionBinding::init() {}
+
+HintSessionWrapper::HintSessionWrapper(pid_t uiThreadId, pid_t renderThreadId)
+ : mUiThreadId(uiThreadId)
+ , mRenderThreadId(renderThreadId)
+ , mBinding(std::make_shared<HintSessionBinding>()) {}
+
+HintSessionWrapper::~HintSessionWrapper() {}
+
+void HintSessionWrapper::destroy() {}
+
+bool HintSessionWrapper::init() {
+ return false;
+}
+
+void HintSessionWrapper::updateTargetWorkDuration(long targetWorkDurationNanos) {}
+
+void HintSessionWrapper::reportActualWorkDuration(long actualDurationNanos) {}
+
+void HintSessionWrapper::sendLoadResetHint() {}
+
+void HintSessionWrapper::sendLoadIncreaseHint() {}
+
+bool HintSessionWrapper::alive() {
+ return false;
+}
+
+nsecs_t HintSessionWrapper::getLastUpdate() {
+ return -1;
+}
+
+void HintSessionWrapper::delayedDestroy(RenderThread& rt, nsecs_t delay,
+ std::shared_ptr<HintSessionWrapper> wrapperPtr) {}
+
+void HintSessionWrapper::setActiveFunctorThreads(std::vector<pid_t> threadIds) {}
+
+} /* namespace renderthread */
+} /* namespace uirenderer */
+} /* namespace android */
diff --git a/libs/hwui/platform/host/renderthread/ReliableSurface.cpp b/libs/hwui/platform/host/renderthread/ReliableSurface.cpp
new file mode 100644
index 000000000000..2deaaf3b909c
--- /dev/null
+++ b/libs/hwui/platform/host/renderthread/ReliableSurface.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "renderthread/ReliableSurface.h"
+
+#include <log/log_main.h>
+#include <system/window.h>
+
+namespace android::uirenderer::renderthread {
+
+ReliableSurface::ReliableSurface(ANativeWindow* window) : mWindow(window) {
+ LOG_ALWAYS_FATAL_IF(!mWindow, "Error, unable to wrap a nullptr");
+ ANativeWindow_acquire(mWindow);
+}
+
+ReliableSurface::~ReliableSurface() {
+ ANativeWindow_release(mWindow);
+}
+
+void ReliableSurface::init() {}
+
+int ReliableSurface::reserveNext() {
+ return OK;
+}
+
+}; // namespace android::uirenderer::renderthread
diff --git a/libs/hwui/platform/host/renderthread/RenderThread.cpp b/libs/hwui/platform/host/renderthread/RenderThread.cpp
index 6f08b5979772..f9d0f4704e08 100644
--- a/libs/hwui/platform/host/renderthread/RenderThread.cpp
+++ b/libs/hwui/platform/host/renderthread/RenderThread.cpp
@@ -17,6 +17,7 @@
#include "renderthread/RenderThread.h"
#include "Readback.h"
+#include "renderstate/RenderState.h"
#include "renderthread/VulkanManager.h"
namespace android {
@@ -66,6 +67,7 @@ RenderThread::RenderThread()
RenderThread::~RenderThread() {}
void RenderThread::initThreadLocals() {
+ mRenderState = new RenderState(*this);
mCacheManager = new CacheManager(*this);
}
diff --git a/libs/hwui/platform/host/thread/CommonPoolBase.h b/libs/hwui/platform/host/thread/CommonPoolBase.h
new file mode 100644
index 000000000000..cd091013ce0c
--- /dev/null
+++ b/libs/hwui/platform/host/thread/CommonPoolBase.h
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+#ifndef FRAMEWORKS_BASE_COMMONPOOLBASE_H
+#define FRAMEWORKS_BASE_COMMONPOOLBASE_H
+
+#include <condition_variable>
+#include <mutex>
+#include <vector>
+
+#include "renderthread/RenderThread.h"
+
+namespace android {
+namespace uirenderer {
+
+class CommonPoolBase {
+ PREVENT_COPY_AND_ASSIGN(CommonPoolBase);
+
+protected:
+ CommonPoolBase() {}
+
+ void setupThread(int i, std::mutex& mLock, std::vector<int>& tids,
+ std::vector<std::condition_variable>& tidConditionVars) {
+ std::array<char, 20> name{"hwuiTask"};
+ snprintf(name.data(), name.size(), "hwuiTask%d", i);
+ {
+ std::unique_lock lock(mLock);
+ tids[i] = -1;
+ tidConditionVars[i].notify_one();
+ }
+ auto startHook = renderthread::RenderThread::getOnStartHook();
+ if (startHook) {
+ startHook(name.data());
+ }
+ }
+
+ bool supportsTid() { return false; }
+};
+
+} // namespace uirenderer
+} // namespace android
+
+#endif // FRAMEWORKS_BASE_COMMONPOOLBASE_H
diff --git a/libs/hwui/platform/host/utils/MessageHandler.h b/libs/hwui/platform/host/utils/MessageHandler.h
new file mode 100644
index 000000000000..51ee48e0c6d2
--- /dev/null
+++ b/libs/hwui/platform/host/utils/MessageHandler.h
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <utils/RefBase.h>
+
+struct Message {
+ Message(int w) {}
+};
+
+class MessageHandler : public virtual android::RefBase {
+protected:
+ virtual ~MessageHandler() override {}
+
+public:
+ /**
+ * Handles a message.
+ */
+ virtual void handleMessage(const Message& message) = 0;
+};
diff --git a/libs/hwui/platform/linux/utils/SharedLib.cpp b/libs/hwui/platform/linux/utils/SharedLib.cpp
new file mode 100644
index 000000000000..a9acf37dfef4
--- /dev/null
+++ b/libs/hwui/platform/linux/utils/SharedLib.cpp
@@ -0,0 +1,33 @@
+/*
+ * 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 "utils/SharedLib.h"
+
+#include <dlfcn.h>
+
+namespace android {
+namespace uirenderer {
+
+void* SharedLib::openSharedLib(std::string filename) {
+ return dlopen((filename + ".so").c_str(), RTLD_NOW | RTLD_NODELETE);
+}
+
+void* SharedLib::getSymbol(void* library, const char* symbol) {
+ return dlsym(library, symbol);
+}
+
+} // namespace uirenderer
+} // namespace android
diff --git a/libs/hwui/private/hwui/WebViewFunctor.h b/libs/hwui/private/hwui/WebViewFunctor.h
index 493c943079ab..dbd8a16dfcfc 100644
--- a/libs/hwui/private/hwui/WebViewFunctor.h
+++ b/libs/hwui/private/hwui/WebViewFunctor.h
@@ -106,6 +106,11 @@ ANDROID_API int WebViewFunctor_create(void* data, const WebViewFunctorCallbacks&
// and it should be considered alive & active until that point.
ANDROID_API void WebViewFunctor_release(int functor);
+// Reports the list of threads critical for frame production for the given
+// functor. Must be called on render thread.
+ANDROID_API void WebViewFunctor_reportRenderingThreads(int functor, const int32_t* thread_ids,
+ size_t size);
+
} // namespace android::uirenderer
#endif // FRAMEWORKS_BASE_WEBVIEWFUNCTOR_H
diff --git a/libs/hwui/renderstate/RenderState.h b/libs/hwui/renderstate/RenderState.h
index e08d32a7735c..60657cf91123 100644
--- a/libs/hwui/renderstate/RenderState.h
+++ b/libs/hwui/renderstate/RenderState.h
@@ -16,11 +16,13 @@
#ifndef RENDERSTATE_H
#define RENDERSTATE_H
-#include "utils/Macros.h"
-
+#include <pthread.h>
#include <utils/RefBase.h>
+
#include <set>
+#include "utils/Macros.h"
+
namespace android {
namespace uirenderer {
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index 1d0330185b1c..8bb11badb607 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -35,8 +35,9 @@
#include "Properties.h"
#include "RenderThread.h"
#include "hwui/Canvas.h"
+#include "pipeline/skia/SkiaCpuPipeline.h"
+#include "pipeline/skia/SkiaGpuPipeline.h"
#include "pipeline/skia/SkiaOpenGLPipeline.h"
-#include "pipeline/skia/SkiaPipeline.h"
#include "pipeline/skia/SkiaVulkanPipeline.h"
#include "thread/CommonPool.h"
#include "utils/GLUtils.h"
@@ -72,7 +73,7 @@ CanvasContext* ScopedActiveContext::sActiveContext = nullptr;
CanvasContext* CanvasContext::create(RenderThread& thread, bool translucent,
RenderNode* rootRenderNode, IContextFactory* contextFactory,
- int32_t uiThreadId, int32_t renderThreadId) {
+ pid_t uiThreadId, pid_t renderThreadId) {
auto renderType = Properties::getRenderPipelineType();
switch (renderType) {
@@ -84,6 +85,12 @@ CanvasContext* CanvasContext::create(RenderThread& thread, bool translucent,
return new CanvasContext(thread, translucent, rootRenderNode, contextFactory,
std::make_unique<skiapipeline::SkiaVulkanPipeline>(thread),
uiThreadId, renderThreadId);
+#ifndef __ANDROID__
+ case RenderPipelineType::SkiaCpu:
+ return new CanvasContext(thread, translucent, rootRenderNode, contextFactory,
+ std::make_unique<skiapipeline::SkiaCpuPipeline>(thread),
+ uiThreadId, renderThreadId);
+#endif
default:
LOG_ALWAYS_FATAL("canvas context type %d not supported", (int32_t)renderType);
break;
@@ -108,7 +115,7 @@ void CanvasContext::invokeFunctor(const RenderThread& thread, Functor* functor)
}
void CanvasContext::prepareToDraw(const RenderThread& thread, Bitmap* bitmap) {
- skiapipeline::SkiaPipeline::prepareToDraw(thread, bitmap);
+ skiapipeline::SkiaGpuPipeline::prepareToDraw(thread, bitmap);
}
CanvasContext::CanvasContext(RenderThread& thread, bool translucent, RenderNode* rootRenderNode,
@@ -182,6 +189,7 @@ static void setBufferCount(ANativeWindow* window) {
}
void CanvasContext::setHardwareBuffer(AHardwareBuffer* buffer) {
+#ifdef __ANDROID__
if (mHardwareBuffer) {
AHardwareBuffer_release(mHardwareBuffer);
mHardwareBuffer = nullptr;
@@ -192,6 +200,7 @@ void CanvasContext::setHardwareBuffer(AHardwareBuffer* buffer) {
mHardwareBuffer = buffer;
}
mRenderPipeline->setHardwareBuffer(mHardwareBuffer);
+#endif
}
void CanvasContext::setSurface(ANativeWindow* window, bool enableTimeout) {
@@ -411,7 +420,8 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy
// If the previous frame was dropped we don't need to hold onto it, so
// just keep using the previous frame's structure instead
- if (const auto reason = wasSkipped(mCurrentFrameInfo)) {
+ const auto reason = wasSkipped(mCurrentFrameInfo);
+ if (reason.has_value()) {
// Use the oldest skipped frame in case we skip more than a single frame
if (!mSkippedFrameInfo) {
switch (*reason) {
@@ -560,6 +570,7 @@ Frame CanvasContext::getFrame() {
}
void CanvasContext::draw(bool solelyTextureViewUpdates) {
+#ifdef __ANDROID__
if (auto grContext = getGrContext()) {
if (grContext->abandoned()) {
if (grContext->isDeviceLost()) {
@@ -570,6 +581,7 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) {
return;
}
}
+#endif
SkRect dirty;
mDamageAccumulator.finish(&dirty);
@@ -593,11 +605,13 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) {
if (skippedFrameReason) {
mCurrentFrameInfo->setSkippedFrameReason(*skippedFrameReason);
+#ifdef __ANDROID__
if (auto grContext = getGrContext()) {
// Submit to ensure that any texture uploads complete and Skia can
// free its staging buffers.
grContext->flushAndSubmit();
}
+#endif
// Notify the callbacks, even if there's nothing to draw so they aren't waiting
// indefinitely
@@ -776,6 +790,8 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) {
(std::min(syncDelayDuration, mLastDequeueBufferDuration)) -
dequeueBufferDuration - idleDuration;
mHintSessionWrapper->reportActualWorkDuration(actualDuration);
+ mHintSessionWrapper->setActiveFunctorThreads(
+ WebViewFunctorManager::instance().getRenderingThreadsForActiveFunctors());
}
mLastDequeueBufferDuration = dequeueBufferDuration;
@@ -994,7 +1010,15 @@ void CanvasContext::destroyHardwareResources() {
}
void CanvasContext::onContextDestroyed() {
- destroyHardwareResources();
+ // We don't want to destroyHardwareResources as that will invalidate display lists which
+ // the client may not be expecting. Instead just purge all scratch resources
+ if (mRenderPipeline->isContextReady()) {
+ freePrefetchedLayers();
+ for (const sp<RenderNode>& node : mRenderNodes) {
+ node->destroyLayers();
+ }
+ mRenderPipeline->onDestroyHardwareResources();
+ }
}
DeferredLayerUpdater* CanvasContext::createTextureLayer() {
diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp
index 1b333bfccbf1..826d00e1f32f 100644
--- a/libs/hwui/renderthread/DrawFrameTask.cpp
+++ b/libs/hwui/renderthread/DrawFrameTask.cpp
@@ -140,12 +140,14 @@ void DrawFrameTask::run() {
if (CC_LIKELY(canDrawThisFrame)) {
context->draw(solelyTextureViewUpdates);
} else {
+#ifdef __ANDROID__
// Do a flush in case syncFrameState performed any texture uploads. Since we skipped
// the draw() call, those uploads (or deletes) will end up sitting in the queue.
// Do them now
if (GrDirectContext* grContext = mRenderThread->getGrContext()) {
grContext->flushAndSubmit();
}
+#endif
// wait on fences so tasks don't overlap next frame
context->waitOnFences();
}
@@ -176,11 +178,13 @@ bool DrawFrameTask::syncFrameState(TreeInfo& info) {
bool canDraw = mContext->makeCurrent();
mContext->unpinImages();
+#ifdef __ANDROID__
for (size_t i = 0; i < mLayers.size(); i++) {
if (mLayers[i]) {
mLayers[i]->apply();
}
}
+#endif
mLayers.clear();
mContext->setContentDrawBounds(mContentDrawBounds);
diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp
index 2904dfe76f40..708b0113e13e 100644
--- a/libs/hwui/renderthread/EglManager.cpp
+++ b/libs/hwui/renderthread/EglManager.cpp
@@ -442,14 +442,17 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window,
}
// TODO: maybe we want to get rid of the WCG check if overlay properties just works?
- const bool canUseFp16 = DeviceInfo::get()->isSupportFp16ForHdr() ||
- DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType;
-
- if (canUseFp16) {
- if (mEglConfigF16 == EGL_NO_CONFIG_KHR) {
- colorMode = ColorMode::Default;
- } else {
- config = mEglConfigF16;
+ bool canUseFp16 = DeviceInfo::get()->isSupportFp16ForHdr() ||
+ DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType;
+
+ if (colorMode == ColorMode::Hdr) {
+ if (canUseFp16 && !DeviceInfo::get()->isSupportRgba10101010ForHdr()) {
+ if (mEglConfigF16 == EGL_NO_CONFIG_KHR) {
+ // If the driver doesn't support fp16 then fallback to 8-bit
+ canUseFp16 = false;
+ } else {
+ config = mEglConfigF16;
+ }
}
}
diff --git a/libs/hwui/renderthread/HintSessionWrapper.cpp b/libs/hwui/renderthread/HintSessionWrapper.cpp
index 2362331aca26..7a155c583fd4 100644
--- a/libs/hwui/renderthread/HintSessionWrapper.cpp
+++ b/libs/hwui/renderthread/HintSessionWrapper.cpp
@@ -20,6 +20,7 @@
#include <private/performance_hint_private.h>
#include <utils/Log.h>
+#include <algorithm>
#include <chrono>
#include <vector>
@@ -44,11 +45,12 @@ void HintSessionWrapper::HintSessionBinding::init() {
LOG_ALWAYS_FATAL_IF(handle_ == nullptr, "Failed to dlopen libandroid.so!");
BIND_APH_METHOD(getManager);
- BIND_APH_METHOD(createSession);
+ BIND_APH_METHOD(createSessionInternal);
BIND_APH_METHOD(closeSession);
BIND_APH_METHOD(updateTargetWorkDuration);
BIND_APH_METHOD(reportActualWorkDuration);
BIND_APH_METHOD(sendHint);
+ BIND_APH_METHOD(setThreads);
mInitialized = true;
}
@@ -67,6 +69,10 @@ void HintSessionWrapper::destroy() {
mHintSession = mHintSessionFuture->get();
mHintSessionFuture = std::nullopt;
}
+ if (mSetThreadsFuture.has_value()) {
+ mSetThreadsFuture->wait();
+ mSetThreadsFuture = std::nullopt;
+ }
if (mHintSession) {
mBinding->closeSession(mHintSession);
mSessionValid = true;
@@ -106,17 +112,18 @@ bool HintSessionWrapper::init() {
APerformanceHintManager* manager = mBinding->getManager();
if (!manager) return false;
- std::vector<pid_t> tids = CommonPool::getThreadIds();
- tids.push_back(mUiThreadId);
- tids.push_back(mRenderThreadId);
+ mPermanentSessionTids = CommonPool::getThreadIds();
+ mPermanentSessionTids.push_back(mUiThreadId);
+ mPermanentSessionTids.push_back(mRenderThreadId);
// Use the cached target value if there is one, otherwise use a default. This is to ensure
// the cached target and target in PowerHAL are consistent, and that it updates correctly
// whenever there is a change.
int64_t targetDurationNanos =
mLastTargetWorkDuration == 0 ? kDefaultTargetDuration : mLastTargetWorkDuration;
- mHintSessionFuture = CommonPool::async([=, this, tids = std::move(tids)] {
- return mBinding->createSession(manager, tids.data(), tids.size(), targetDurationNanos);
+ mHintSessionFuture = CommonPool::async([=, this, tids = mPermanentSessionTids] {
+ return mBinding->createSessionInternal(manager, tids.data(), tids.size(),
+ targetDurationNanos, SessionTag::HWUI);
});
return false;
}
@@ -143,6 +150,23 @@ void HintSessionWrapper::reportActualWorkDuration(long actualDurationNanos) {
mLastFrameNotification = systemTime();
}
+void HintSessionWrapper::setActiveFunctorThreads(std::vector<pid_t> threadIds) {
+ if (!init()) return;
+ if (!mBinding || !mHintSession) return;
+ // Sort the vector to make sure they're compared as sets.
+ std::sort(threadIds.begin(), threadIds.end());
+ if (threadIds == mActiveFunctorTids) return;
+ mActiveFunctorTids = std::move(threadIds);
+ std::vector<pid_t> combinedTids = mPermanentSessionTids;
+ std::copy(mActiveFunctorTids.begin(), mActiveFunctorTids.end(),
+ std::back_inserter(combinedTids));
+ mSetThreadsFuture = CommonPool::async([this, tids = std::move(combinedTids)] {
+ int ret = mBinding->setThreads(mHintSession, tids.data(), tids.size());
+ ALOGE_IF(ret != 0, "APerformaceHint_setThreads failed: %d", ret);
+ return ret;
+ });
+}
+
void HintSessionWrapper::sendLoadResetHint() {
static constexpr int kMaxResetsSinceLastReport = 2;
if (!init()) return;
diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h
index 41891cd80a42..859cc57dea9f 100644
--- a/libs/hwui/renderthread/HintSessionWrapper.h
+++ b/libs/hwui/renderthread/HintSessionWrapper.h
@@ -17,9 +17,11 @@
#pragma once
#include <android/performance_hint.h>
+#include <private/performance_hint_private.h>
#include <future>
#include <optional>
+#include <vector>
#include "utils/TimeUtils.h"
@@ -47,11 +49,15 @@ public:
nsecs_t getLastUpdate();
void delayedDestroy(renderthread::RenderThread& rt, nsecs_t delay,
std::shared_ptr<HintSessionWrapper> wrapperPtr);
+ // Must be called on Render thread. Otherwise can cause a race condition.
+ void setActiveFunctorThreads(std::vector<pid_t> threadIds);
private:
APerformanceHintSession* mHintSession = nullptr;
// This needs to work concurrently for testing
std::optional<std::shared_future<APerformanceHintSession*>> mHintSessionFuture;
+ // This needs to work concurrently for testing
+ std::optional<std::shared_future<int>> mSetThreadsFuture;
int mResetsSinceLastReport = 0;
nsecs_t mLastFrameNotification = 0;
@@ -59,6 +65,8 @@ private:
pid_t mUiThreadId;
pid_t mRenderThreadId;
+ std::vector<pid_t> mPermanentSessionTids;
+ std::vector<pid_t> mActiveFunctorTids;
bool mSessionValid = true;
@@ -73,15 +81,18 @@ private:
virtual ~HintSessionBinding() = default;
virtual void init();
APerformanceHintManager* (*getManager)();
- APerformanceHintSession* (*createSession)(APerformanceHintManager* manager,
- const int32_t* tids, size_t tidCount,
- int64_t defaultTarget) = nullptr;
+ APerformanceHintSession* (*createSessionInternal)(APerformanceHintManager* manager,
+ const int32_t* tids, size_t tidCount,
+ int64_t defaultTarget,
+ SessionTag tag) = nullptr;
void (*closeSession)(APerformanceHintSession* session) = nullptr;
void (*updateTargetWorkDuration)(APerformanceHintSession* session,
int64_t targetDuration) = nullptr;
void (*reportActualWorkDuration)(APerformanceHintSession* session,
int64_t actualDuration) = nullptr;
void (*sendHint)(APerformanceHintSession* session, int32_t hintId) = nullptr;
+ int (*setThreads)(APerformanceHintSession* session, const pid_t* tids,
+ size_t size) = nullptr;
private:
bool mInitialized = false;
diff --git a/libs/hwui/renderthread/IRenderPipeline.h b/libs/hwui/renderthread/IRenderPipeline.h
index b8c3a4de2bd4..ee1d1f8789d9 100644
--- a/libs/hwui/renderthread/IRenderPipeline.h
+++ b/libs/hwui/renderthread/IRenderPipeline.h
@@ -30,8 +30,6 @@
#include "SwapBehavior.h"
#include "hwui/Bitmap.h"
-class GrDirectContext;
-
struct ANativeWindow;
namespace android {
@@ -94,7 +92,6 @@ public:
virtual void setSurfaceColorProperties(ColorMode colorMode) = 0;
virtual SkColorType getSurfaceColorType() const = 0;
virtual sk_sp<SkColorSpace> getSurfaceColorSpace() = 0;
- virtual GrSurfaceOrigin getSurfaceOrigin() = 0;
virtual void setPictureCapturedCallback(
const std::function<void(sk_sp<SkPicture>&&)>& callback) = 0;
diff --git a/libs/hwui/renderthread/ReliableSurface.h b/libs/hwui/renderthread/ReliableSurface.h
index 595964741049..d6a4d50d3327 100644
--- a/libs/hwui/renderthread/ReliableSurface.h
+++ b/libs/hwui/renderthread/ReliableSurface.h
@@ -21,7 +21,9 @@
#include <apex/window.h>
#include <utils/Errors.h>
#include <utils/Macros.h>
+#ifdef __ANDROID__
#include <utils/NdkUtils.h>
+#endif
#include <utils/StrongPointer.h>
#include <memory>
@@ -62,9 +64,11 @@ private:
mutable std::mutex mMutex;
uint64_t mUsage = AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER;
+#ifdef __ANDROID__
AHardwareBuffer_Format mFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
UniqueAHardwareBuffer mScratchBuffer;
ANativeWindowBuffer* mReservedBuffer = nullptr;
+#endif
base::unique_fd mReservedFenceFd;
bool mHasDequeuedBuffer = false;
int mBufferQueueState = OK;
diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp
index eab36050896f..715153b5083d 100644
--- a/libs/hwui/renderthread/RenderProxy.cpp
+++ b/libs/hwui/renderthread/RenderProxy.cpp
@@ -42,7 +42,11 @@ namespace renderthread {
RenderProxy::RenderProxy(bool translucent, RenderNode* rootRenderNode,
IContextFactory* contextFactory)
: mRenderThread(RenderThread::getInstance()), mContext(nullptr) {
+#ifdef __ANDROID__
pid_t uiThreadId = pthread_gettid_np(pthread_self());
+#else
+ pid_t uiThreadId = 0;
+#endif
pid_t renderThreadId = getRenderThreadTid();
mContext = mRenderThread.queue().runSync([=, this]() -> CanvasContext* {
CanvasContext* context = CanvasContext::create(mRenderThread, translucent, rootRenderNode,
@@ -90,6 +94,7 @@ void RenderProxy::setName(const char* name) {
}
void RenderProxy::setHardwareBuffer(AHardwareBuffer* buffer) {
+#ifdef __ANDROID__
if (buffer) {
AHardwareBuffer_acquire(buffer);
}
@@ -99,6 +104,7 @@ void RenderProxy::setHardwareBuffer(AHardwareBuffer* buffer) {
AHardwareBuffer_release(hardwareBuffer);
}
});
+#endif
}
void RenderProxy::setSurface(ANativeWindow* window, bool enableTimeout) {
@@ -216,7 +222,9 @@ void RenderProxy::cancelLayerUpdate(DeferredLayerUpdater* layer) {
}
void RenderProxy::detachSurfaceTexture(DeferredLayerUpdater* layer) {
+#ifdef __ANDROID__
return mRenderThread.queue().runSync([&]() { layer->detachSurfaceTexture(); });
+#endif
}
void RenderProxy::destroyHardwareResources() {
@@ -324,11 +332,13 @@ void RenderProxy::dumpGraphicsMemory(int fd, bool includeProfileData, bool reset
}
});
}
+#ifdef __ANDROID__
if (!Properties::isolatedProcess) {
std::string grallocInfo;
GraphicBufferAllocator::getInstance().dump(grallocInfo);
dprintf(fd, "%s\n", grallocInfo.c_str());
}
+#endif
}
void RenderProxy::getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage) {
@@ -352,7 +362,11 @@ void RenderProxy::rotateProcessStatsBuffer() {
}
int RenderProxy::getRenderThreadTid() {
+#ifdef __ANDROID__
return mRenderThread.getTid();
+#else
+ return 0;
+#endif
}
void RenderProxy::addRenderNode(RenderNode* node, bool placeFront) {
@@ -461,7 +475,7 @@ void RenderProxy::prepareToDraw(Bitmap& bitmap) {
int RenderProxy::copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap) {
ATRACE_NAME("HardwareBitmap readback");
RenderThread& thread = RenderThread::getInstance();
- if (gettid() == thread.getTid()) {
+ if (RenderThread::isCurrent()) {
// TODO: fix everything that hits this. We should never be triggering a readback ourselves.
return (int)thread.readback().copyHWBitmapInto(hwBitmap, bitmap);
} else {
@@ -472,7 +486,7 @@ int RenderProxy::copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap) {
int RenderProxy::copyImageInto(const sk_sp<SkImage>& image, SkBitmap* bitmap) {
RenderThread& thread = RenderThread::getInstance();
- if (gettid() == thread.getTid()) {
+ if (RenderThread::isCurrent()) {
// TODO: fix everything that hits this. We should never be triggering a readback ourselves.
return (int)thread.readback().copyImageInto(image, bitmap);
} else {
diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp
index b5f7caaf1b5b..0d0af1110ca4 100644
--- a/libs/hwui/renderthread/VulkanManager.cpp
+++ b/libs/hwui/renderthread/VulkanManager.cpp
@@ -25,6 +25,7 @@
#include <android/sync.h>
#include <gui/TraceUtils.h>
#include <include/gpu/ganesh/SkSurfaceGanesh.h>
+#include <include/gpu/ganesh/vk/GrVkBackendSemaphore.h>
#include <include/gpu/ganesh/vk/GrVkBackendSurface.h>
#include <include/gpu/ganesh/vk/GrVkDirectContext.h>
#include <ui/FatVector.h>
@@ -597,15 +598,14 @@ Frame VulkanManager::dequeueNextBuffer(VulkanSurface* surface) {
close(fence_clone);
sync_wait(bufferInfo->dequeue_fence, -1 /* forever */);
} else {
- GrBackendSemaphore backendSemaphore;
- backendSemaphore.initVulkan(semaphore);
+ GrBackendSemaphore beSemaphore = GrBackendSemaphores::MakeVk(semaphore);
// Skia will take ownership of the VkSemaphore and delete it once the wait
// has finished. The VkSemaphore also owns the imported fd, so it will
// close the fd when it is deleted.
- bufferInfo->skSurface->wait(1, &backendSemaphore);
+ bufferInfo->skSurface->wait(1, &beSemaphore);
// The following flush blocks the GPU immediately instead of waiting for
// other drawing ops. It seems dequeue_fence is not respected otherwise.
- // TODO: remove the flush after finding why backendSemaphore is not working.
+ // TODO: remove the flush after finding why beSemaphore is not working.
skgpu::ganesh::FlushAndSubmit(bufferInfo->skSurface.get());
}
}
@@ -626,7 +626,7 @@ class SharedSemaphoreInfo : public LightRefBase<SharedSemaphoreInfo> {
SharedSemaphoreInfo(PFN_vkDestroySemaphore destroyFunction, VkDevice device,
VkSemaphore semaphore)
: mDestroyFunction(destroyFunction), mDevice(device), mSemaphore(semaphore) {
- mGrBackendSemaphore.initVulkan(semaphore);
+ mGrBackendSemaphore = GrBackendSemaphores::MakeVk(mSemaphore);
}
~SharedSemaphoreInfo() { mDestroyFunction(mDevice, mSemaphore, nullptr); }
@@ -798,8 +798,7 @@ status_t VulkanManager::fenceWait(int fence, GrDirectContext* grContext) {
return UNKNOWN_ERROR;
}
- GrBackendSemaphore beSemaphore;
- beSemaphore.initVulkan(semaphore);
+ GrBackendSemaphore beSemaphore = GrBackendSemaphores::MakeVk(semaphore);
// Skia will take ownership of the VkSemaphore and delete it once the wait has finished. The
// VkSemaphore also owns the imported fd, so it will close the fd when it is deleted.
diff --git a/libs/hwui/renderthread/VulkanSurface.cpp b/libs/hwui/renderthread/VulkanSurface.cpp
index a8e85475aff0..0f29613cad33 100644
--- a/libs/hwui/renderthread/VulkanSurface.cpp
+++ b/libs/hwui/renderthread/VulkanSurface.cpp
@@ -322,11 +322,16 @@ bool VulkanSurface::UpdateWindow(ANativeWindow* window, const WindowInfo& window
return false;
}
- err = native_window_set_buffer_count(window, windowInfo.bufferCount);
- if (err != 0) {
- ALOGE("VulkanSurface::UpdateWindow() native_window_set_buffer_count(%zu) failed: %s (%d)",
- windowInfo.bufferCount, strerror(-err), err);
- return false;
+ // If bufferCount == 1 then we're in shared buffer mode and we cannot actually call
+ // set_buffer_count, it'll just fail.
+ if (windowInfo.bufferCount > 1) {
+ err = native_window_set_buffer_count(window, windowInfo.bufferCount);
+ if (err != 0) {
+ ALOGE("VulkanSurface::UpdateWindow() native_window_set_buffer_count(%zu) failed: %s "
+ "(%d)",
+ windowInfo.bufferCount, strerror(-err), err);
+ return false;
+ }
}
err = native_window_set_usage(window, windowInfo.windowUsageFlags);
diff --git a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
index 10a740a1f803..a8db0f4aa4f0 100644
--- a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
+++ b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
@@ -52,12 +52,13 @@ protected:
void init() override;
MOCK_METHOD(APerformanceHintManager*, fakeGetManager, ());
- MOCK_METHOD(APerformanceHintSession*, fakeCreateSession,
- (APerformanceHintManager*, const int32_t*, size_t, int64_t));
+ MOCK_METHOD(APerformanceHintSession*, fakeCreateSessionInternal,
+ (APerformanceHintManager*, const int32_t*, size_t, int64_t, SessionTag));
MOCK_METHOD(void, fakeCloseSession, (APerformanceHintSession*));
MOCK_METHOD(void, fakeUpdateTargetWorkDuration, (APerformanceHintSession*, int64_t));
MOCK_METHOD(void, fakeReportActualWorkDuration, (APerformanceHintSession*, int64_t));
MOCK_METHOD(void, fakeSendHint, (APerformanceHintSession*, int32_t));
+ MOCK_METHOD(int, fakeSetThreads, (APerformanceHintSession*, const std::vector<pid_t>&));
// Needs to be on the binding so it can be accessed from static methods
std::promise<int> allowCreationToFinish;
};
@@ -71,22 +72,28 @@ protected:
// Must be static so we can point to them as normal fn pointers with HintSessionBinding
static APerformanceHintManager* stubGetManager() { return sMockBinding->fakeGetManager(); };
- static APerformanceHintSession* stubCreateSession(APerformanceHintManager* manager,
- const int32_t* ids, size_t idsSize,
- int64_t initialTarget) {
- return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget);
+ static APerformanceHintSession* stubCreateSessionInternal(APerformanceHintManager* manager,
+ const int32_t* ids, size_t idsSize,
+ int64_t initialTarget,
+ SessionTag tag) {
+ return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget,
+ SessionTag::HWUI);
}
- static APerformanceHintSession* stubManagedCreateSession(APerformanceHintManager* manager,
- const int32_t* ids, size_t idsSize,
- int64_t initialTarget) {
+ static APerformanceHintSession* stubManagedCreateSessionInternal(
+ APerformanceHintManager* manager, const int32_t* ids, size_t idsSize,
+ int64_t initialTarget, SessionTag tag) {
sMockBinding->allowCreationToFinish.get_future().wait();
- return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget);
+ return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget,
+ SessionTag::HWUI);
}
- static APerformanceHintSession* stubSlowCreateSession(APerformanceHintManager* manager,
- const int32_t* ids, size_t idsSize,
- int64_t initialTarget) {
+ static APerformanceHintSession* stubSlowCreateSessionInternal(APerformanceHintManager* manager,
+ const int32_t* ids,
+ size_t idsSize,
+ int64_t initialTarget,
+ SessionTag tag) {
std::this_thread::sleep_for(50ms);
- return sMockBinding->fakeCreateSession(manager, ids, idsSize, initialTarget);
+ return sMockBinding->fakeCreateSessionInternal(manager, ids, idsSize, initialTarget,
+ SessionTag::HWUI);
}
static void stubCloseSession(APerformanceHintSession* session) {
sMockBinding->fakeCloseSession(session);
@@ -102,11 +109,20 @@ protected:
static void stubSendHint(APerformanceHintSession* session, int32_t hintId) {
sMockBinding->fakeSendHint(session, hintId);
};
+ static int stubSetThreads(APerformanceHintSession* session, const pid_t* ids, size_t size) {
+ std::vector<pid_t> tids(ids, ids + size);
+ return sMockBinding->fakeSetThreads(session, tids);
+ }
void waitForWrapperReady() {
if (mWrapper->mHintSessionFuture.has_value()) {
mWrapper->mHintSessionFuture->wait();
}
}
+ void waitForSetThreadsReady() {
+ if (mWrapper->mSetThreadsFuture.has_value()) {
+ mWrapper->mSetThreadsFuture->wait();
+ }
+ }
void scheduleDelayedDestroyManaged() {
TestUtils::runOnRenderThread([&](renderthread::RenderThread& rt) {
// Guaranteed to be scheduled first, allows destruction to start
@@ -129,18 +145,20 @@ void HintSessionWrapperTests::SetUp() {
mWrapper = std::make_shared<HintSessionWrapper>(uiThreadId, renderThreadId);
mWrapper->mBinding = sMockBinding;
EXPECT_CALL(*sMockBinding, fakeGetManager).WillOnce(Return(managerPtr));
- ON_CALL(*sMockBinding, fakeCreateSession).WillByDefault(Return(sessionPtr));
+ ON_CALL(*sMockBinding, fakeCreateSessionInternal).WillByDefault(Return(sessionPtr));
+ ON_CALL(*sMockBinding, fakeSetThreads).WillByDefault(Return(0));
}
void HintSessionWrapperTests::MockHintSessionBinding::init() {
sMockBinding->getManager = &stubGetManager;
- if (sMockBinding->createSession == nullptr) {
- sMockBinding->createSession = &stubCreateSession;
+ if (sMockBinding->createSessionInternal == nullptr) {
+ sMockBinding->createSessionInternal = &stubCreateSessionInternal;
}
sMockBinding->closeSession = &stubCloseSession;
sMockBinding->updateTargetWorkDuration = &stubUpdateTargetWorkDuration;
sMockBinding->reportActualWorkDuration = &stubReportActualWorkDuration;
sMockBinding->sendHint = &stubSendHint;
+ sMockBinding->setThreads = &stubSetThreads;
}
void HintSessionWrapperTests::TearDown() {
@@ -151,14 +169,14 @@ void HintSessionWrapperTests::TearDown() {
TEST_F(HintSessionWrapperTests, destructorClosesBackgroundSession) {
EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1);
- sMockBinding->createSession = stubSlowCreateSession;
+ sMockBinding->createSessionInternal = stubSlowCreateSessionInternal;
mWrapper->init();
mWrapper = nullptr;
Mock::VerifyAndClearExpectations(sMockBinding.get());
}
TEST_F(HintSessionWrapperTests, sessionInitializesCorrectly) {
- EXPECT_CALL(*sMockBinding, fakeCreateSession(managerPtr, _, Gt(1), _)).Times(1);
+ EXPECT_CALL(*sMockBinding, fakeCreateSessionInternal(managerPtr, _, Gt(1), _, _)).Times(1);
mWrapper->init();
waitForWrapperReady();
}
@@ -207,7 +225,7 @@ TEST_F(HintSessionWrapperTests, delayedDeletionResolvesBeforeAsyncCreationFinish
// Here we test whether queueing delayedDestroy works while creation is still happening, if
// creation happens after
EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1);
- sMockBinding->createSession = &stubManagedCreateSession;
+ sMockBinding->createSessionInternal = &stubManagedCreateSessionInternal;
// Start creating the session and destroying it at the same time
mWrapper->init();
@@ -234,7 +252,7 @@ TEST_F(HintSessionWrapperTests, delayedDeletionResolvesAfterAsyncCreationFinishe
// Here we test whether queueing delayedDestroy works while creation is still happening, if
// creation happens before
EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1);
- sMockBinding->createSession = &stubManagedCreateSession;
+ sMockBinding->createSessionInternal = &stubManagedCreateSessionInternal;
// Start creating the session and destroying it at the same time
mWrapper->init();
@@ -339,4 +357,44 @@ TEST_F(HintSessionWrapperTests, manualSessionDestroyPlaysNiceWithDelayedDestruct
EXPECT_EQ(mWrapper->alive(), false);
}
+TEST_F(HintSessionWrapperTests, setThreadsUpdatesSessionThreads) {
+ EXPECT_CALL(*sMockBinding, fakeCreateSessionInternal(managerPtr, _, Gt(1), _, _)).Times(1);
+ EXPECT_CALL(*sMockBinding, fakeSetThreads(sessionPtr, testing::IsSupersetOf({11, 22})))
+ .Times(1);
+ mWrapper->init();
+ waitForWrapperReady();
+
+ // This changes the overall set of threads in the session, so the session wrapper should call
+ // setThreads.
+ mWrapper->setActiveFunctorThreads({11, 22});
+ waitForSetThreadsReady();
+
+ // The set of threads doesn't change, so the session wrapper should not call setThreads this
+ // time. The order of the threads shouldn't matter.
+ mWrapper->setActiveFunctorThreads({22, 11});
+ waitForSetThreadsReady();
+}
+
+TEST_F(HintSessionWrapperTests, setThreadsDoesntCrashAfterDestroy) {
+ EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1);
+
+ mWrapper->init();
+ waitForWrapperReady();
+ // Init a second time just to grab the wrapper from the promise
+ mWrapper->init();
+ EXPECT_EQ(mWrapper->alive(), true);
+
+ // Then, kill the session
+ mWrapper->destroy();
+
+ // Verify it died
+ Mock::VerifyAndClearExpectations(sMockBinding.get());
+ EXPECT_EQ(mWrapper->alive(), false);
+
+ // setActiveFunctorThreads shouldn't do anything, and shouldn't crash.
+ EXPECT_CALL(*sMockBinding, fakeSetThreads(_, _)).Times(0);
+ mWrapper->setActiveFunctorThreads({11, 22});
+ waitForSetThreadsReady();
+}
+
} // namespace android::uirenderer::renderthread \ No newline at end of file
diff --git a/libs/hwui/thread/CommonPool.cpp b/libs/hwui/thread/CommonPool.cpp
index dc92f9f0d39a..6c0c30f95955 100644
--- a/libs/hwui/thread/CommonPool.cpp
+++ b/libs/hwui/thread/CommonPool.cpp
@@ -16,16 +16,14 @@
#include "CommonPool.h"
-#include <sys/resource.h>
#include <utils/Trace.h>
-#include "renderthread/RenderThread.h"
#include <array>
namespace android {
namespace uirenderer {
-CommonPool::CommonPool() {
+CommonPool::CommonPool() : CommonPoolBase() {
ATRACE_CALL();
CommonPool* pool = this;
@@ -36,22 +34,7 @@ CommonPool::CommonPool() {
// Create 2 workers
for (int i = 0; i < THREAD_COUNT; i++) {
std::thread worker([pool, i, &mLock, &tids, &tidConditionVars] {
- {
- std::array<char, 20> name{"hwuiTask"};
- snprintf(name.data(), name.size(), "hwuiTask%d", i);
- auto self = pthread_self();
- pthread_setname_np(self, name.data());
- {
- std::unique_lock lock(mLock);
- tids[i] = pthread_gettid_np(self);
- tidConditionVars[i].notify_one();
- }
- setpriority(PRIO_PROCESS, 0, PRIORITY_FOREGROUND);
- auto startHook = renderthread::RenderThread::getOnStartHook();
- if (startHook) {
- startHook(name.data());
- }
- }
+ pool->setupThread(i, mLock, tids, tidConditionVars);
pool->workerLoop();
});
worker.detach();
@@ -64,7 +47,9 @@ CommonPool::CommonPool() {
}
}
}
- mWorkerThreadIds = std::move(tids);
+ if (pool->supportsTid()) {
+ mWorkerThreadIds = std::move(tids);
+ }
}
CommonPool& CommonPool::instance() {
@@ -95,7 +80,7 @@ void CommonPool::enqueue(Task&& task) {
void CommonPool::workerLoop() {
std::unique_lock lock(mLock);
- while (true) {
+ while (!mIsStopping) {
if (!mWorkQueue.hasWork()) {
mWaitingThreads++;
mCondition.wait(lock);
diff --git a/libs/hwui/thread/CommonPool.h b/libs/hwui/thread/CommonPool.h
index 74f852bd1413..0c025b4f0ee7 100644
--- a/libs/hwui/thread/CommonPool.h
+++ b/libs/hwui/thread/CommonPool.h
@@ -17,8 +17,6 @@
#ifndef FRAMEWORKS_BASE_COMMONPOOL_H
#define FRAMEWORKS_BASE_COMMONPOOL_H
-#include "utils/Macros.h"
-
#include <log/log.h>
#include <condition_variable>
@@ -27,6 +25,9 @@
#include <mutex>
#include <vector>
+#include "thread/CommonPoolBase.h"
+#include "utils/Macros.h"
+
namespace android {
namespace uirenderer {
@@ -73,7 +74,7 @@ private:
int mTail = 0;
};
-class CommonPool {
+class CommonPool : private CommonPoolBase {
PREVENT_COPY_AND_ASSIGN(CommonPool);
public:
@@ -107,7 +108,10 @@ private:
static CommonPool& instance();
CommonPool();
- ~CommonPool() {}
+ ~CommonPool() {
+ mIsStopping = true;
+ mCondition.notify_all();
+ }
void enqueue(Task&&);
void doWaitForIdle();
@@ -120,6 +124,7 @@ private:
std::condition_variable mCondition;
int mWaitingThreads = 0;
ArrayQueue<Task, QUEUE_SIZE> mWorkQueue;
+ std::atomic_bool mIsStopping = false;
};
} // namespace uirenderer
diff --git a/libs/hwui/utils/Color.cpp b/libs/hwui/utils/Color.cpp
index 913af8ac3474..6a560b365247 100644
--- a/libs/hwui/utils/Color.cpp
+++ b/libs/hwui/utils/Color.cpp
@@ -16,22 +16,18 @@
#include "Color.h"
-#include <ui/ColorSpace.h>
-#include <utils/Log.h>
-
-#ifdef __ANDROID__ // Layoutlib does not support hardware buffers or native windows
+#include <Properties.h>
#include <android/hardware_buffer.h>
#include <android/native_window.h>
-#endif
+#include <ui/ColorSpace.h>
+#include <utils/Log.h>
#include <algorithm>
#include <cmath>
-#include <Properties.h>
namespace android {
namespace uirenderer {
-#ifdef __ANDROID__ // Layoutlib does not support hardware buffers or native windows
static inline SkImageInfo createImageInfo(int32_t width, int32_t height, int32_t format,
sk_sp<SkColorSpace> colorSpace) {
SkColorType colorType = kUnknown_SkColorType;
@@ -121,7 +117,6 @@ SkColorType BufferFormatToColorType(uint32_t format) {
return kUnknown_SkColorType;
}
}
-#endif
namespace {
static constexpr skcms_TransferFunction k2Dot6 = {2.6f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
@@ -408,7 +403,7 @@ skcms_TransferFunction GetPQSkTransferFunction(float sdr_white_level) {
}
static skcms_TransferFunction trfn_apply_gain(const skcms_TransferFunction trfn, float gain) {
- float pow_gain_ginv = sk_float_pow(gain, 1 / trfn.g);
+ float pow_gain_ginv = std::pow(gain, 1 / trfn.g);
skcms_TransferFunction result;
result.g = trfn.g;
result.a = trfn.a * pow_gain_ginv;
diff --git a/libs/hwui/utils/Color.h b/libs/hwui/utils/Color.h
index 0fd61c7b990b..08f1c9300c30 100644
--- a/libs/hwui/utils/Color.h
+++ b/libs/hwui/utils/Color.h
@@ -92,7 +92,6 @@ static constexpr float EOCF_sRGB(float srgb) {
return srgb <= 0.04045f ? srgb / 12.92f : powf((srgb + 0.055f) / 1.055f, 2.4f);
}
-#ifdef __ANDROID__ // Layoutlib does not support hardware buffers or native windows
SkImageInfo ANativeWindowToImageInfo(const ANativeWindow_Buffer& buffer,
sk_sp<SkColorSpace> colorSpace);
@@ -101,7 +100,6 @@ SkImageInfo BufferDescriptionToImageInfo(const AHardwareBuffer_Desc& bufferDesc,
uint32_t ColorTypeToBufferFormat(SkColorType colorType);
SkColorType BufferFormatToColorType(uint32_t bufferFormat);
-#endif
ANDROID_API sk_sp<SkColorSpace> DataSpaceToColorSpace(android_dataspace dataspace);
diff --git a/libs/hwui/utils/SharedLib.h b/libs/hwui/utils/SharedLib.h
new file mode 100644
index 000000000000..f4dcf0f664a2
--- /dev/null
+++ b/libs/hwui/utils/SharedLib.h
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+#ifndef SHAREDLIB_H
+#define SHAREDLIB_H
+
+#include <string>
+
+namespace android {
+namespace uirenderer {
+
+class SharedLib {
+public:
+ static void* openSharedLib(std::string filename);
+ static void* getSymbol(void* library, const char* symbol);
+};
+
+} /* namespace uirenderer */
+} /* namespace android */
+
+#endif // SHAREDLIB_H
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
index 5ce990fdeb82..7a82938435af 100644
--- a/libs/input/Android.bp
+++ b/libs/input/Android.bp
@@ -46,9 +46,7 @@ cc_library_shared {
"liblog",
"libutils",
"libgui",
- "libui",
"libinput",
- "libnativewindow",
],
header_libs: [
diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp
index 6a465442c2b4..eecc741a3bbb 100644
--- a/libs/input/MouseCursorController.cpp
+++ b/libs/input/MouseCursorController.cpp
@@ -117,7 +117,7 @@ FloatPoint MouseCursorController::getPosition() const {
return {mLocked.pointerX, mLocked.pointerY};
}
-int32_t MouseCursorController::getDisplayId() const {
+ui::LogicalDisplayId MouseCursorController::getDisplayId() const {
std::scoped_lock lock(mLock);
return mLocked.viewport.displayId;
}
@@ -165,6 +165,15 @@ void MouseCursorController::setStylusHoverMode(bool stylusHoverMode) {
}
}
+void MouseCursorController::setSkipScreenshot(bool skip) {
+ std::scoped_lock lock(mLock);
+ if (mLocked.skipScreenshot == skip) {
+ return;
+ }
+ mLocked.skipScreenshot = skip;
+ updatePointerLocked();
+}
+
void MouseCursorController::reloadPointerResources(bool getAdditionalMouseResources) {
std::scoped_lock lock(mLock);
@@ -352,6 +361,7 @@ void MouseCursorController::updatePointerLocked() REQUIRES(mLock) {
mLocked.pointerSprite->setLayer(Sprite::BASE_LAYER_POINTER);
mLocked.pointerSprite->setPosition(mLocked.pointerX, mLocked.pointerY);
mLocked.pointerSprite->setDisplayId(mLocked.viewport.displayId);
+ mLocked.pointerSprite->setSkipScreenshot(mLocked.skipScreenshot);
if (mLocked.pointerAlpha > 0) {
mLocked.pointerSprite->setAlpha(mLocked.pointerAlpha);
@@ -467,10 +477,10 @@ void MouseCursorController::startAnimationLocked() REQUIRES(mLock) {
std::function<bool(nsecs_t)> func = std::bind(&MouseCursorController::doAnimations, this, _1);
/*
- * Using -1 for displayId here to avoid removing the callback
+ * Using ui::LogicalDisplayId::INVALID for displayId here to avoid removing the callback
* if a TouchSpotController with the same display is removed.
*/
- mContext.addAnimationCallback(-1, func);
+ mContext.addAnimationCallback(ui::LogicalDisplayId::INVALID, func);
}
} // namespace android
diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h
index 00dc0854440e..78f6413ff111 100644
--- a/libs/input/MouseCursorController.h
+++ b/libs/input/MouseCursorController.h
@@ -47,12 +47,15 @@ public:
void move(float deltaX, float deltaY);
void setPosition(float x, float y);
FloatPoint getPosition() const;
- int32_t getDisplayId() const;
+ ui::LogicalDisplayId getDisplayId() const;
void fade(PointerControllerInterface::Transition transition);
void unfade(PointerControllerInterface::Transition transition);
void setDisplayViewport(const DisplayViewport& viewport, bool getAdditionalMouseResources);
void setStylusHoverMode(bool stylusHoverMode);
+ // Set/Unset flag to hide the mouse cursor on the mirrored display
+ void setSkipScreenshot(bool skip);
+
void updatePointerIcon(PointerIconStyle iconId);
void setCustomPointerIcon(const SpriteIcon& icon);
void reloadPointerResources(bool getAdditionalMouseResources);
@@ -94,6 +97,7 @@ private:
PointerIconStyle requestedPointerType;
PointerIconStyle resolvedPointerType;
+ bool skipScreenshot{false};
bool animating{false};
} mLocked GUARDED_BY(mLock);
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index f9dc5fac7e21..11b27a214984 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -24,7 +24,6 @@
#include <SkColor.h>
#include <android-base/stringprintf.h>
#include <android-base/thread_annotations.h>
-#include <com_android_input_flags.h>
#include <ftl/enum.h>
#include <mutex>
@@ -35,14 +34,10 @@
#define INDENT2 " "
#define INDENT3 " "
-namespace input_flags = com::android::input::flags;
-
namespace android {
namespace {
-static const bool ENABLE_POINTER_CHOREOGRAPHER = input_flags::enable_pointer_choreographer();
-
const ui::Transform kIdentityTransform;
} // namespace
@@ -68,27 +63,24 @@ void PointerController::DisplayInfoListener::onPointerControllerDestroyed() {
std::shared_ptr<PointerController> PointerController::create(
const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled, ControllerType type) {
+ SpriteController& spriteController, ControllerType type) {
// using 'new' to access non-public constructor
std::shared_ptr<PointerController> controller;
switch (type) {
case ControllerType::MOUSE:
controller = std::shared_ptr<PointerController>(
- new MousePointerController(policy, looper, spriteController, enabled));
+ new MousePointerController(policy, looper, spriteController));
break;
case ControllerType::TOUCH:
controller = std::shared_ptr<PointerController>(
- new TouchPointerController(policy, looper, spriteController, enabled));
+ new TouchPointerController(policy, looper, spriteController));
break;
case ControllerType::STYLUS:
controller = std::shared_ptr<PointerController>(
- new StylusPointerController(policy, looper, spriteController, enabled));
+ new StylusPointerController(policy, looper, spriteController));
break;
- case ControllerType::LEGACY:
default:
- controller = std::shared_ptr<PointerController>(
- new PointerController(policy, looper, spriteController, enabled));
- break;
+ LOG_ALWAYS_FATAL("Invalid ControllerType: %d", static_cast<int>(type));
}
/*
@@ -108,10 +100,9 @@ std::shared_ptr<PointerController> PointerController::create(
}
PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy,
- const sp<Looper>& looper, SpriteController& spriteController,
- bool enabled)
+ const sp<Looper>& looper, SpriteController& spriteController)
: PointerController(
- policy, looper, spriteController, enabled,
+ policy, looper, spriteController,
[](const sp<android::gui::WindowInfosListener>& listener) {
auto initialInfo = std::make_pair(std::vector<android::gui::WindowInfo>{},
std::vector<android::gui::DisplayInfo>{});
@@ -125,11 +116,9 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>&
PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy,
const sp<Looper>& looper, SpriteController& spriteController,
- bool enabled,
const WindowListenerRegisterConsumer& registerListener,
WindowListenerUnregisterConsumer unregisterListener)
- : mEnabled(enabled),
- mContext(policy, looper, spriteController, *this),
+ : mContext(policy, looper, spriteController, *this),
mCursorController(mContext),
mDisplayInfoListener(sp<DisplayInfoListener>::make(this)),
mUnregisterWindowInfosListener(std::move(unregisterListener)) {
@@ -142,7 +131,6 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>&
PointerController::~PointerController() {
mDisplayInfoListener->onPointerControllerDestroyed();
mUnregisterWindowInfosListener(mDisplayInfoListener);
- mContext.getPolicy()->onPointerDisplayIdChanged(ADISPLAY_ID_NONE, FloatPoint{0, 0});
}
std::mutex& PointerController::getLock() const {
@@ -150,15 +138,11 @@ std::mutex& PointerController::getLock() const {
}
std::optional<FloatRect> PointerController::getBounds() const {
- if (!mEnabled) return {};
-
return mCursorController.getBounds();
}
void PointerController::move(float deltaX, float deltaY) {
- if (!mEnabled) return;
-
- const int32_t displayId = mCursorController.getDisplayId();
+ const ui::LogicalDisplayId displayId = mCursorController.getDisplayId();
vec2 transformed;
{
std::scoped_lock lock(getLock());
@@ -169,9 +153,7 @@ void PointerController::move(float deltaX, float deltaY) {
}
void PointerController::setPosition(float x, float y) {
- if (!mEnabled) return;
-
- const int32_t displayId = mCursorController.getDisplayId();
+ const ui::LogicalDisplayId displayId = mCursorController.getDisplayId();
vec2 transformed;
{
std::scoped_lock lock(getLock());
@@ -182,11 +164,7 @@ void PointerController::setPosition(float x, float y) {
}
FloatPoint PointerController::getPosition() const {
- if (!mEnabled) {
- return FloatPoint{0, 0};
- }
-
- const int32_t displayId = mCursorController.getDisplayId();
+ const ui::LogicalDisplayId displayId = mCursorController.getDisplayId();
const auto p = mCursorController.getPosition();
{
std::scoped_lock lock(getLock());
@@ -195,29 +173,21 @@ FloatPoint PointerController::getPosition() const {
}
}
-int32_t PointerController::getDisplayId() const {
- if (!mEnabled) return ADISPLAY_ID_NONE;
-
+ui::LogicalDisplayId PointerController::getDisplayId() const {
return mCursorController.getDisplayId();
}
void PointerController::fade(Transition transition) {
- if (!mEnabled) return;
-
std::scoped_lock lock(getLock());
mCursorController.fade(transition);
}
void PointerController::unfade(Transition transition) {
- if (!mEnabled) return;
-
std::scoped_lock lock(getLock());
mCursorController.unfade(transition);
}
void PointerController::setPresentation(Presentation presentation) {
- if (!mEnabled) return;
-
std::scoped_lock lock(getLock());
if (mLocked.presentation == presentation) {
@@ -226,33 +196,13 @@ void PointerController::setPresentation(Presentation presentation) {
mLocked.presentation = presentation;
- if (ENABLE_POINTER_CHOREOGRAPHER) {
- // When pointer choreographer is enabled, the presentation mode is only set once when the
- // PointerController is constructed, before the display viewport is provided.
- // TODO(b/293587049): Clean up the PointerController interface after pointer choreographer
- // is permanently enabled. The presentation can be set in the constructor.
- mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER);
- return;
- }
-
- if (!mCursorController.isViewportValid()) {
- return;
- }
-
- if (presentation == Presentation::POINTER || presentation == Presentation::STYLUS_HOVER) {
- // For now, we support stylus hover using the mouse cursor implementation.
- // TODO: Add proper support for stylus hover icons.
- mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER);
-
- mCursorController.getAdditionalMouseResources();
- clearSpotsLocked();
- }
+ // The presentation mode is only set once when the PointerController is constructed,
+ // before the display viewport is provided.
+ mCursorController.setStylusHoverMode(presentation == Presentation::STYLUS_HOVER);
}
void PointerController::setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
- BitSet32 spotIdBits, int32_t displayId) {
- if (!mEnabled) return;
-
+ BitSet32 spotIdBits, ui::LogicalDisplayId displayId) {
std::scoped_lock lock(getLock());
std::array<PointerCoords, MAX_POINTERS> outSpotCoords{};
const ui::Transform& transform = getTransformForDisplayLocked(displayId);
@@ -272,12 +222,13 @@ void PointerController::setSpots(const PointerCoords* spotCoords, const uint32_t
if (it == mLocked.spotControllers.end()) {
mLocked.spotControllers.try_emplace(displayId, displayId, mContext);
}
- mLocked.spotControllers.at(displayId).setSpots(outSpotCoords.data(), spotIdToIndex, spotIdBits);
+ bool skipScreenshot = mLocked.displaysToSkipScreenshot.find(displayId) !=
+ mLocked.displaysToSkipScreenshot.end();
+ mLocked.spotControllers.at(displayId).setSpots(outSpotCoords.data(), spotIdToIndex, spotIdBits,
+ skipScreenshot);
}
void PointerController::clearSpots() {
- if (!mEnabled) return;
-
std::scoped_lock lock(getLock());
clearSpotsLocked();
}
@@ -310,12 +261,6 @@ void PointerController::reloadPointerResources() {
}
void PointerController::setDisplayViewport(const DisplayViewport& viewport) {
- struct PointerDisplayChangeArgs {
- int32_t displayId;
- FloatPoint cursorPosition;
- };
- std::optional<PointerDisplayChangeArgs> pointerDisplayChanged;
-
{ // acquire lock
std::scoped_lock lock(getLock());
@@ -327,44 +272,45 @@ void PointerController::setDisplayViewport(const DisplayViewport& viewport) {
mCursorController.setDisplayViewport(viewport, getAdditionalMouseResources);
if (viewport.displayId != mLocked.pointerDisplayId) {
mLocked.pointerDisplayId = viewport.displayId;
- pointerDisplayChanged = {viewport.displayId, mCursorController.getPosition()};
}
} // release lock
-
- if (pointerDisplayChanged) {
- // Notify the policy without holding the pointer controller lock.
- mContext.getPolicy()->onPointerDisplayIdChanged(pointerDisplayChanged->displayId,
- pointerDisplayChanged->cursorPosition);
- }
}
void PointerController::updatePointerIcon(PointerIconStyle iconId) {
- if (!mEnabled) return;
-
std::scoped_lock lock(getLock());
mCursorController.updatePointerIcon(iconId);
}
void PointerController::setCustomPointerIcon(const SpriteIcon& icon) {
- if (!mEnabled) return;
-
std::scoped_lock lock(getLock());
mCursorController.setCustomPointerIcon(icon);
}
+void PointerController::setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId displayId) {
+ std::scoped_lock lock(getLock());
+ mLocked.displaysToSkipScreenshot.insert(displayId);
+ mCursorController.setSkipScreenshot(true);
+}
+
+void PointerController::clearSkipScreenshotFlags() {
+ std::scoped_lock lock(getLock());
+ mLocked.displaysToSkipScreenshot.clear();
+ mCursorController.setSkipScreenshot(false);
+}
+
void PointerController::doInactivityTimeout() {
fade(Transition::GRADUAL);
}
void PointerController::onDisplayViewportsUpdated(const std::vector<DisplayViewport>& viewports) {
- std::unordered_set<int32_t> displayIdSet;
+ std::unordered_set<ui::LogicalDisplayId> displayIdSet;
for (const DisplayViewport& viewport : viewports) {
displayIdSet.insert(viewport.displayId);
}
std::scoped_lock lock(getLock());
for (auto it = mLocked.spotControllers.begin(); it != mLocked.spotControllers.end();) {
- int32_t displayId = it->first;
+ ui::LogicalDisplayId displayId = it->first;
if (!displayIdSet.count(displayId)) {
/*
* Ensures that an in-progress animation won't dereference
@@ -383,7 +329,8 @@ void PointerController::onDisplayInfosChangedLocked(
mLocked.mDisplayInfos = displayInfo;
}
-const ui::Transform& PointerController::getTransformForDisplayLocked(int displayId) const {
+const ui::Transform& PointerController::getTransformForDisplayLocked(
+ ui::LogicalDisplayId displayId) const {
const auto& di = mLocked.mDisplayInfos;
auto it = std::find_if(di.begin(), di.end(), [displayId](const gui::DisplayInfo& info) {
return info.displayId == displayId;
@@ -392,15 +339,12 @@ const ui::Transform& PointerController::getTransformForDisplayLocked(int display
}
std::string PointerController::dump() {
- if (!mEnabled) {
- return INDENT "PointerController: DISABLED due to ongoing PointerChoreographer refactor\n";
- }
-
std::string dump = INDENT "PointerController:\n";
std::scoped_lock lock(getLock());
dump += StringPrintf(INDENT2 "Presentation: %s\n",
ftl::enum_string(mLocked.presentation).c_str());
- dump += StringPrintf(INDENT2 "Pointer Display ID: %" PRIu32 "\n", mLocked.pointerDisplayId);
+ dump += StringPrintf(INDENT2 "Pointer Display ID: %s\n",
+ mLocked.pointerDisplayId.toString().c_str());
dump += StringPrintf(INDENT2 "Viewports:\n");
for (const auto& info : mLocked.mDisplayInfos) {
info.dump(dump, INDENT3);
@@ -416,8 +360,8 @@ std::string PointerController::dump() {
MousePointerController::MousePointerController(const sp<PointerControllerPolicyInterface>& policy,
const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled)
- : PointerController(policy, looper, spriteController, enabled) {
+ SpriteController& spriteController)
+ : PointerController(policy, looper, spriteController) {
PointerController::setPresentation(Presentation::POINTER);
}
@@ -429,8 +373,8 @@ MousePointerController::~MousePointerController() {
TouchPointerController::TouchPointerController(const sp<PointerControllerPolicyInterface>& policy,
const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled)
- : PointerController(policy, looper, spriteController, enabled) {
+ SpriteController& spriteController)
+ : PointerController(policy, looper, spriteController) {
PointerController::setPresentation(Presentation::SPOT);
}
@@ -442,8 +386,8 @@ TouchPointerController::~TouchPointerController() {
StylusPointerController::StylusPointerController(const sp<PointerControllerPolicyInterface>& policy,
const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled)
- : PointerController(policy, looper, spriteController, enabled) {
+ SpriteController& spriteController)
+ : PointerController(policy, looper, spriteController) {
PointerController::setPresentation(Presentation::STYLUS_HOVER);
}
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index 6ee5707622ca..4d1e1d733cc1 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -47,8 +47,7 @@ class PointerController : public PointerControllerInterface {
public:
static std::shared_ptr<PointerController> create(
const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled,
- ControllerType type = ControllerType::LEGACY);
+ SpriteController& spriteController, ControllerType type);
~PointerController() override;
@@ -56,17 +55,19 @@ public:
void move(float deltaX, float deltaY) override;
void setPosition(float x, float y) override;
FloatPoint getPosition() const override;
- int32_t getDisplayId() const override;
+ ui::LogicalDisplayId getDisplayId() const override;
void fade(Transition transition) override;
void unfade(Transition transition) override;
void setDisplayViewport(const DisplayViewport& viewport) override;
void setPresentation(Presentation presentation) override;
void setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
- BitSet32 spotIdBits, int32_t displayId) override;
+ BitSet32 spotIdBits, ui::LogicalDisplayId displayId) override;
void clearSpots() override;
void updatePointerIcon(PointerIconStyle iconId) override;
void setCustomPointerIcon(const SpriteIcon& icon) override;
+ void setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId displayId) override;
+ void clearSkipScreenshotFlags() override;
virtual void setInactivityTimeout(InactivityTimeout inactivityTimeout);
void doInactivityTimeout();
@@ -86,12 +87,12 @@ protected:
// Constructor used to test WindowInfosListener registration.
PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled,
+ SpriteController& spriteController,
const WindowListenerRegisterConsumer& registerListener,
WindowListenerUnregisterConsumer unregisterListener);
PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper,
- SpriteController& spriteController, bool enabled);
+ SpriteController& spriteController);
private:
friend PointerControllerContext::LooperCallback;
@@ -103,18 +104,17 @@ private:
// we use the DisplayInfoListener's lock in PointerController.
std::mutex& getLock() const;
- const bool mEnabled;
-
PointerControllerContext mContext;
MouseCursorController mCursorController;
struct Locked {
Presentation presentation;
- int32_t pointerDisplayId = ADISPLAY_ID_NONE;
+ ui::LogicalDisplayId pointerDisplayId = ui::LogicalDisplayId::INVALID;
std::vector<gui::DisplayInfo> mDisplayInfos;
- std::unordered_map<int32_t /* displayId */, TouchSpotController> spotControllers;
+ std::unordered_map<ui::LogicalDisplayId, TouchSpotController> spotControllers;
+ std::unordered_set<ui::LogicalDisplayId> displaysToSkipScreenshot;
} mLocked GUARDED_BY(getLock());
class DisplayInfoListener : public gui::WindowInfosListener {
@@ -133,7 +133,8 @@ private:
sp<DisplayInfoListener> mDisplayInfoListener;
const WindowListenerUnregisterConsumer mUnregisterWindowInfosListener;
- const ui::Transform& getTransformForDisplayLocked(int displayId) const REQUIRES(getLock());
+ const ui::Transform& getTransformForDisplayLocked(ui::LogicalDisplayId displayId) const
+ REQUIRES(getLock());
void clearSpotsLocked() REQUIRES(getLock());
};
@@ -142,15 +143,14 @@ class MousePointerController : public PointerController {
public:
/** A version of PointerController that controls one mouse pointer. */
MousePointerController(const sp<PointerControllerPolicyInterface>& policy,
- const sp<Looper>& looper, SpriteController& spriteController,
- bool enabled);
+ const sp<Looper>& looper, SpriteController& spriteController);
~MousePointerController() override;
void setPresentation(Presentation) override {
LOG_ALWAYS_FATAL("Should not be called");
}
- void setSpots(const PointerCoords*, const uint32_t*, BitSet32, int32_t) override {
+ void setSpots(const PointerCoords*, const uint32_t*, BitSet32, ui::LogicalDisplayId) override {
LOG_ALWAYS_FATAL("Should not be called");
}
void clearSpots() override {
@@ -162,8 +162,7 @@ class TouchPointerController : public PointerController {
public:
/** A version of PointerController that controls touch spots. */
TouchPointerController(const sp<PointerControllerPolicyInterface>& policy,
- const sp<Looper>& looper, SpriteController& spriteController,
- bool enabled);
+ const sp<Looper>& looper, SpriteController& spriteController);
~TouchPointerController() override;
@@ -179,7 +178,7 @@ public:
FloatPoint getPosition() const override {
LOG_ALWAYS_FATAL("Should not be called");
}
- int32_t getDisplayId() const override {
+ ui::LogicalDisplayId getDisplayId() const override {
LOG_ALWAYS_FATAL("Should not be called");
}
void fade(Transition) override {
@@ -208,15 +207,14 @@ class StylusPointerController : public PointerController {
public:
/** A version of PointerController that controls one stylus pointer. */
StylusPointerController(const sp<PointerControllerPolicyInterface>& policy,
- const sp<Looper>& looper, SpriteController& spriteController,
- bool enabled);
+ const sp<Looper>& looper, SpriteController& spriteController);
~StylusPointerController() override;
void setPresentation(Presentation) override {
LOG_ALWAYS_FATAL("Should not be called");
}
- void setSpots(const PointerCoords*, const uint32_t*, BitSet32, int32_t) override {
+ void setSpots(const PointerCoords*, const uint32_t*, BitSet32, ui::LogicalDisplayId) override {
LOG_ALWAYS_FATAL("Should not be called");
}
void clearSpots() override {
diff --git a/libs/input/PointerControllerContext.cpp b/libs/input/PointerControllerContext.cpp
index 15c35176afce..747eb8e5ad1b 100644
--- a/libs/input/PointerControllerContext.cpp
+++ b/libs/input/PointerControllerContext.cpp
@@ -138,12 +138,12 @@ int PointerControllerContext::LooperCallback::handleEvent(int /* fd */, int even
return 1; // keep the callback
}
-void PointerControllerContext::addAnimationCallback(int32_t displayId,
+void PointerControllerContext::addAnimationCallback(ui::LogicalDisplayId displayId,
std::function<bool(nsecs_t)> callback) {
mAnimator.addCallback(displayId, callback);
}
-void PointerControllerContext::removeAnimationCallback(int32_t displayId) {
+void PointerControllerContext::removeAnimationCallback(ui::LogicalDisplayId displayId) {
mAnimator.removeCallback(displayId);
}
@@ -161,14 +161,14 @@ void PointerControllerContext::PointerAnimator::initializeDisplayEventReceiver()
}
}
-void PointerControllerContext::PointerAnimator::addCallback(int32_t displayId,
+void PointerControllerContext::PointerAnimator::addCallback(ui::LogicalDisplayId displayId,
std::function<bool(nsecs_t)> callback) {
std::scoped_lock lock(mLock);
mLocked.callbacks[displayId] = callback;
startAnimationLocked();
}
-void PointerControllerContext::PointerAnimator::removeCallback(int32_t displayId) {
+void PointerControllerContext::PointerAnimator::removeCallback(ui::LogicalDisplayId displayId) {
std::scoped_lock lock(mLock);
auto it = mLocked.callbacks.find(displayId);
if (it == mLocked.callbacks.end()) {
diff --git a/libs/input/PointerControllerContext.h b/libs/input/PointerControllerContext.h
index 98c3988e7df4..d42214883d3a 100644
--- a/libs/input/PointerControllerContext.h
+++ b/libs/input/PointerControllerContext.h
@@ -72,16 +72,16 @@ protected:
virtual ~PointerControllerPolicyInterface() {}
public:
- virtual void loadPointerIcon(SpriteIcon* icon, int32_t displayId) = 0;
- virtual void loadPointerResources(PointerResources* outResources, int32_t displayId) = 0;
+ virtual void loadPointerIcon(SpriteIcon* icon, ui::LogicalDisplayId displayId) = 0;
+ virtual void loadPointerResources(PointerResources* outResources,
+ ui::LogicalDisplayId displayId) = 0;
virtual void loadAdditionalMouseResources(
std::map<PointerIconStyle, SpriteIcon>* outResources,
std::map<PointerIconStyle, PointerAnimation>* outAnimationResources,
- int32_t displayId) = 0;
+ ui::LogicalDisplayId displayId) = 0;
virtual PointerIconStyle getDefaultPointerIconId() = 0;
virtual PointerIconStyle getDefaultStylusIconId() = 0;
virtual PointerIconStyle getCustomPointerIconId() = 0;
- virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) = 0;
};
/*
@@ -103,7 +103,7 @@ public:
nsecs_t getAnimationTime();
- void clearSpotsByDisplay(int32_t displayId);
+ void clearSpotsByDisplay(ui::LogicalDisplayId displayId);
void setHandlerController(std::shared_ptr<PointerController> controller);
void setCallbackController(std::shared_ptr<PointerController> controller);
@@ -113,8 +113,9 @@ public:
void handleDisplayEvents();
- void addAnimationCallback(int32_t displayId, std::function<bool(nsecs_t)> callback);
- void removeAnimationCallback(int32_t displayId);
+ void addAnimationCallback(ui::LogicalDisplayId displayId,
+ std::function<bool(nsecs_t)> callback);
+ void removeAnimationCallback(ui::LogicalDisplayId displayId);
class MessageHandler : public virtual android::MessageHandler {
public:
@@ -137,8 +138,8 @@ private:
public:
PointerAnimator(PointerControllerContext& context);
- void addCallback(int32_t displayId, std::function<bool(nsecs_t)> callback);
- void removeCallback(int32_t displayId);
+ void addCallback(ui::LogicalDisplayId displayId, std::function<bool(nsecs_t)> callback);
+ void removeCallback(ui::LogicalDisplayId displayId);
void handleVsyncEvents();
nsecs_t getAnimationTimeLocked();
@@ -149,7 +150,7 @@ private:
bool animationPending{false};
nsecs_t animationTime{systemTime(SYSTEM_TIME_MONOTONIC)};
- std::unordered_map<int32_t, std::function<bool(nsecs_t)>> callbacks;
+ std::unordered_map<ui::LogicalDisplayId, std::function<bool(nsecs_t)>> callbacks;
} mLocked GUARDED_BY(mLock);
DisplayEventReceiver mDisplayEventReceiver;
diff --git a/libs/input/SpriteController.cpp b/libs/input/SpriteController.cpp
index 6dc45a6aebec..af499390d390 100644
--- a/libs/input/SpriteController.cpp
+++ b/libs/input/SpriteController.cpp
@@ -19,9 +19,9 @@
#include "SpriteController.h"
-#include <log/log.h>
-#include <utils/String8.h>
+#include <android-base/logging.h>
#include <gui/Surface.h>
+#include <utils/String8.h>
namespace android {
@@ -129,7 +129,7 @@ void SpriteController::doUpdateSprites() {
update.state.surfaceVisible = false;
update.state.surfaceControl =
obtainSurface(update.state.surfaceWidth, update.state.surfaceHeight,
- update.state.displayId);
+ update.state.displayId, update.state.skipScreenshot);
if (update.state.surfaceControl != NULL) {
update.surfaceChanged = surfaceChanged = true;
}
@@ -148,8 +148,9 @@ void SpriteController::doUpdateSprites() {
if (update.state.wantSurfaceVisible()) {
int32_t desiredWidth = update.state.icon.width();
int32_t desiredHeight = update.state.icon.height();
- if (update.state.surfaceWidth < desiredWidth
- || update.state.surfaceHeight < desiredHeight) {
+ // TODO(b/331260947): investigate using a larger surface width with smaller sprites.
+ if (update.state.surfaceWidth != desiredWidth ||
+ update.state.surfaceHeight != desiredHeight) {
needApplyTransaction = true;
update.state.surfaceControl->updateDefaultBufferSize(desiredWidth, desiredHeight);
@@ -202,11 +203,13 @@ void SpriteController::doUpdateSprites() {
&& update.state.surfaceDrawn;
bool becomingVisible = wantSurfaceVisibleAndDrawn && !update.state.surfaceVisible;
bool becomingHidden = !wantSurfaceVisibleAndDrawn && update.state.surfaceVisible;
- if (update.state.surfaceControl != NULL && (becomingVisible || becomingHidden
- || (wantSurfaceVisibleAndDrawn && (update.state.dirty & (DIRTY_ALPHA
- | DIRTY_POSITION | DIRTY_TRANSFORMATION_MATRIX | DIRTY_LAYER
- | DIRTY_VISIBILITY | DIRTY_HOTSPOT | DIRTY_DISPLAY_ID
- | DIRTY_ICON_STYLE))))) {
+ if (update.state.surfaceControl != NULL &&
+ (becomingVisible || becomingHidden ||
+ (wantSurfaceVisibleAndDrawn &&
+ (update.state.dirty &
+ (DIRTY_ALPHA | DIRTY_POSITION | DIRTY_TRANSFORMATION_MATRIX | DIRTY_LAYER |
+ DIRTY_VISIBILITY | DIRTY_HOTSPOT | DIRTY_DISPLAY_ID | DIRTY_ICON_STYLE |
+ DIRTY_DRAW_DROP_SHADOW | DIRTY_SKIP_SCREENSHOT))))) {
needApplyTransaction = true;
if (wantSurfaceVisibleAndDrawn
@@ -235,13 +238,15 @@ void SpriteController::doUpdateSprites() {
update.state.transformationMatrix.dtdy);
}
- if (wantSurfaceVisibleAndDrawn
- && (becomingVisible
- || (update.state.dirty & (DIRTY_HOTSPOT | DIRTY_ICON_STYLE)))) {
+ if (wantSurfaceVisibleAndDrawn &&
+ (becomingVisible ||
+ (update.state.dirty &
+ (DIRTY_HOTSPOT | DIRTY_ICON_STYLE | DIRTY_DRAW_DROP_SHADOW)))) {
Parcel p;
p.writeInt32(static_cast<int32_t>(update.state.icon.style));
p.writeFloat(update.state.icon.hotSpotX);
p.writeFloat(update.state.icon.hotSpotY);
+ p.writeBool(update.state.icon.drawNativeDropShadow);
// Pass cursor metadata in the sprite surface so that when Android is running as a
// client OS (e.g. ARC++) the host OS can get the requested cursor metadata and
@@ -255,6 +260,14 @@ void SpriteController::doUpdateSprites() {
t.setLayer(update.state.surfaceControl, surfaceLayer);
}
+ if (wantSurfaceVisibleAndDrawn &&
+ (becomingVisible || (update.state.dirty & DIRTY_SKIP_SCREENSHOT))) {
+ int32_t flags =
+ update.state.skipScreenshot ? ISurfaceComposerClient::eSkipScreenshot : 0;
+ t.setFlags(update.state.surfaceControl, flags,
+ ISurfaceComposerClient::eSkipScreenshot);
+ }
+
if (becomingVisible) {
t.show(update.state.surfaceControl);
@@ -328,19 +341,22 @@ void SpriteController::ensureSurfaceComposerClient() {
}
sp<SurfaceControl> SpriteController::obtainSurface(int32_t width, int32_t height,
- int32_t displayId) {
+ ui::LogicalDisplayId displayId,
+ bool hideOnMirrored) {
ensureSurfaceComposerClient();
const sp<SurfaceControl> parent = mParentSurfaceProvider(displayId);
if (parent == nullptr) {
- ALOGE("Failed to get the parent surface for pointers on display %d", displayId);
+ LOG(ERROR) << "Failed to get the parent surface for pointers on display " << displayId;
}
+ int32_t createFlags = ISurfaceComposerClient::eHidden | ISurfaceComposerClient::eCursorWindow;
+ if (hideOnMirrored) {
+ createFlags |= ISurfaceComposerClient::eSkipScreenshot;
+ }
const sp<SurfaceControl> surfaceControl =
mSurfaceComposerClient->createSurface(String8("Sprite"), width, height,
- PIXEL_FORMAT_RGBA_8888,
- ISurfaceComposerClient::eHidden |
- ISurfaceComposerClient::eCursorWindow,
+ PIXEL_FORMAT_RGBA_8888, createFlags,
parent ? parent->getHandle() : nullptr);
if (surfaceControl == nullptr || !surfaceControl->isValid()) {
ALOGE("Error creating sprite surface.");
@@ -388,12 +404,13 @@ void SpriteController::SpriteImpl::setIcon(const SpriteIcon& icon) {
uint32_t dirty;
if (icon.isValid()) {
mLocked.state.icon.bitmap = icon.bitmap.copy(ANDROID_BITMAP_FORMAT_RGBA_8888);
- if (!mLocked.state.icon.isValid()
- || mLocked.state.icon.hotSpotX != icon.hotSpotX
- || mLocked.state.icon.hotSpotY != icon.hotSpotY) {
+ if (!mLocked.state.icon.isValid() || mLocked.state.icon.hotSpotX != icon.hotSpotX ||
+ mLocked.state.icon.hotSpotY != icon.hotSpotY ||
+ mLocked.state.icon.drawNativeDropShadow != icon.drawNativeDropShadow) {
mLocked.state.icon.hotSpotX = icon.hotSpotX;
mLocked.state.icon.hotSpotY = icon.hotSpotY;
- dirty = DIRTY_BITMAP | DIRTY_HOTSPOT;
+ mLocked.state.icon.drawNativeDropShadow = icon.drawNativeDropShadow;
+ dirty = DIRTY_BITMAP | DIRTY_HOTSPOT | DIRTY_DRAW_DROP_SHADOW;
} else {
dirty = DIRTY_BITMAP;
}
@@ -404,7 +421,7 @@ void SpriteController::SpriteImpl::setIcon(const SpriteIcon& icon) {
}
} else if (mLocked.state.icon.isValid()) {
mLocked.state.icon.bitmap.reset();
- dirty = DIRTY_BITMAP | DIRTY_HOTSPOT | DIRTY_ICON_STYLE;
+ dirty = DIRTY_BITMAP | DIRTY_HOTSPOT | DIRTY_ICON_STYLE | DIRTY_DRAW_DROP_SHADOW;
} else {
return; // setting to invalid icon and already invalid so nothing to do
}
@@ -459,7 +476,7 @@ void SpriteController::SpriteImpl::setTransformationMatrix(
}
}
-void SpriteController::SpriteImpl::setDisplayId(int32_t displayId) {
+void SpriteController::SpriteImpl::setDisplayId(ui::LogicalDisplayId displayId) {
AutoMutex _l(mController.mLock);
if (mLocked.state.displayId != displayId) {
@@ -468,6 +485,15 @@ void SpriteController::SpriteImpl::setDisplayId(int32_t displayId) {
}
}
+void SpriteController::SpriteImpl::setSkipScreenshot(bool skip) {
+ AutoMutex _l(mController.mLock);
+
+ if (mLocked.state.skipScreenshot != skip) {
+ mLocked.state.skipScreenshot = skip;
+ invalidateLocked(DIRTY_SKIP_SCREENSHOT);
+ }
+}
+
void SpriteController::SpriteImpl::invalidateLocked(uint32_t dirty) {
bool wasDirty = mLocked.state.dirty;
mLocked.state.dirty |= dirty;
diff --git a/libs/input/SpriteController.h b/libs/input/SpriteController.h
index 04ecb3895aa2..e147c567ae2d 100644
--- a/libs/input/SpriteController.h
+++ b/libs/input/SpriteController.h
@@ -95,7 +95,11 @@ public:
virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix) = 0;
/* Sets the id of the display where the sprite should be shown. */
- virtual void setDisplayId(int32_t displayId) = 0;
+ virtual void setDisplayId(ui::LogicalDisplayId displayId) = 0;
+
+ /* Sets the flag to hide sprite on mirrored displays.
+ * This will add ISurfaceComposerClient::eSkipScreenshot flag to the sprite. */
+ virtual void setSkipScreenshot(bool skip) = 0;
};
/*
@@ -111,7 +115,7 @@ public:
*/
class SpriteController {
public:
- using ParentSurfaceProvider = std::function<sp<SurfaceControl>(int /*displayId*/)>;
+ using ParentSurfaceProvider = std::function<sp<SurfaceControl>(ui::LogicalDisplayId)>;
SpriteController(const sp<Looper>& looper, int32_t overlayLayer, ParentSurfaceProvider parent);
SpriteController(const SpriteController&) = delete;
SpriteController& operator=(const SpriteController&) = delete;
@@ -151,6 +155,8 @@ private:
DIRTY_HOTSPOT = 1 << 6,
DIRTY_DISPLAY_ID = 1 << 7,
DIRTY_ICON_STYLE = 1 << 8,
+ DIRTY_DRAW_DROP_SHADOW = 1 << 9,
+ DIRTY_SKIP_SCREENSHOT = 1 << 10,
};
/* Describes the state of a sprite.
@@ -159,28 +165,23 @@ private:
* on the sprites for a long time.
* Note that the SpriteIcon holds a reference to a shared (and immutable) bitmap. */
struct SpriteState {
- inline SpriteState() :
- dirty(0), visible(false),
- positionX(0), positionY(0), layer(0), alpha(1.0f), displayId(ADISPLAY_ID_DEFAULT),
- surfaceWidth(0), surfaceHeight(0), surfaceDrawn(false), surfaceVisible(false) {
- }
-
- uint32_t dirty;
+ uint32_t dirty{0};
SpriteIcon icon;
- bool visible;
- float positionX;
- float positionY;
- int32_t layer;
- float alpha;
+ bool visible{false};
+ float positionX{0};
+ float positionY{0};
+ int32_t layer{0};
+ float alpha{1.0f};
SpriteTransformationMatrix transformationMatrix;
- int32_t displayId;
+ ui::LogicalDisplayId displayId{ui::LogicalDisplayId::DEFAULT};
sp<SurfaceControl> surfaceControl;
- int32_t surfaceWidth;
- int32_t surfaceHeight;
- bool surfaceDrawn;
- bool surfaceVisible;
+ int32_t surfaceWidth{0};
+ int32_t surfaceHeight{0};
+ bool surfaceDrawn{false};
+ bool surfaceVisible{false};
+ bool skipScreenshot{false};
inline bool wantSurfaceVisible() const {
return visible && alpha > 0.0f && icon.isValid();
@@ -207,7 +208,8 @@ private:
virtual void setLayer(int32_t layer);
virtual void setAlpha(float alpha);
virtual void setTransformationMatrix(const SpriteTransformationMatrix& matrix);
- virtual void setDisplayId(int32_t displayId);
+ virtual void setDisplayId(ui::LogicalDisplayId displayId);
+ virtual void setSkipScreenshot(bool skip);
inline const SpriteState& getStateLocked() const {
return mLocked.state;
@@ -271,7 +273,8 @@ private:
void doDisposeSurfaces();
void ensureSurfaceComposerClient();
- sp<SurfaceControl> obtainSurface(int32_t width, int32_t height, int32_t displayId);
+ sp<SurfaceControl> obtainSurface(int32_t width, int32_t height, ui::LogicalDisplayId displayId,
+ bool hideOnMirrored);
};
} // namespace android
diff --git a/libs/input/SpriteIcon.h b/libs/input/SpriteIcon.h
index 0939af46c258..7d45d02b4a6f 100644
--- a/libs/input/SpriteIcon.h
+++ b/libs/input/SpriteIcon.h
@@ -40,7 +40,7 @@ struct SpriteIcon {
PointerIconStyle style{PointerIconStyle::TYPE_NULL};
float hotSpotX{};
float hotSpotY{};
- bool drawNativeDropShadow{false};
+ bool drawNativeDropShadow{};
inline bool isValid() const { return bitmap.isValid() && !bitmap.isEmpty(); }
diff --git a/libs/input/TouchSpotController.cpp b/libs/input/TouchSpotController.cpp
index 99952aa14904..7462481f8779 100644
--- a/libs/input/TouchSpotController.cpp
+++ b/libs/input/TouchSpotController.cpp
@@ -40,12 +40,13 @@ namespace android {
// --- Spot ---
void TouchSpotController::Spot::updateSprite(const SpriteIcon* icon, float newX, float newY,
- int32_t displayId) {
+ ui::LogicalDisplayId displayId, bool skipScreenshot) {
sprite->setLayer(Sprite::BASE_LAYER_SPOT + id);
sprite->setAlpha(alpha);
sprite->setTransformationMatrix(SpriteTransformationMatrix(scale, 0.0f, 0.0f, scale));
sprite->setPosition(newX, newY);
sprite->setDisplayId(displayId);
+ sprite->setSkipScreenshot(skipScreenshot);
x = newX;
y = newY;
@@ -68,7 +69,8 @@ void TouchSpotController::Spot::dump(std::string& out, const char* prefix) const
// --- TouchSpotController ---
-TouchSpotController::TouchSpotController(int32_t displayId, PointerControllerContext& context)
+TouchSpotController::TouchSpotController(ui::LogicalDisplayId displayId,
+ PointerControllerContext& context)
: mDisplayId(displayId), mContext(context) {
mContext.getPolicy()->loadPointerResources(&mResources, mDisplayId);
}
@@ -84,7 +86,7 @@ TouchSpotController::~TouchSpotController() {
}
void TouchSpotController::setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
- BitSet32 spotIdBits) {
+ BitSet32 spotIdBits, bool skipScreenshot) {
#if DEBUG_SPOT_UPDATES
ALOGD("setSpots: idBits=%08x", spotIdBits.value);
for (BitSet32 idBits(spotIdBits); !idBits.isEmpty();) {
@@ -93,7 +95,7 @@ void TouchSpotController::setSpots(const PointerCoords* spotCoords, const uint32
const PointerCoords& c = spotCoords[spotIdToIndex[id]];
ALOGD(" spot %d: position=(%0.3f, %0.3f), pressure=%0.3f, displayId=%" PRId32 ".", id,
c.getAxisValue(AMOTION_EVENT_AXIS_X), c.getAxisValue(AMOTION_EVENT_AXIS_Y),
- c.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE), mDisplayId);
+ c.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE), mDisplayId.id);
}
#endif
@@ -116,7 +118,7 @@ void TouchSpotController::setSpots(const PointerCoords* spotCoords, const uint32
spot = createAndAddSpotLocked(id, mLocked.displaySpots);
}
- spot->updateSprite(&icon, x, y, mDisplayId);
+ spot->updateSprite(&icon, x, y, mDisplayId, skipScreenshot);
}
for (Spot* spot : mLocked.displaySpots) {
@@ -273,7 +275,7 @@ void TouchSpotController::dump(std::string& out, const char* prefix) const {
out += prefix;
out += "SpotController:\n";
out += prefix;
- StringAppendF(&out, INDENT "DisplayId: %" PRId32 "\n", mDisplayId);
+ StringAppendF(&out, INDENT "DisplayId: %s\n", mDisplayId.toString().c_str());
std::scoped_lock lock(mLock);
out += prefix;
StringAppendF(&out, INDENT "Animating: %s\n", toString(mLocked.animating));
diff --git a/libs/input/TouchSpotController.h b/libs/input/TouchSpotController.h
index 5bbc75d9570b..ac37fa430249 100644
--- a/libs/input/TouchSpotController.h
+++ b/libs/input/TouchSpotController.h
@@ -29,10 +29,10 @@ namespace android {
*/
class TouchSpotController {
public:
- TouchSpotController(int32_t displayId, PointerControllerContext& context);
+ TouchSpotController(ui::LogicalDisplayId displayId, PointerControllerContext& context);
~TouchSpotController();
void setSpots(const PointerCoords* spotCoords, const uint32_t* spotIdToIndex,
- BitSet32 spotIdBits);
+ BitSet32 spotIdBits, bool skipScreenshot);
void clearSpots();
void reloadSpotResources();
@@ -59,14 +59,15 @@ private:
y(0.0f),
mLastIcon(nullptr) {}
- void updateSprite(const SpriteIcon* icon, float x, float y, int32_t displayId);
+ void updateSprite(const SpriteIcon* icon, float x, float y, ui::LogicalDisplayId displayId,
+ bool skipScreenshot);
void dump(std::string& out, const char* prefix = "") const;
private:
const SpriteIcon* mLastIcon;
};
- int32_t mDisplayId;
+ ui::LogicalDisplayId mDisplayId;
mutable std::mutex mLock;
diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp
index a1bb5b3f1cc4..5b00fca4d857 100644
--- a/libs/input/tests/PointerController_test.cpp
+++ b/libs/input/tests/PointerController_test.cpp
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-#include <com_android_input_flags.h>
#include <flag_macros.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
@@ -30,8 +29,6 @@
namespace android {
-namespace input_flags = com::android::input::flags;
-
enum TestCursorType {
CURSOR_TYPE_DEFAULT = 0,
CURSOR_TYPE_HOVER,
@@ -55,20 +52,19 @@ std::pair<float, float> getHotSpotCoordinatesForType(int32_t type) {
class MockPointerControllerPolicyInterface : public PointerControllerPolicyInterface {
public:
- virtual void loadPointerIcon(SpriteIcon* icon, int32_t displayId) override;
- virtual void loadPointerResources(PointerResources* outResources, int32_t displayId) override;
+ virtual void loadPointerIcon(SpriteIcon* icon, ui::LogicalDisplayId displayId) override;
+ virtual void loadPointerResources(PointerResources* outResources,
+ ui::LogicalDisplayId displayId) override;
virtual void loadAdditionalMouseResources(
std::map<PointerIconStyle, SpriteIcon>* outResources,
std::map<PointerIconStyle, PointerAnimation>* outAnimationResources,
- int32_t displayId) override;
+ ui::LogicalDisplayId displayId) override;
virtual PointerIconStyle getDefaultPointerIconId() override;
virtual PointerIconStyle getDefaultStylusIconId() override;
virtual PointerIconStyle getCustomPointerIconId() override;
- virtual void onPointerDisplayIdChanged(int32_t displayId, const FloatPoint& position) override;
bool allResourcesAreLoaded();
bool noResourcesAreLoaded();
- std::optional<int32_t> getLastReportedPointerDisplayId() { return latestPointerDisplayId; }
private:
void loadPointerIconForType(SpriteIcon* icon, int32_t cursorType);
@@ -76,16 +72,15 @@ private:
bool pointerIconLoaded{false};
bool pointerResourcesLoaded{false};
bool additionalMouseResourcesLoaded{false};
- std::optional<int32_t /*displayId*/> latestPointerDisplayId;
};
-void MockPointerControllerPolicyInterface::loadPointerIcon(SpriteIcon* icon, int32_t) {
+void MockPointerControllerPolicyInterface::loadPointerIcon(SpriteIcon* icon, ui::LogicalDisplayId) {
loadPointerIconForType(icon, CURSOR_TYPE_DEFAULT);
pointerIconLoaded = true;
}
void MockPointerControllerPolicyInterface::loadPointerResources(PointerResources* outResources,
- int32_t) {
+ ui::LogicalDisplayId) {
loadPointerIconForType(&outResources->spotHover, CURSOR_TYPE_HOVER);
loadPointerIconForType(&outResources->spotTouch, CURSOR_TYPE_TOUCH);
loadPointerIconForType(&outResources->spotAnchor, CURSOR_TYPE_ANCHOR);
@@ -94,7 +89,7 @@ void MockPointerControllerPolicyInterface::loadPointerResources(PointerResources
void MockPointerControllerPolicyInterface::loadAdditionalMouseResources(
std::map<PointerIconStyle, SpriteIcon>* outResources,
- std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, int32_t) {
+ std::map<PointerIconStyle, PointerAnimation>* outAnimationResources, ui::LogicalDisplayId) {
SpriteIcon icon;
PointerAnimation anim;
@@ -146,12 +141,6 @@ void MockPointerControllerPolicyInterface::loadPointerIconForType(SpriteIcon* ic
icon->hotSpotY = hotSpot.second;
}
-void MockPointerControllerPolicyInterface::onPointerDisplayIdChanged(int32_t displayId,
- const FloatPoint& /*position*/
-) {
- latestPointerDisplayId = displayId;
-}
-
class TestPointerController : public PointerController {
public:
TestPointerController(sp<android::gui::WindowInfosListener>& registeredListener,
@@ -159,7 +148,6 @@ public:
SpriteController& spriteController)
: PointerController(
policy, looper, spriteController,
- /*enabled=*/true,
[&registeredListener](const sp<android::gui::WindowInfosListener>& listener)
-> std::vector<gui::DisplayInfo> {
// Register listener
@@ -174,33 +162,37 @@ public:
};
class PointerControllerTest : public Test {
+private:
+ void loopThread();
+
+ std::atomic<bool> mRunning = true;
+ class MyLooper : public Looper {
+ public:
+ MyLooper() : Looper(false) {}
+ ~MyLooper() = default;
+ };
+
protected:
PointerControllerTest();
~PointerControllerTest();
- void ensureDisplayViewportIsSet(int32_t displayId = ADISPLAY_ID_DEFAULT);
+ void ensureDisplayViewportIsSet(ui::LogicalDisplayId displayId = ui::LogicalDisplayId::DEFAULT);
sp<MockSprite> mPointerSprite;
sp<MockPointerControllerPolicyInterface> mPolicy;
std::unique_ptr<MockSpriteController> mSpriteController;
std::shared_ptr<PointerController> mPointerController;
sp<android::gui::WindowInfosListener> mRegisteredListener;
+ sp<MyLooper> mLooper;
private:
- void loopThread();
-
- std::atomic<bool> mRunning = true;
- class MyLooper : public Looper {
- public:
- MyLooper() : Looper(false) {}
- ~MyLooper() = default;
- };
- sp<MyLooper> mLooper;
std::thread mThread;
};
-PointerControllerTest::PointerControllerTest() : mPointerSprite(new NiceMock<MockSprite>),
- mLooper(new MyLooper), mThread(&PointerControllerTest::loopThread, this) {
+PointerControllerTest::PointerControllerTest()
+ : mPointerSprite(new NiceMock<MockSprite>),
+ mLooper(new MyLooper),
+ mThread(&PointerControllerTest::loopThread, this) {
mSpriteController.reset(new NiceMock<MockSpriteController>(mLooper));
mPolicy = new MockPointerControllerPolicyInterface();
@@ -217,7 +209,7 @@ PointerControllerTest::~PointerControllerTest() {
mThread.join();
}
-void PointerControllerTest::ensureDisplayViewportIsSet(int32_t displayId) {
+void PointerControllerTest::ensureDisplayViewportIsSet(ui::LogicalDisplayId displayId) {
DisplayViewport viewport;
viewport.displayId = displayId;
viewport.logicalRight = 1600;
@@ -267,8 +259,7 @@ TEST_F(PointerControllerTest, useStylusTypeForStylusHover) {
mPointerController->reloadPointerResources();
}
-TEST_F_WITH_FLAGS(PointerControllerTest, setPresentationBeforeDisplayViewportDoesNotLoadResources,
- REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(input_flags, enable_pointer_choreographer))) {
+TEST_F(PointerControllerTest, setPresentationBeforeDisplayViewportDoesNotLoadResources) {
// Setting the presentation mode before a display viewport is set will not load any resources.
mPointerController->setPresentation(PointerController::Presentation::POINTER);
ASSERT_TRUE(mPolicy->noResourcesAreLoaded());
@@ -278,26 +269,7 @@ TEST_F_WITH_FLAGS(PointerControllerTest, setPresentationBeforeDisplayViewportDoe
ASSERT_TRUE(mPolicy->allResourcesAreLoaded());
}
-TEST_F_WITH_FLAGS(PointerControllerTest, updatePointerIcon,
- REQUIRES_FLAGS_DISABLED(ACONFIG_FLAG(input_flags,
- enable_pointer_choreographer))) {
- ensureDisplayViewportIsSet();
- mPointerController->setPresentation(PointerController::Presentation::POINTER);
- mPointerController->unfade(PointerController::Transition::IMMEDIATE);
-
- int32_t type = CURSOR_TYPE_ADDITIONAL;
- std::pair<float, float> hotspot = getHotSpotCoordinatesForType(type);
- EXPECT_CALL(*mPointerSprite, setVisible(true));
- EXPECT_CALL(*mPointerSprite, setAlpha(1.0f));
- EXPECT_CALL(*mPointerSprite,
- setIcon(AllOf(Field(&SpriteIcon::style, static_cast<PointerIconStyle>(type)),
- Field(&SpriteIcon::hotSpotX, hotspot.first),
- Field(&SpriteIcon::hotSpotY, hotspot.second))));
- mPointerController->updatePointerIcon(static_cast<PointerIconStyle>(type));
-}
-
-TEST_F_WITH_FLAGS(PointerControllerTest, updatePointerIconWithChoreographer,
- REQUIRES_FLAGS_ENABLED(ACONFIG_FLAG(input_flags, enable_pointer_choreographer))) {
+TEST_F(PointerControllerTest, updatePointerIconWithChoreographer) {
// When PointerChoreographer is enabled, the presentation mode is set before the viewport.
mPointerController->setPresentation(PointerController::Presentation::POINTER);
ensureDisplayViewportIsSet();
@@ -348,30 +320,85 @@ TEST_F(PointerControllerTest, doesNotGetResourcesBeforeSettingViewport) {
ensureDisplayViewportIsSet();
}
-TEST_F(PointerControllerTest, notifiesPolicyWhenPointerDisplayChanges) {
- EXPECT_FALSE(mPolicy->getLastReportedPointerDisplayId())
- << "A pointer display change does not occur when PointerController is created.";
+TEST_F(PointerControllerTest, updatesSkipScreenshotFlagForTouchSpots) {
+ ensureDisplayViewportIsSet();
- ensureDisplayViewportIsSet(ADISPLAY_ID_DEFAULT);
+ PointerCoords testSpotCoords;
+ testSpotCoords.clear();
+ testSpotCoords.setAxisValue(AMOTION_EVENT_AXIS_X, 1);
+ testSpotCoords.setAxisValue(AMOTION_EVENT_AXIS_Y, 1);
+ BitSet32 testIdBits;
+ testIdBits.markBit(0);
+ std::array<uint32_t, MAX_POINTER_ID + 1> testIdToIndex;
+
+ sp<MockSprite> testSpotSprite(new NiceMock<MockSprite>);
+
+ // By default sprite is not marked secure
+ EXPECT_CALL(*mSpriteController, createSprite).WillOnce(Return(testSpotSprite));
+ EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(false));
+
+ // Update spots to sync state with sprite
+ mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits,
+ ui::LogicalDisplayId::DEFAULT);
+ testing::Mock::VerifyAndClearExpectations(testSpotSprite.get());
+
+ // Marking the display to skip screenshot should update sprite as well
+ mPointerController->setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId::DEFAULT);
+ EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(true));
+
+ // Update spots to sync state with sprite
+ mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits,
+ ui::LogicalDisplayId::DEFAULT);
+ testing::Mock::VerifyAndClearExpectations(testSpotSprite.get());
+
+ // Reset flag and verify again
+ mPointerController->clearSkipScreenshotFlags();
+ EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(false));
+ mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits,
+ ui::LogicalDisplayId::DEFAULT);
+ testing::Mock::VerifyAndClearExpectations(testSpotSprite.get());
+}
+
+class PointerControllerSkipScreenshotFlagTest
+ : public PointerControllerTest,
+ public testing::WithParamInterface<PointerControllerInterface::ControllerType> {};
+
+TEST_P(PointerControllerSkipScreenshotFlagTest, updatesSkipScreenshotFlag) {
+ sp<MockSprite> testPointerSprite(new NiceMock<MockSprite>);
+ EXPECT_CALL(*mSpriteController, createSprite).WillOnce(Return(testPointerSprite));
- const auto lastReportedPointerDisplayId = mPolicy->getLastReportedPointerDisplayId();
- ASSERT_TRUE(lastReportedPointerDisplayId)
- << "The policy is notified of a pointer display change when the viewport is first set.";
- EXPECT_EQ(ADISPLAY_ID_DEFAULT, *lastReportedPointerDisplayId)
- << "Incorrect pointer display notified.";
+ // Create a pointer controller
+ mPointerController =
+ PointerController::create(mPolicy, mLooper, *mSpriteController, GetParam());
+ ensureDisplayViewportIsSet(ui::LogicalDisplayId::DEFAULT);
- ensureDisplayViewportIsSet(42);
+ // By default skip screenshot flag is not set for the sprite
+ EXPECT_CALL(*testPointerSprite, setSkipScreenshot).With(testing::Args<0>(false));
- EXPECT_EQ(42, *mPolicy->getLastReportedPointerDisplayId())
- << "The policy is notified when the pointer display changes.";
+ // Update pointer to sync state with sprite
+ mPointerController->setPosition(100, 100);
+ testing::Mock::VerifyAndClearExpectations(testPointerSprite.get());
- // Release the PointerController.
- mPointerController = nullptr;
+ // Marking the controller to skip screenshot should update pointer sprite
+ mPointerController->setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId::DEFAULT);
+ EXPECT_CALL(*testPointerSprite, setSkipScreenshot).With(testing::Args<0>(true));
- EXPECT_EQ(ADISPLAY_ID_NONE, *mPolicy->getLastReportedPointerDisplayId())
- << "The pointer display changes to invalid when PointerController is destroyed.";
+ // Update pointer to sync state with sprite
+ mPointerController->move(10, 10);
+ testing::Mock::VerifyAndClearExpectations(testPointerSprite.get());
+
+ // Reset flag and verify again
+ mPointerController->clearSkipScreenshotFlags();
+ EXPECT_CALL(*testPointerSprite, setSkipScreenshot).With(testing::Args<0>(false));
+ mPointerController->move(10, 10);
+ testing::Mock::VerifyAndClearExpectations(testPointerSprite.get());
}
+INSTANTIATE_TEST_SUITE_P(PointerControllerSkipScreenshotFlagTest,
+ PointerControllerSkipScreenshotFlagTest,
+ testing::Values(PointerControllerInterface::ControllerType::MOUSE,
+ PointerControllerInterface::ControllerType::STYLUS));
+
class PointerControllerWindowInfoListenerTest : public Test {};
TEST_F(PointerControllerWindowInfoListenerTest,
diff --git a/libs/input/tests/mocks/MockSprite.h b/libs/input/tests/mocks/MockSprite.h
index 013b79c3a3bf..21628fb9f72c 100644
--- a/libs/input/tests/mocks/MockSprite.h
+++ b/libs/input/tests/mocks/MockSprite.h
@@ -33,7 +33,8 @@ public:
MOCK_METHOD(void, setLayer, (int32_t), (override));
MOCK_METHOD(void, setAlpha, (float), (override));
MOCK_METHOD(void, setTransformationMatrix, (const SpriteTransformationMatrix&), (override));
- MOCK_METHOD(void, setDisplayId, (int32_t), (override));
+ MOCK_METHOD(void, setDisplayId, (ui::LogicalDisplayId), (override));
+ MOCK_METHOD(void, setSkipScreenshot, (bool), (override));
};
} // namespace android
diff --git a/libs/input/tests/mocks/MockSpriteController.h b/libs/input/tests/mocks/MockSpriteController.h
index 62f1d65e77a5..9ef6b7c3b480 100644
--- a/libs/input/tests/mocks/MockSpriteController.h
+++ b/libs/input/tests/mocks/MockSpriteController.h
@@ -27,7 +27,7 @@ class MockSpriteController : public SpriteController {
public:
MockSpriteController(sp<Looper> looper)
- : SpriteController(looper, 0, [](int) { return nullptr; }) {}
+ : SpriteController(looper, 0, [](ui::LogicalDisplayId) { return nullptr; }) {}
~MockSpriteController() {}
MOCK_METHOD(sp<Sprite>, createSprite, (), (override));