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.java89
-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.java1371
-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.java530
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java167
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java274
-rw-r--r--libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java118
-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.java785
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java10
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java427
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java154
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java40
-rw-r--r--libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java13
-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.java14
-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.bp24
-rw-r--r--libs/WindowManager/Shell/AndroidManifest.xml8
-rw-r--r--libs/WindowManager/Shell/OWNERS2
-rw-r--r--libs/WindowManager/Shell/aconfig/Android.bp1
-rw-r--r--libs/WindowManager/Shell/aconfig/multitasking.aconfig15
-rw-r--r--libs/WindowManager/Shell/multivalentTests/Android.bp2
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt194
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt153
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt279
-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.xml2
-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_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)10
-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/layout/bubble_bar_drop_target.xml22
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml9
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml5
-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.xml110
-rw-r--r--libs/WindowManager/Shell/res/layout/maximize_menu_button.xml10
-rw-r--r--libs/WindowManager/Shell/res/values-af/strings.xml2
-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.xml2
-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.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-da/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-de/strings.xml2
-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.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-fr/strings.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-gl/strings.xml2
-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.xml2
-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.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ky/strings.xml2
-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.xml2
-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.xml2
-rw-r--r--libs/WindowManager/Shell/res/values-ne/strings.xml2
-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.xml2
-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.xml2
-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.xml2
-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.xml3
-rw-r--r--libs/WindowManager/Shell/res/values/dimen.xml57
-rw-r--r--libs/WindowManager/Shell/res/values/strings.xml4
-rw-r--r--libs/WindowManager/Shell/res/values/styles.xml8
-rw-r--r--libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java (renamed from libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java)81
-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.java8
-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.java6
-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/ActivityEmbeddingController.java7
-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/BackAnimationController.java333
-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.kt529
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt256
-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.kt81
-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/BubbleController.java226
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java58
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java20
-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.java119
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java224
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl10
-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.java27
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt57
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java66
-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/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/DisplayInsetsController.java2
-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/SyncTransactionQueue.java6
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java11
-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.kt196
-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/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/magnetictarget/MagnetizedObject.kt2
-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/PipPerfHintController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipUtils.kt24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitDecorManager.java10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java34
-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.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java2
-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.java24
-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.java50
-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.java90
-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.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java70
-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.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt353
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt29
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt112
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt100
-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.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt647
-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.kt128
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java4
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl7
-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/draganddrop/DragAndDropController.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java7
-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.java21
-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.java51
-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.java2
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS1
-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.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java18
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/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/pip2/OWNERS1
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java252
-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.java777
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java571
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java88
-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.java1123
-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.java302
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java283
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java3
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java76
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java157
-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.java50
-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.java14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java107
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java12
-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/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.java80
-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.java21
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java9
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java5
-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.java5
-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/Transitions.java165
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java54
-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.java87
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java24
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java350
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java200
-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.java45
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java394
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java522
-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.java53
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt34
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt194
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt39
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java283
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java11
-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.java208
-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/DesktopModeFocusedWindowDecorationViewHolder.kt2
-rw-r--r--libs/WindowManager/Shell/tests/OWNERS3
-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.kt16
-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/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/SwitchBetweenSplitPairsNoPip.kt2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt6
-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/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.java21
-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.java16
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java89
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java25
-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.java70
-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/magnetictarget/MagnetizedObjectTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java20
-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.java9
-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.java15
-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/desktopmode/DesktopModeLoggerTransitionObserverTest.kt368
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt252
-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.kt1076
-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.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java9
-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.java110
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java207
-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/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/windowdecor/DesktopModeWindowDecorViewModelTests.kt142
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java69
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt5
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java348
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt116
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt216
-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.java168
-rw-r--r--libs/androidfw/ZipFileRO.cpp31
-rw-r--r--libs/androidfw/fuzz/resourcefile_fuzzer/Android.bp14
-rw-r--r--libs/androidfw/fuzz/resxmlparser_fuzzer/Android.bp51
-rw-r--r--libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp80
-rw-r--r--libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/attributes.xml10
-rw-r--r--libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/basic.xml5
-rw-r--r--libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/cdata.xml6
-rw-r--r--libs/androidfw/fuzz/resxmlparser_fuzzer/xmlparser_fuzzer.dict11
-rw-r--r--libs/androidfw/include/androidfw/ZipFileRO.h6
-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.cpp16
-rw-r--r--libs/hwui/Properties.h2
-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.aconfig14
-rw-r--r--libs/hwui/apex/LayoutlibLoader.cpp4
-rw-r--r--libs/hwui/apex/jni_runtime.cpp4
-rw-r--r--libs/hwui/effects/GainmapRenderer.cpp22
-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/Graphics.cpp13
-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/ShaderCache.cpp4
-rw-r--r--libs/hwui/pipeline/skia/ShaderCache.h2
-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/MouseCursorController.cpp6
-rw-r--r--libs/input/MouseCursorController.h2
-rw-r--r--libs/input/PointerController.cpp141
-rw-r--r--libs/input/PointerController.h37
-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.cpp111
-rw-r--r--libs/input/tests/mocks/MockSprite.h3
-rw-r--r--libs/input/tests/mocks/MockSpriteController.h2
598 files changed, 25169 insertions, 6416 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..16c77d0c3c81 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -16,15 +16,19 @@
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.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 +42,38 @@ 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 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, "Initializing Window Extensions, vendor API level=" + mVersion
+ + ", activity embedding enabled=" + mIsActivityEmbeddingEnabled);
}
// TODO(b/241126279) Introduce constants to better version functionality
@Override
public int getVendorApiLevel() {
- return 5;
+ return mVersion;
}
@NonNull
@@ -74,8 +91,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 +108,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 +119,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 +161,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..29936cc2cac3
--- /dev/null
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java
@@ -0,0 +1,1371 @@
+/*
+ * 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.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.pm.ActivityInfo;
+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.
+
+ // 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, topSplitContainer.getPrimaryContainer());
+ return;
+ }
+
+ // Update the decor surface owner if needed.
+ boolean isDraggableExpandType =
+ SplitAttributesHelper.isDraggableExpandType(splitAttributes);
+ final TaskFragmentContainer decorSurfaceOwnerContainer = isDraggableExpandType
+ ? topSplitContainer.getSecondaryContainer()
+ : topSplitContainer.getPrimaryContainer();
+
+ if (!Objects.equals(
+ mDecorSurfaceOwner, decorSurfaceOwnerContainer.getTaskFragmentToken())) {
+ createOrMoveDecorSurfaceLocked(wct, decorSurfaceOwnerContainer);
+ }
+ final boolean isVerticalSplit = isVerticalSplit(topSplitContainer);
+ final boolean isReversedLayout = isReversedLayout(
+ topSplitContainer.getCurrentSplitAttributes(),
+ parentInfo.getConfiguration());
+
+ updateProperties(
+ new Properties(
+ parentInfo.getConfiguration(),
+ dividerAttributes,
+ decorSurface,
+ getInitialDividerPosition(
+ topSplitContainer, isVerticalSplit, isReversedLayout),
+ isVerticalSplit,
+ isReversedLayout,
+ parentInfo.getDisplayId(),
+ isDraggableExpandType,
+ getContainerBackgroundColor(topSplitContainer.getPrimaryContainer(),
+ DEFAULT_PRIMARY_VEIL_COLOR),
+ getContainerBackgroundColor(topSplitContainer.getSecondaryContainer(),
+ DEFAULT_SECONDARY_VEIL_COLOR)
+ ));
+ }
+ }
+
+ @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 SplitContainer splitContainer,
+ boolean isVerticalSplit,
+ boolean isReversedLayout) {
+ final Rect primaryBounds =
+ splitContainer.getPrimaryContainer().getLastRequestedBounds();
+ final Rect secondaryBounds =
+ splitContainer.getSecondaryContainer().getLastRequestedBounds();
+ final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes();
+
+ if (SplitAttributesHelper.isDraggableExpandType(splitAttributes)) {
+ // If the container is fully expanded by dragging the divider, we display the divider
+ // on the edge.
+ final int dividerWidth = getDividerWidthPx(splitAttributes.getDividerAttributes());
+ final int fullyExpandedPosition = isVerticalSplit
+ ? primaryBounds.right - dividerWidth
+ : primaryBounds.bottom - dividerWidth;
+ return isReversedLayout ? fullyExpandedPosition : 0;
+ } else {
+ return isVerticalSplit
+ ? Math.min(primaryBounds.right, secondaryBounds.right)
+ : Math.min(primaryBounds.bottom, secondaryBounds.bottom);
+ }
+ }
+
+ private static boolean isVerticalSplit(@NonNull SplitContainer splitContainer) {
+ final int layoutDirection = splitContainer.getCurrentSplitAttributes().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, mRenderer.mDividerWidthPx,
+ mProperties.mDividerAttributes, mProperties.mIsVerticalSplit,
+ calculateMinPosition(), calculateMaxPosition());
+ mRenderer.setDividerPosition(mDividerPosition);
+ switch (event.getAction()) {
+ 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.right - mRenderer.mDividerWidthPx
+ : taskBounds.bottom - mRenderer.mDividerWidthPx;
+
+ if (isDraggingToFullscreenAllowed(mProperties.mDividerAttributes)) {
+ final float displayDensity = getDisplayDensity();
+ return dividerPositionWithDraggingToFullscreenAllowed(
+ dividerPosition,
+ minPosition,
+ maxPosition,
+ fullyExpandedPosition,
+ velocity,
+ displayDensity);
+ }
+ return Math.clamp(dividerPosition, minPosition, maxPosition);
+ }
+
+ /**
+ * Returns the divider position given a set of position options. A snap algorithm is used to
+ * adjust the ending position to either fully expand one container or move the divider back to
+ * the specified min/max ratio depending on the dragging velocity.
+ */
+ @VisibleForTesting
+ static int dividerPositionWithDraggingToFullscreenAllowed(int dividerPosition, int minPosition,
+ int maxPosition, int fullyExpandedPosition, float velocity, float displayDensity) {
+ final float minDismissVelocityPxPerSecond =
+ MIN_DISMISS_VELOCITY_DP_PER_SECOND * displayDensity;
+ final float minFlingVelocityPxPerSecond =
+ MIN_FLING_VELOCITY_DP_PER_SECOND * displayDensity;
+ if (dividerPosition < minPosition && velocity < -minDismissVelocityPxPerSecond) {
+ return 0;
+ }
+ if (dividerPosition > maxPosition && velocity > minDismissVelocityPxPerSecond) {
+ return fullyExpandedPosition;
+ }
+ if (Math.abs(velocity) < minFlingVelocityPxPerSecond) {
+ if (dividerPosition >= minPosition && dividerPosition <= maxPosition) {
+ return dividerPosition;
+ }
+ int[] possiblePositions = {0, minPosition, maxPosition, fullyExpandedPosition};
+ return snap(dividerPosition, possiblePositions);
+ }
+ if (velocity < 0) {
+ return 0;
+ } else {
+ return fullyExpandedPosition;
+ }
+ }
+
+ /** Calculates the snapped divider position based on the possible positions and distance. */
+ 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(),
+ mRenderer.mDividerWidthPx, mProperties.mDividerAttributes,
+ mProperties.mIsVerticalSplit, mProperties.mIsReversedLayout);
+ }
+
+ @GuardedBy("mLock")
+ private int calculateMaxPosition() {
+ return calculateMaxPosition(
+ mProperties.mConfiguration.windowConfiguration.getBounds(),
+ mRenderer.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(@NonNull SplitContainer topSplitContainer) {
+ synchronized (mLock) {
+ return calculateNewSplitRatio(
+ topSplitContainer,
+ mDividerPosition,
+ mProperties.mConfiguration.windowConfiguration.getBounds(),
+ mRenderer.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.
+ return true;
+ }
+
+ /**
+ * Returns the new split ratio of the {@link SplitContainer} based on the current divider
+ * position.
+ *
+ * @param topSplitContainer the {@link SplitContainer} for which to compute the split ratio.
+ * @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(SplitContainer)}.
+ * @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(
+ @NonNull SplitContainer topSplitContainer,
+ 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 TaskFragmentContainer primaryContainer = topSplitContainer.getPrimaryContainer();
+ final Rect origPrimaryBounds = primaryContainer.getLastRequestedBounds();
+ final int usableSize = isVerticalSplit
+ ? taskBounds.width() - dividerWidthPx
+ : taskBounds.height() - dividerWidthPx;
+
+ final float newRatio;
+ if (isVerticalSplit) {
+ final int newPrimaryWidth = isReversedLayout
+ ? (origPrimaryBounds.right - (dividerPosition + dividerWidthPx))
+ : (dividerPosition - origPrimaryBounds.left);
+ newRatio = 1.0f * newPrimaryWidth / usableSize;
+ } else {
+ final int newPrimaryHeight = isReversedLayout
+ ? (origPrimaryBounds.bottom - (dividerPosition + dividerWidthPx))
+ : (dividerPosition - origPrimaryBounds.top);
+ 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 =
+ ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_WINDOW_CONFIGURATION;
+ @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;
+ private final Color mPrimaryVeilColor;
+ private final Color mSecondaryVeilColor;
+
+ @VisibleForTesting
+ Properties(
+ @NonNull Configuration configuration,
+ @NonNull DividerAttributes dividerAttributes,
+ @NonNull SurfaceControl decorSurface,
+ int initialDividerPosition,
+ boolean isVerticalSplit,
+ boolean isReversedLayout,
+ int displayId,
+ boolean isDraggableExpandType,
+ @NonNull Color primaryVeilColor,
+ @NonNull Color secondaryVeilColor) {
+ mConfiguration = configuration;
+ mDividerAttributes = dividerAttributes;
+ mDecorSurface = decorSurface;
+ mInitialDividerPosition = initialDividerPosition;
+ mIsVerticalSplit = isVerticalSplit;
+ mIsReversedLayout = isReversedLayout;
+ mDisplayId = displayId;
+ mIsDraggableExpandType = isDraggableExpandType;
+ mPrimaryVeilColor = primaryVeilColor;
+ mSecondaryVeilColor = secondaryVeilColor;
+ }
+
+ /**
+ * 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.mPrimaryVeilColor.equals(b.mPrimaryVeilColor)
+ && a.mSecondaryVeilColor.equals(b.mSecondaryVeilColor);
+ }
+
+ 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 mDividerWidthPx;
+ 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() {
+ mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes);
+ 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(mDividerWidthPx, mHandleWidthPx);
+ dividerSurfacePosition =
+ mProperties.mIsReversedLayout
+ ? mDividerPosition
+ : mDividerPosition + mDividerWidthPx - mDividerSurfaceWidthPx;
+ dividerSurfacePosition = Math.clamp(dividerSurfacePosition, 0,
+ mProperties.mIsVerticalSplit ? taskBounds.width() : taskBounds.height());
+ } else {
+ mDividerSurfaceWidthPx = 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
+ if (!mProperties.mIsReversedLayout) {
+ final int offset = mDividerPosition - dividerSurfacePosition;
+ mDividerLine.setX(mProperties.mIsVerticalSplit ? offset : 0);
+ mDividerLine.setY(mProperties.mIsVerticalSplit ? 0 : offset);
+ } else {
+ // For reversed layout, the divider line is always at the start of the divider
+ // surface.
+ mDividerLine.setX(0);
+ mDividerLine.setY(0);
+ }
+
+ 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);
+ 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(mDividerWidthPx, taskBounds.height())
+ : new FrameLayout.LayoutParams(taskBounds.width(), 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) {
+ t.setColor(mPrimaryVeil, colorToFloatArray(mProperties.mPrimaryVeilColor))
+ .setColor(mSecondaryVeil, colorToFloatArray(mProperties.mSecondaryVeilColor))
+ .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 + 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 + mDividerWidthPx,
+ taskBounds.width(), taskBounds.height());
+ primaryBounds = mProperties.mIsReversedLayout ? boundsBottom : boundsTop;
+ secondaryBounds = mProperties.mIsReversedLayout ? boundsTop : boundsBottom;
+ }
+ t.setWindowCrop(mPrimaryVeil, primaryBounds.width(), primaryBounds.height());
+ t.setWindowCrop(mSecondaryVeil, secondaryBounds.width(), secondaryBounds.height());
+ t.setPosition(mPrimaryVeil, primaryBounds.left, primaryBounds.top);
+ t.setPosition(mSecondaryVeil, secondaryBounds.left, secondaryBounds.top);
+ }
+
+ 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 3061d9789255..13c2d1f73461 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;
@@ -56,6 +58,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 +68,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 +89,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 +105,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")
@@ -161,6 +168,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 +189,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 +350,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 +419,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 +432,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) {
@@ -829,7 +854,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
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.
- taskContainer.setIsVisible(false);
+ if (taskContainer.isVisible()) {
+ taskContainer.setInvisible();
+ }
return;
}
@@ -837,9 +864,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
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;
@@ -998,6 +1030,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;
}
@@ -1044,8 +1077,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;
}
@@ -1216,7 +1248,7 @@ 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));
@@ -1386,9 +1418,27 @@ 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);
+ }
+ 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.
@@ -1398,7 +1448,7 @@ 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);
}
// We didn't trigger the callback if there were any pending appeared activities, so check
// again after the pending is removed.
@@ -1479,12 +1529,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;
}
}
@@ -1578,7 +1631,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 */);
}
/**
@@ -1593,7 +1647,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;
@@ -1611,15 +1665,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
}
final TaskFragmentContainer container = newContainer(null /* pendingAppearedActivity */,
intent, activityInTask, taskId, null /* pairedPrimaryContainer*/, overlayTag,
- launchOptions);
+ launchOptions, associateLaunchingActivity);
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(),
@@ -1680,17 +1733,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;
+ for (int i = mTaskContainers.size() - 1; i >= 0; --i) {
+ final TaskFragmentContainer container = mTaskContainers.valueAt(i)
+ .getContainerWithActivity(activityToken);
+ if (container != null) {
+ return container;
+ }
}
-
-
- // Check appeared activity if there is no such pending appeared activity.
- return getContainer(container -> container.hasAppearedActivity(activityToken));
+ return null;
}
@GuardedBy("mLock")
@@ -1703,7 +1753,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@NonNull Activity activityInTask, int taskId) {
return newContainer(pendingAppearedActivity, null /* pendingAppearedIntent */,
activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */,
- null /* launchOptions */);
+ null /* launchOptions */, false /* associateLaunchingActivity */);
}
@GuardedBy("mLock")
@@ -1711,7 +1761,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@NonNull Activity activityInTask, int taskId) {
return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent,
activityInTask, taskId, null /* pairedPrimaryContainer */, null /* tag */,
- null /* launchOptions */);
+ null /* launchOptions */, false /* associateLaunchingActivity */);
}
@GuardedBy("mLock")
@@ -1720,7 +1770,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@NonNull TaskFragmentContainer pairedPrimaryContainer) {
return newContainer(null /* pendingAppearedActivity */, pendingAppearedIntent,
activityInTask, taskId, pairedPrimaryContainer, null /* tag */,
- null /* launchOptions */);
+ null /* launchOptions */, false /* associateLaunchingActivity */);
}
/**
@@ -1732,29 +1782,32 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
* @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
+ * @param pairedContainer 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.
+ * @param associateLaunchingActivity {@code true} to indicate this overlay container
+ * should associate with launching activity.
*/
@GuardedBy("mLock")
TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity,
@Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId,
- @Nullable TaskFragmentContainer pairedPrimaryContainer, @Nullable String overlayTag,
- @Nullable Bundle launchOptions) {
+ @Nullable TaskFragmentContainer pairedContainer, @Nullable String overlayTag,
+ @Nullable Bundle launchOptions, boolean associateLaunchingActivity) {
if (activityInTask == null) {
throw new IllegalArgumentException("activityInTask must not be null,");
}
if (!mTaskContainers.contains(taskId)) {
mTaskContainers.put(taskId, new TaskContainer(taskId, activityInTask));
+ mDividerPresenters.put(taskId, new DividerPresenter(taskId, this, mExecutor));
}
final TaskContainer taskContainer = mTaskContainers.get(taskId);
final TaskFragmentContainer container = new TaskFragmentContainer(pendingAppearedActivity,
- pendingAppearedIntent, taskContainer, this, pairedPrimaryContainer, overlayTag,
- launchOptions);
+ pendingAppearedIntent, taskContainer, this, pairedContainer, overlayTag,
+ launchOptions, associateLaunchingActivity ? activityInTask : null);
return container;
}
@@ -1920,7 +1973,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;
@@ -1943,7 +1996,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@NonNull TaskFragmentContainer container) {
final TaskContainer taskContainer = container.getTaskContainer();
- if (dismissOverlayContainerIfNeeded(wct, taskContainer)) {
+ if (dismissAlwaysOnTopOverlayIfNeeded(wct, taskContainer)) {
return;
}
@@ -1967,22 +2020,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;
}
/**
@@ -2046,19 +2104,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);
}
/**
@@ -2108,6 +2154,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.
@@ -2121,6 +2172,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());
@@ -2143,6 +2198,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) {
@@ -2216,6 +2276,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);
@@ -2413,13 +2476,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;
@@ -2464,6 +2524,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;
}
@@ -2476,8 +2543,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;
}
@@ -2560,25 +2626,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
@@ -2589,8 +2682,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
@NonNull WindowContainerTransaction wct, @NonNull Bundle options,
@NonNull Intent intent, @NonNull Activity launchActivity) {
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.
@@ -2611,35 +2706,91 @@ 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);
}
private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter {
@@ -2728,6 +2879,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
@@ -2735,7 +2903,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 */);
}
}
}
@@ -2884,17 +3057,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
+ @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 static ActivityWindowInfo getActivityWindowInfo(@NonNull Activity activity) {
+ 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
@@ -2965,4 +3221,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..27048136afd8 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,
@@ -367,6 +368,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 +401,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 +430,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 +497,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 +600,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 +616,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,7 +648,6 @@ 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);
}
@@ -612,7 +657,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
*/
@NonNull
static Rect sanitizeBounds(@NonNull Rect bounds, @Nullable Size minDimension,
- @NonNull Rect taskBounds) {
+ @NonNull TaskFragmentContainer container) {
if (bounds.isEmpty()) {
// Don't need to check if the bounds follows the task bounds.
return bounds;
@@ -621,10 +666,33 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
// Expand the bounds if the bounds are smaller than minimum dimensions.
return new Rect();
}
+ final TaskContainer taskContainer = container.getTaskContainer();
+ final Rect taskBounds = taskContainer.getBounds();
if (!taskBounds.contains(bounds)) {
// Expand the bounds if the bounds exceed the task bounds.
return new Rect();
}
+
+ if (!container.isOverlay()) {
+ // Stop here if the container is not an overlay.
+ return bounds;
+ }
+
+ final IBinder associatedActivityToken = container.getAssociatedActivityToken();
+
+ if (associatedActivityToken == null) {
+ // Stop here if the container is an always-on-top overlay.
+ return bounds;
+ }
+
+ // Expand the overlay with activity association if the associated activity is part of a
+ // split, or we may need to handle three change transition together.
+ final TaskFragmentContainer associatedContainer = taskContainer
+ .getContainerWithActivity(associatedActivityToken);
+ if (taskContainer.getActiveSplitForContainer(associatedContainer) != null) {
+ return new Rect();
+ }
+
return bounds;
}
@@ -685,8 +753,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 +763,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 +782,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 +823,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 +1024,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 +1043,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 +1054,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 +1065,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 +1191,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 +1199,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 c4adf16f4536..c708da97d908 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,36 +137,39 @@ class TaskContainer {
}
int getDisplayId() {
- return mDisplayId;
+ return mInfo.getDisplayId();
}
boolean isVisible() {
- return mIsVisible;
+ return mInfo.isVisible();
}
- void setIsVisible(boolean visible) {
- mIsVisible = visible;
+ 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;
}
/**
@@ -159,16 +185,16 @@ class TaskContainer {
// If the task properties equals regardless of starting position, don't
// need to update the container.
- return mConfiguration.diffPublicOnly(configuration) != 0
- || mDisplayId != info.getDisplayId();
+ 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) {
@@ -187,7 +213,7 @@ class TaskContainer {
}
boolean isInPictureInPicture() {
- return isInPictureInPicture(mConfiguration);
+ return isInPictureInPicture(mInfo.getConfiguration());
}
private static boolean isInPictureInPicture(@NonNull Configuration configuration) {
@@ -200,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. */
@@ -208,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);
}
}
@@ -234,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)) {
@@ -279,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()) {
@@ -310,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);
@@ -324,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);
}
@@ -395,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(topSplitContainer);
+
+ // 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() {
@@ -429,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..482554351b97 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -113,6 +113,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,13 +184,18 @@ class TaskFragmentContainer {
private boolean mIsIsolatedNavigationEnabled;
/**
+ * Whether this TaskFragment is pinned.
+ */
+ private boolean mIsPinned;
+
+ /**
* Whether to apply dimming on the parent Task that was requested last.
*/
private boolean mLastDimOnTask;
/**
* @see #TaskFragmentContainer(Activity, Intent, TaskContainer, SplitController,
- * TaskFragmentContainer, String, Bundle)
+ * TaskFragmentContainer, String, Bundle, Activity)
*/
TaskFragmentContainer(@Nullable Activity pendingAppearedActivity,
@Nullable Intent pendingAppearedIntent,
@@ -187,7 +204,7 @@ class TaskFragmentContainer {
@Nullable TaskFragmentContainer pairedPrimaryContainer) {
this(pendingAppearedActivity, pendingAppearedIntent, taskContainer,
controller, pairedPrimaryContainer, null /* overlayTag */,
- null /* launchOptions */);
+ null /* launchOptions */, null /* associatedActivity */);
}
/**
@@ -197,12 +214,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,
@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(
@@ -214,7 +233,13 @@ class TaskFragmentContainer {
mOverlayTag = overlayTag;
if (overlayTag != null) {
Objects.requireNonNull(launchOptions);
+ } else if (associatedActivity != null) {
+ throw new IllegalArgumentException("Associated activity must be null for "
+ + "non-overlay activity.");
}
+ mAssociatedActivityToken = associatedActivity != null
+ ? associatedActivity.getActivityToken() : null;
+
if (launchOptions != null) {
mLaunchOptions.putAll(launchOptions);
}
@@ -420,14 +445,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 +505,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 +797,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 +898,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 +1038,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 +1083,7 @@ class TaskFragmentContainer {
+ " runningActivityCount=" + getRunningActivityCount()
+ " isFinished=" + mIsFinished
+ " overlayTag=" + mOverlayTag
+ + " associatedActivityToken=" + mAssociatedActivityToken
+ " lastRequestedBounds=" + mLastRequestedBounds
+ " pendingAppearedActivities=" + mPendingAppearedActivities
+ (includeContainersToFinishOnExit ? " containersToFinishOnExit="
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..746607c8094c
--- /dev/null
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java
@@ -0,0 +1,785 @@
+/*
+ * 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 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(
+ mSplitContainer, true /* isVerticalSplit */, false /* isReversedLayout */),
+ true /* isVerticalSplit */,
+ false /* isReversedLayout */,
+ Display.DEFAULT_DISPLAY,
+ false /* isDraggableExpandType */,
+ Color.valueOf(Color.BLACK), /* primaryVeilColor */
+ Color.valueOf(Color.GRAY) /* secondaryVeilColor */
+ );
+
+ 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(
+ mSplitContainer,
+ 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(
+ mSplitContainer,
+ 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(
+ mSplitContainer,
+ 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(
+ mSplitContainer,
+ 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(
+ mSplitContainer,
+ 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(
+ mSplitContainer,
+ 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.dividerPositionWithDraggingToFullscreenAllowed(
+ 10 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ -dismissVelocity,
+ displayDensity));
+
+ // Divider position is greater than maxPosition and the velocity is enough to be dismissed
+ assertEquals(
+ 1200, // Fully expanded position
+ DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
+ 1000 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ dismissVelocity,
+ displayDensity));
+
+ // Divider position is returned when the velocity is not fast enough for fling and is in
+ // between minPosition and maxPosition
+ assertEquals(
+ 500, // dividerPosition is not snapped
+ DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
+ 500 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ nonFlingVelocity,
+ displayDensity));
+
+ // Divider position is snapped when the velocity is not fast enough for fling and larger
+ // than maxPosition
+ assertEquals(
+ 900, // Closest position is maxPosition
+ DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
+ 950 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ nonFlingVelocity,
+ displayDensity));
+
+ // Divider position is snapped when the velocity is not fast enough for fling and smaller
+ // than minPosition
+ assertEquals(
+ 30, // Closest position is minPosition
+ DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
+ 20 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ nonFlingVelocity,
+ displayDensity));
+
+ // Divider position is greater than minPosition and the velocity is enough for fling
+ assertEquals(
+ 0, // Closed position
+ DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
+ 50 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ -flingVelocity,
+ displayDensity));
+
+ // Divider position is less than maxPosition and the velocity is enough for fling
+ assertEquals(
+ 1200, // Fully expanded position
+ DividerPresenter.dividerPositionWithDraggingToFullscreenAllowed(
+ 800 /* dividerPosition */,
+ 30 /* minPosition */,
+ 900 /* maxPosition */,
+ 1200 /* fullyExpandedPosition */,
+ flingVelocity,
+ displayDensity));
+ }
+
+ 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/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java
index dd087e8eb7c9..76e6a0ff2c21 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
@@ -42,10 +42,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 +63,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 +78,6 @@ public class JetpackTaskFragmentOrganizerTest {
@Before
public void setup() {
- MockitoAnnotations.initMocks(this);
mOrganizer = new JetpackTaskFragmentOrganizer(Runnable::run, mCallback);
mOrganizer.registerOrganizer();
spyOn(mOrganizer);
@@ -107,7 +111,7 @@ public class JetpackTaskFragmentOrganizerTest {
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 c64c3abb056f..9ebcb759115d 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,15 @@ 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.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 +43,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 +85,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 +104,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 +137,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 +188,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 +281,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 +306,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 +348,20 @@ 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);
}
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 +370,49 @@ 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");
+
+ assertThat(sanitizeBounds(bounds, null, overlayContainer)
+ .isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testSanitizeBounds_visibleSplit_expandOverlay() {
+ // Launch a visible split
+ final Activity primaryActivity = createMockActivity();
+ final Activity secondaryActivity = createMockActivity();
+ final TaskFragmentContainer primaryContainer =
+ createMockTaskFragmentContainer(primaryActivity, true /* isVisible */);
+ final TaskFragmentContainer secondaryContainer =
+ createMockTaskFragmentContainer(secondaryActivity, true /* isVisible */);
+
+ final SplitPairRule splitPairRule = createSplitPairRuleBuilder(
+ activityActivityPair -> true /* activityPairPredicate */,
+ activityIntentPair -> true /* activityIntentPairPredicate */,
+ parentWindowMetrics -> true /* parentWindowMetricsPredicate */)
+ .build();
+ mSplitController.registerSplit(mTransaction, primaryContainer, primaryActivity,
+ secondaryContainer, splitPairRule, splitPairRule.getDefaultSplitAttributes());
- SplitPresenter.sanitizeBounds(bounds, null, TASK_BOUNDS);
+ final Rect bounds = new Rect(0, 0, 100, 100);
+ final TaskFragmentContainer overlayContainer =
+ createTestOverlayContainer(TASK_ID, "test1", true /* isVisible */,
+ true /* associatedLaunchingActivity */, secondaryActivity);
+
+ assertThat(sanitizeBounds(bounds, null, overlayContainer)
+ .isEmpty()).isTrue();
}
@Test
@@ -294,9 +422,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 +432,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 +483,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 +503,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);
@@ -443,11 +574,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,16 +593,15 @@ 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(
@@ -487,7 +616,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 +634,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 +660,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 +706,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 +724,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 +733,187 @@ 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());
+ }
+
/**
- * 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) {
+ 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 = mSplitController.newContainer(activity,
activity.getTaskId());
- setupTaskFragmentInfo(container, activity);
+ setupTaskFragmentInfo(container, activity, isVisible);
return container;
}
@NonNull
private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag) {
- Activity activity = createMockActivity();
+ 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 = mSplitController.newContainer(
null /* pendingAppearedActivity */, mIntent, activity, taskId,
- null /* pairedPrimaryContainer */, tag, Bundle.EMPTY);
- setupTaskFragmentInfo(overlayContainer, activity);
+ null /* pairedPrimaryContainer */, tag, Bundle.EMPTY,
+ associateLaunchingActivity);
+ 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..7d86ec2272af 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
@@ -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 =
@@ -208,13 +230,13 @@ public class SplitControllerTest {
// 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()));
}
@@ -275,7 +297,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
@@ -590,7 +615,7 @@ public class SplitControllerTest {
assertFalse(result);
verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(),
- anyString(), any());
+ anyString(), any(), anyBoolean());
}
@Test
@@ -620,7 +645,7 @@ public class SplitControllerTest {
false /* isOnReparent */);
assertTrue(result);
- verify(mSplitPresenter).expandTaskFragment(mTransaction, container.getTaskFragmentToken());
+ verify(mSplitPresenter).expandTaskFragment(mTransaction, container);
}
@Test
@@ -753,7 +778,7 @@ public class SplitControllerTest {
assertTrue(result);
verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(),
- anyString(), any());
+ anyString(), any(), anyBoolean());
verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any());
}
@@ -796,7 +821,7 @@ public class SplitControllerTest {
assertTrue(result);
verify(mSplitController, never()).newContainer(any(), any(), any(), anyInt(), any(),
- anyString(), any());
+ anyString(), any(), anyBoolean());
verify(mSplitController, never()).registerSplit(any(), any(), any(), any(), any(), any());
}
@@ -1529,6 +1554,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,13 +1669,17 @@ 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;
}
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..3fbce9ec31a5 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;
@@ -85,10 +86,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 +109,10 @@ import java.util.ArrayList;
public class SplitPresenterTest {
private Activity mActivity;
+
+ @Rule
+ public MockitoRule rule = MockitoJUnit.rule();
+
@Mock
private Resources mActivityResources;
@Mock
@@ -119,7 +126,6 @@ public class SplitPresenterTest {
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
doReturn(new WindowLayoutInfo(new ArrayList<>())).when(mWindowLayoutComponent)
.getCurrentWindowLayoutInfo(anyInt(), any());
DeviceStateManagerFoldingFeatureProducer producer =
@@ -280,6 +286,28 @@ public class SplitPresenterTest {
}
@Test
+ public void testSetTaskFragmentPinned() {
+ final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID);
+
+ // 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);
@@ -665,8 +693,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,8 +703,8 @@ 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
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..8913b22115e9 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
@@ -42,11 +42,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;
@@ -60,14 +61,12 @@ import java.util.List;
@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();
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..44ab2c458e39 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);
@@ -402,7 +406,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()));
@@ -534,9 +538,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());
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..89781fd650a4 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -45,7 +45,6 @@ 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",
@@ -166,10 +165,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 {
@@ -193,7 +210,8 @@ android_library {
"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..7a986835359a 100644
--- a/libs/WindowManager/Shell/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/AndroidManifest.xml
@@ -23,4 +23,12 @@
<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" />
+ </application>
</manifest>
diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS
index 0c4fd140780e..ebebd8a52c9a 100644
--- a/libs/WindowManager/Shell/OWNERS
+++ b/libs/WindowManager/Shell/OWNERS
@@ -1,5 +1,5 @@
xutan@google.com
# Give submodule owners in shell resource approval
-per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, nmusgrave@google.com, pbdr@google.com, tkachenkoi@google.com
+per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, nmusgrave@google.com, pbdr@google.com, tkachenkoi@google.com, mpodolian@google.com, liranb@google.com
per-file res*/*/tv_*.xml = bronger@google.com
diff --git a/libs/WindowManager/Shell/aconfig/Android.bp b/libs/WindowManager/Shell/aconfig/Android.bp
index 1a98ffcea9e7..7f8f57b172ff 100644
--- a/libs/WindowManager/Shell/aconfig/Android.bp
+++ b/libs/WindowManager/Shell/aconfig/Android.bp
@@ -1,6 +1,7 @@
aconfig_declarations {
name: "com_android_wm_shell_flags",
package: "com.android.wm.shell",
+ container: "system",
srcs: [
"multitasking.aconfig",
],
diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
index b61dda4c4e53..8977d5cad780 100644
--- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig
+++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig
@@ -1,4 +1,5 @@
package: "com.android.wm.shell"
+container: "system"
flag {
name: "enable_app_pairs"
@@ -63,3 +64,17 @@ 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"
+}
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..8487e3792993 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,58 @@ 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)
+ )
+ val bubbleBarBounds = Rect(1700, 2500, 1780, 2600)
+
+ positioner.setShowingInBubbleBar(true)
+ positioner.update(deviceConfig)
+ positioner.bubbleBarBounds = bubbleBarBounds
+
+ 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)
+ )
+ val bubbleBarBounds = Rect(100, 2500, 280, 2550)
+ positioner.setShowingInBubbleBar(true)
+ positioner.update(deviceConfig)
+ positioner.bubbleBarBounds = bubbleBarBounds
+
+ 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 +296,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 +324,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 +533,106 @@ 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 deviceConfig =
+ defaultDeviceConfig.copy(
+ isLargeScreen = true,
+ isLandscape = true,
+ insets = Insets.of(10, 20, 5, 15),
+ windowBounds = Rect(0, 0, 2000, 2600)
+ )
+ positioner.update(deviceConfig)
+
+ positioner.bubbleBarBounds = getBubbleBarBounds(onLeft, deviceConfig)
+
+ 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.bubbleBarBounds.top - 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
@@ -517,4 +666,21 @@ class BubblePositionerTest {
positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent
}
+
+ private fun getBubbleBarBounds(onLeft: Boolean, deviceConfig: DeviceConfig): Rect {
+ val width = 200
+ val height = 100
+ val bottom = deviceConfig.windowBounds.bottom - deviceConfig.insets.bottom
+ val top = bottom - height
+ val left: Int
+ val right: Int
+ if (onLeft) {
+ left = deviceConfig.insets.left
+ right = left + width
+ } else {
+ right = deviceConfig.windowBounds.right - deviceConfig.insets.right
+ left = right - width
+ }
+ return Rect(left, top, right, bottom)
+ }
}
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..35a4a627c0d1 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,27 +18,32 @@ 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.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.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 org.junit.After
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
@@ -64,6 +69,7 @@ class BubbleStackViewTest {
@Before
fun setUp() {
+ PhysicsAnimatorTestUtils.prepareForTest()
// Disable protolog tool when running the tests from studio
ProtoLog.REQUIRE_PROTOLOGTOOL = false
windowManager = WindowManagerGlobal.getWindowManagerService()!!
@@ -104,34 +110,158 @@ 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()
+ }
- 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()
+ }
+
+ 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 +282,6 @@ class BubbleStackViewTest {
assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
assertThat(bubble.isInflated).isTrue()
- return bubble
}
private class FakeBubbleStackViewManager : BubbleStackViewManager {
@@ -176,7 +305,7 @@ class BubbleStackViewTest {
r.run()
}
- override fun removeCallbacks(r: Runnable) {}
+ override fun removeCallbacks(r: Runnable?) {}
override fun hasCallback(r: Runnable): Boolean = false
}
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..076414132e27
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt
@@ -0,0 +1,279 @@
+/*
+ * 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
+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.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_WIDTH = 100
+ 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 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.bubbleBarBounds =
+ Rect(
+ SCREEN_WIDTH - deviceConfig.insets.right - BUBBLE_BAR_WIDTH,
+ SCREEN_HEIGHT - deviceConfig.insets.bottom - BUBBLE_BAR_HEIGHT,
+ SCREEN_WIDTH - deviceConfig.insets.right,
+ SCREEN_HEIGHT - deviceConfig.insets.bottom
+ )
+
+ controller = BubbleExpandedViewPinController(context, container, positioner)
+ testListener = TestLocationChangeListener()
+ controller.setListener(testListener)
+ }
+
+ @After
+ fun tearDown() {
+ runOnMainSync { controller.onDragEnd() }
+ waitForAnimateOut()
+ }
+
+ @Test
+ fun drag_stayOnSameSide() {
+ 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(BubbleBarLocation.RIGHT)
+ }
+
+ @Test
+ fun drag_toLeft() {
+ // Drag to left, but don't finish
+ runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = false)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ }
+ waitForAnimateIn()
+
+ assertThat(dropTargetView).isNotNull()
+ assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+
+ val expectedDropTargetBounds = getExpectedDropTargetBounds(onLeft = true)
+ assertThat(dropTargetView!!.layoutParams.width).isEqualTo(expectedDropTargetBounds.width())
+ assertThat(dropTargetView!!.layoutParams.height)
+ .isEqualTo(expectedDropTargetBounds.height())
+
+ assertThat(testListener.locationChanges).containsExactly(BubbleBarLocation.LEFT)
+ assertThat(testListener.locationReleases).isEmpty()
+
+ // Finish the drag
+ runOnMainSync { controller.onDragEnd() }
+ assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.LEFT)
+ }
+
+ @Test
+ fun drag_toLeftAndBackToRight() {
+ // Drag to left
+ runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = false)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNotNull()
+
+ // Drag to right
+ runOnMainSync { controller.onDragUpdate(pointOnRight.x, pointOnRight.y) }
+ // We have to wait for existing drop target to animate out and new to animate in
+ waitForAnimateOut()
+ waitForAnimateIn()
+
+ assertThat(dropTargetView).isNotNull()
+ assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+
+ val expectedDropTargetBounds = getExpectedDropTargetBounds(onLeft = false)
+ assertThat(dropTargetView!!.layoutParams.width).isEqualTo(expectedDropTargetBounds.width())
+ assertThat(dropTargetView!!.layoutParams.height)
+ .isEqualTo(expectedDropTargetBounds.height())
+
+ assertThat(testListener.locationChanges)
+ .containsExactly(BubbleBarLocation.LEFT, BubbleBarLocation.RIGHT)
+ assertThat(testListener.locationReleases).isEmpty()
+
+ // Release the view
+ runOnMainSync { controller.onDragEnd() }
+ assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT)
+ }
+
+ @Test
+ fun drag_toLeftInExclusionRect() {
+ runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = false)
+ // 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()
+
+ runOnMainSync { controller.onDragEnd() }
+ assertThat(testListener.locationReleases).containsExactly(BubbleBarLocation.RIGHT)
+ }
+
+ @Test
+ fun toggleSetDropTargetHidden_dropTargetExists() {
+ runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = false)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ }
+ waitForAnimateIn()
+
+ assertThat(dropTargetView).isNotNull()
+ assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+
+ runOnMainSync { controller.setDropTargetHidden(true) }
+ waitForAnimateOut()
+ assertThat(dropTargetView).isNotNull()
+ assertThat(dropTargetView!!.alpha).isEqualTo(0f)
+
+ runOnMainSync { controller.setDropTargetHidden(false) }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNotNull()
+ assertThat(dropTargetView!!.alpha).isEqualTo(1f)
+ }
+
+ @Test
+ fun toggleSetDropTargetHidden_noDropTarget() {
+ runOnMainSync { controller.setDropTargetHidden(true) }
+ waitForAnimateOut()
+ assertThat(dropTargetView).isNull()
+
+ runOnMainSync { controller.setDropTargetHidden(false) }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNull()
+ }
+
+ @Test
+ fun onDragEnd_dropTargetExists() {
+ runOnMainSync {
+ controller.onDragStart(initialLocationOnLeft = false)
+ controller.onDragUpdate(pointOnLeft.x, pointOnLeft.y)
+ }
+ waitForAnimateIn()
+ assertThat(dropTargetView).isNotNull()
+
+ runOnMainSync { controller.onDragEnd() }
+ waitForAnimateOut()
+ assertThat(dropTargetView).isNull()
+ }
+
+ @Test
+ fun onDragEnd_noDropTarget() {
+ runOnMainSync { controller.onDragEnd() }
+ waitForAnimateOut()
+ assertThat(dropTargetView).isNull()
+ }
+
+ private val dropTargetView: View?
+ get() = container.findViewById(R.id.bubble_bar_drop_target)
+
+ private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect = Rect().also {
+ positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, it)
+ }
+
+ private fun runOnMainSync(runnable: Runnable) {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable)
+ }
+
+ private fun waitForAnimateIn() {
+ // Advance animator for on-device test
+ runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) }
+ }
+
+ private fun waitForAnimateOut() {
+ // Advance animator for on-device test
+ runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) }
+ }
+
+ 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..294b1f0e21fd 100644
--- a/libs/WindowManager/Shell/res/drawable/circular_progress.xml
+++ b/libs/WindowManager/Shell/res/drawable/circular_progress.xml
@@ -25,7 +25,7 @@
<shape
android:shape="ring"
android:thickness="3dp"
- android:innerRadius="17dp"
+ 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_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..04ad572e046f 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,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_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"/>
+ <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/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_app_controls_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
index a5605a7ff50a..fa18e2bc5add 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_controls_window_decor.xml
@@ -80,11 +80,14 @@
<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:paddingHorizontal="5dp"
+ android:paddingVertical="3dp"
android:clickable="true"
- android:focusable="true" />
+ android:focusable="true"/>
<ImageButton
android:id="@+id/close_window"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_focused_window_decor.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_focused_window_decor.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_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..9599658384f0 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,95 @@
~ 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"/>
+ <FrameLayout
+ android:id="@+id/maximize_menu_maximize_button_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/desktop_mode_maximize_menu_layout_background"
+ android:padding="4dp"
+ android:layout_marginRight="8dp"
+ android:layout_marginBottom="4dp"
+ android:alpha="0">
+ <Button
+ android:id="@+id/maximize_menu_maximize_button"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="86dp"
+ android:layout_height="@dimen/desktop_mode_maximize_menu_button_height"
+ android:background="@drawable/desktop_mode_maximize_menu_button_background"
+ android:stateListAnimator="@null"/>
+ </FrameLayout>
- <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..296c89568386 100644
--- a/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
+++ b/libs/WindowManager/Shell/res/layout/maximize_menu_button.xml
@@ -20,16 +20,16 @@
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:progressDrawable="@drawable/circular_progress"
- android:layout_width="40dp"
- android:layout_height="40dp"
+ android:layout_width="34dp"
+ android:layout_height="34dp"
android:indeterminate="false"
android:visibility="invisible"/>
<ImageButton
android:id="@+id/maximize_window"
- android:layout_width="40dp"
- android:layout_height="40dp"
- android:padding="9dp"
+ android:layout_width="34dp"
+ android:layout_height="34dp"
+ android:padding="5dp"
android:contentDescription="@string/maximize_button_text"
android:tint="?androidprv:attr/materialColorOnSurface"
android:background="?android:selectableItemBackgroundBorderless"
diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml
index 5375a0913403..1c8f5e60c5c9 100644
--- a/libs/WindowManager/Shell/res/values-af/strings.xml
+++ b/libs/WindowManager/Shell/res/values-af/strings.xml
@@ -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..81d066f82261 100644
--- a/libs/WindowManager/Shell/res/values-be/strings.xml
+++ b/libs/WindowManager/Shell/res/values-be/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-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..aafb2e16b703 100644
--- a/libs/WindowManager/Shell/res/values-cs/strings.xml
+++ b/libs/WindowManager/Shell/res/values-cs/strings.xml
@@ -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 19d5d17c03fe..bcdc2a9c8539 100644
--- a/libs/WindowManager/Shell/res/values-de/strings.xml
+++ b/libs/WindowManager/Shell/res/values-de/strings.xml
@@ -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 0465850df5cf..3492f136c4f9 100644
--- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml
@@ -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 61c51ed82f17..4002e4d04d51 100644
--- a/libs/WindowManager/Shell/res/values-fr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-fr/strings.xml
@@ -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 a07406942ff2..c371f7f62feb 100644
--- a/libs/WindowManager/Shell/res/values-gl/strings.xml
+++ b/libs/WindowManager/Shell/res/values-gl/strings.xml
@@ -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 554068662dc1..27d4cfcf22d5 100644
--- a/libs/WindowManager/Shell/res/values-hr/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hr/strings.xml
@@ -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..e8b5522838b7 100644
--- a/libs/WindowManager/Shell/res/values-ko/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ko/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-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml
index c4014d35437c..302c0071a73a 100644
--- a/libs/WindowManager/Shell/res/values-ky/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ky/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-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 ee7fb5a02f59..5e43506ab621 100644
--- a/libs/WindowManager/Shell/res/values-mn/strings.xml
+++ b/libs/WindowManager/Shell/res/values-mn/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-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..9f03d8b5b178 100644
--- a/libs/WindowManager/Shell/res/values-nb/strings.xml
+++ b/libs/WindowManager/Shell/res/values-nb/strings.xml
@@ -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 1ceb706927d1..a5bd2ab5c10b 100644
--- a/libs/WindowManager/Shell/res/values-ne/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ne/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-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 3c6d4c1524e2..1210fe8fda05 100644
--- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml
+++ b/libs/WindowManager/Shell/res/values-pt-rPT/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 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 b6af5824b227..971e146ba77e 100644
--- a/libs/WindowManager/Shell/res/values-ru/strings.xml
+++ b/libs/WindowManager/Shell/res/values-ru/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-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 30ec587c9c6e..fe0b74c469f4 100644
--- a/libs/WindowManager/Shell/res/values-th/strings.xml
+++ b/libs/WindowManager/Shell/res/values-th/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-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 38ee6e2fb7de..c2ba064ac7b6 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -176,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..8d24c161e3e4 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 -->
@@ -254,6 +254,8 @@
<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>
+ <!-- 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 +274,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 +423,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>
@@ -452,16 +462,22 @@
<dimen name="desktop_mode_customizable_caption_margin_end">152dp</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 larger of the two corner radii of the maximize menu buttons. -->
- <dimen name="desktop_mode_maximize_menu_buttons_large_corner_radius">4dp</dimen>
+ <!-- The padding of the maximize menu in desktop mode. -->
+ <dimen name="desktop_mode_menu_padding">16dp</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 height of the buttons in the maximize menu. -->
+ <dimen name="desktop_mode_maximize_menu_button_height">52dp</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 corner radius of the maximize menu. -->
<dimen name="desktop_mode_maximize_menu_corner_radius">8dp</dimen>
@@ -502,18 +518,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>
@@ -535,5 +562,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..bf654d979856 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -280,4 +280,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/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java
index 22ba70860587..bdd89c0e1ac9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/DesktopModeStatus.java
@@ -14,10 +14,14 @@
* limitations under the License.
*/
-package com.android.wm.shell.desktopmode;
+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;
/**
@@ -37,15 +41,6 @@ public class DesktopModeStatus {
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.
*/
@@ -61,14 +56,38 @@ public class DesktopModeStatus {
"persist.wm.debug.desktop_use_window_shadows_focused_window", false);
/**
- * Flag to indicate whether to apply shadows to windows in desktop mode.
+ * 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);
/**
- * Return {@code true} if desktop windowing is enabled
+ * 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);
+
+ /**
+ * 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();
}
@@ -81,14 +100,6 @@ public class DesktopModeStatus {
}
/**
- * 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
@@ -104,4 +115,34 @@ public class DesktopModeStatus {
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();
+ }
}
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..785e30d879d2 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,12 @@ 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) || mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK;
+ }
+
+ /** 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 has a display change. */
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..3244837324b6 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;
@@ -718,8 +719,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/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index 1f9358e2aa91..d6b9d34c5ab3 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;
@@ -90,6 +91,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(
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/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 2606fb661e80..5600664a8f47 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
@@ -22,9 +22,6 @@ import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTas
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,6 +29,7 @@ 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.hardware.input.InputManager;
import android.net.Uri;
@@ -45,7 +43,6 @@ 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;
@@ -56,6 +53,7 @@ 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,14 @@ 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.R;
import com.android.wm.shell.animation.FlingAnimationUtils;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
-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,7 +80,8 @@ 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;
@@ -114,7 +115,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 boolean mThresholdCrossed = false;
+ private boolean mPointersPilfered = false;
+ private final boolean mRequirePointerPilfer;
private final FlingAnimationUtils mFlingAnimationUtils;
@@ -134,18 +137,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
/**
* 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 +157,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
@Nullable
private IOnBackInvokedCallback mActiveCallback;
+ @Nullable
+ private RemoteAnimationTarget[] mApps;
@VisibleForTesting
final RemoteCallback mNavigationObserver = new RemoteCallback(
@@ -169,6 +174,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 +187,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,6 +228,8 @@ 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;
@@ -240,6 +250,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 +300,11 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
private final BackAnimationImpl mBackAnimation = new BackAnimationImpl();
@Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ mShellBackAnimationRegistry.onConfigurationChanged(newConfig);
+ }
+
+ @Override
public Context getContext() {
return mContext;
}
@@ -318,8 +334,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
}
@Override
- public void onPilferPointers() {
- BackAnimationController.this.onPilferPointers();
+ public void onThresholdCrossed() {
+ BackAnimationController.this.onThresholdCrossed();
}
@Override
@@ -341,6 +357,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,20 +420,23 @@ 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));
+ } else if (shouldDispatchToAnimator) {
+ tryPilferPointers();
}
}
@@ -426,7 +452,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 +488,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 +507,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 +536,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,6 +549,7 @@ 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.
@@ -557,10 +598,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 +633,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 +644,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 +661,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont
private void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
BackMotionEvent backEvent) {
- if (callback == null) {
+ if (callback == null || !shouldDispatchToAnimator()) {
return;
}
try {
@@ -691,7 +675,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 +696,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 +712,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 +724,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 +734,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 +794,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 +808,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 +839,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 +851,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 +883,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 +921,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 +984,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.");
+ if (!validateAnimationTargets(apps)) {
+ Log.e(TAG, "Invalid animation targets!");
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);
- }
- }
- 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 +1035,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..c988c2fb5103
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
@@ -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.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.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.jank.Cuj
+import com.android.internal.policy.ScreenDecorationsUtils
+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 var cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(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()
+ private 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
+
+ /** 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 [startEnteringRect] and [targetEnteringRect] to define the movement
+ * of the enteringTarget during pre-commit phase.
+ */
+ abstract fun preparePreCommitEnteringRectMovement()
+
+ /**
+ * 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)
+ }
+
+ 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)
+
+ 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 (backMotionEvent.swipeEdge != BackEvent.EDGE_RIGHT) {
+ targetClosingRect.offset(
+ startClosingRect.right - targetClosingRect.right - displayBoundsMargin,
+ 0f
+ )
+ }
+
+ preparePreCommitEnteringRectMovement()
+
+ background.ensureBackground(
+ closingTarget!!.windowConfiguration.bounds,
+ getBackgroundColor(),
+ transaction
+ )
+ 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)
+ background.onBackProgressed(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()
+ }
+
+ 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() {
+ if (
+ closingTarget?.leash == null ||
+ enteringTarget?.leash == null ||
+ !enteringTarget!!.leash.isValid ||
+ !closingTarget!!.leash.isValid
+ ) {
+ finishAnimation()
+ return
+ }
+
+ val valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(POST_COMMIT_DURATION)
+ 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)
+ 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
+ }
+
+ protected fun applyTransform(
+ leash: SurfaceControl?,
+ rect: RectF,
+ alpha: Float,
+ baseTransformation: Transformation? = null
+ ) {
+ if (leash == null || !leash.isValid) return
+ val scale = rect.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(rect.left, rect.top)
+ transaction
+ .setAlpha(leash, keepMinimumAlpha(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()
+ }
+ }
+
+ 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 POST_COMMIT_DURATION = 300L
+ }
+}
+
+// The target will loose focus when alpha == 0, so keep a minimum value for it.
+private fun keepMinimumAlpha(transAlpha: Float): Float {
+ return max(transAlpha.toDouble(), 0.005).toFloat()
+}
+
+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..ee898a73a291 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;
@@ -49,7 +51,7 @@ import com.android.internal.policy.ScreenDecorationsUtils;
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 +81,7 @@ 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;
// The closing window properties.
private final Rect mClosingStartRect = new Rect();
@@ -91,7 +93,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();
@@ -119,6 +121,11 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
mContext = context;
}
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext);
+ }
+
private static float mapRange(float value, float min, float max) {
return min + (value * (max - min));
}
@@ -192,7 +199,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
applyTransform(mClosingTarget.leash, mClosingCurrentRect, mCornerRadius);
applyTransform(mEnteringTarget.leash, mEnteringCurrentRect, mCornerRadius);
- mTransaction.apply();
+ applyTransaction();
mBackground.onBackProgressed(progress);
}
@@ -242,6 +249,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 +267,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
if (mBackground != null) {
mBackground.removeBackground(mTransaction);
}
-
- mTransaction.apply();
+ applyTransaction();
mBackInProgress = false;
mTransformMatrix.reset();
mClosingCurrentRect.setEmpty();
@@ -275,8 +286,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 +314,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation {
if (progress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD) {
mBackground.resetStatusBarCustomization();
}
- mTransaction.apply();
+ applyTransaction();
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@@ -326,6 +335,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..e6ec2b449616
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt
@@ -0,0 +1,256 @@
+/*
+ * 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.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
+
+/** 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()
+ private var gestureProgress = 0f
+
+ 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 preparePreCommitEnteringRectMovement() {
+ // No movement for the entering rect
+ startEnteringRect.set(startClosingRect)
+ targetEnteringRect.set(startClosingRect)
+ }
+
+ override fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation {
+ gestureProgress = progress
+ 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
+
+ // TODO: Should we use the duration from the custom xml spec for the post-commit animation?
+ applyTransform(closingTarget!!.leash, currentClosingRect, linearProgress, closeAnimation!!)
+ val enteringProgress =
+ MathUtils.lerp(gestureProgress * PRE_COMMIT_MAX_PROGRESS, 1f, linearProgress)
+ applyTransform(
+ enteringTarget!!.leash,
+ currentEnteringRect,
+ enteringProgress,
+ enterAnimation!!
+ )
+ applyTransaction()
+ }
+
+ private fun applyTransform(
+ leash: SurfaceControl,
+ rect: RectF,
+ progress: Float,
+ animation: Animation
+ ) {
+ transformation.clear()
+ animation.getTransformationAt(progress, transformation)
+ applyTransform(leash, rect, transformation.alpha, transformation)
+ }
+
+ override fun finishAnimation() {
+ closeAnimation?.reset()
+ closeAnimation = null
+ enterAnimation?.reset()
+ enterAnimation = null
+ transformation.clear()
+ gestureProgress = 0f
+ 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
+ }
+
+ class AnimationLoadResult {
+ var closeAnimation: Animation? = null
+ var enterAnimation: Animation? = null
+ var backgroundColor = 0
+ }
+
+ companion object {
+ private const val PRE_COMMIT_MAX_PROGRESS = 0.2f
+ }
+}
+
+/** 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..f33c5b9bd183
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt
@@ -0,0 +1,81 @@
+/*
+ * 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 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.FAST_OUT_SLOW_IN
+ private val enteringStartOffset =
+ context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset)
+ override val allowEnteringYShift = true
+
+ 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 onGestureCommitted() {
+ // 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()
+ }
+
+ override fun onPostCommitProgress(linearProgress: Float) {
+ super.onPostCommitProgress(linearProgress)
+ val closingAlpha = max(1f - linearProgress * 2, 0f)
+ val progress = postCommitInterpolator.getInterpolation(linearProgress)
+ currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
+ applyTransform(closingTarget?.leash, currentClosingRect, closingAlpha)
+ currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
+ applyTransform(enteringTarget?.leash, currentEnteringRect, 1f)
+ applyTransaction()
+ }
+}
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/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index ea59715bc246..38c344322a30 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
@@ -87,6 +87,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.statusbar.IStatusBarService;
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;
@@ -101,13 +102,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 +171,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 +458,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;
}
}
@@ -597,13 +600,6 @@ public class BubbleController implements ConfigurationChangeListener,
}
}
- 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).
@@ -713,6 +709,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
@@ -1134,16 +1165,50 @@ 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 being dragged
+ */
+ 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 bubbleKey key of the bubble to collapse/expand
- * @param isBeingDragged whether the bubble is being dragged
+ *
+ * @param location location where bubble was released
*/
- 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 stopBubbleDrag(BubbleBarLocation location) {
+ mBubblePositioner.setBubbleBarLocation(location);
+ 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);
}
}
@@ -1184,7 +1249,7 @@ public class BubbleController implements ConfigurationChangeListener,
*/
@VisibleForTesting
public void expandStackAndSelectBubbleFromLauncher(String key, Rect bubbleBarBounds) {
- mBubblePositioner.setBubbleBarPosition(bubbleBarBounds);
+ mBubblePositioner.setBubbleBarBounds(bubbleBarBounds);
if (BubbleOverflow.KEY.equals(key)) {
mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow());
@@ -1227,8 +1292,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);
@@ -1239,20 +1303,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) {
@@ -1323,43 +1388,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);
}
@@ -1797,6 +1864,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. */
@@ -1829,6 +1905,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
@@ -1867,7 +1948,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",
@@ -1876,13 +1957,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();
@@ -2239,15 +2324,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;
@@ -2282,12 +2371,6 @@ public class BubbleController implements ConfigurationChangeListener,
}
@Override
- public void removeBubble(String key) {
- mMainExecutor.execute(
- () -> mController.removeBubble(key, Bubbles.DISMISS_USER_GESTURE));
- }
-
- @Override
public void removeAllBubbles() {
mMainExecutor.execute(() -> mController.removeAllBubbles(Bubbles.DISMISS_USER_GESTURE));
}
@@ -2298,8 +2381,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) {
+ mMainExecutor.execute(() -> mController.stopBubbleDrag(location));
+ }
+
+ @Override
+ public void dragBubbleToDismiss(String key) {
+ mMainExecutor.execute(() -> mController.dragBubbleToDismiss(key));
}
@Override
@@ -2307,6 +2400,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 setBubbleBarBounds(Rect bubbleBarBounds) {
+ mMainExecutor.execute(() -> {
+ mBubblePositioner.setBubbleBarBounds(bubbleBarBounds);
+ if (mLayerView != null) mLayerView.updateExpandedView();
+ });
+ }
}
private class BubblesImpl implements Bubbles {
@@ -2617,6 +2724,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.getIsImeVisible();
+ }
+ 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..26483c8428c6 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());
@@ -255,7 +259,9 @@ public class BubbleData {
* Returns a bubble bar update populated with the current list of active bubbles.
*/
public BubbleBarUpdate getInitialStateForBubbleBar() {
- return mStateChange.getInitialState();
+ BubbleBarUpdate initialState = mStateChange.getInitialState();
+ initialState.bubbleBarLocation = mPositioner.getBubbleBarLocation();
+ return initialState;
}
public void setSuppressionChangedListener(Bubbles.BubbleMetadataFlagListener listener) {
@@ -321,6 +327,14 @@ public class BubbleData {
return mSelectedBubble;
}
+ /**
+ * Returns the key of the selected bubble, or null if no bubble is selected.
+ */
+ @Nullable
+ public String getSelectedBubbleKey() {
+ return mSelectedBubble != null ? mSelectedBubble.getKey() : null;
+ }
+
public BubbleOverflow getOverflow() {
return mOverflow;
}
@@ -363,6 +377,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 +422,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 +512,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.
@@ -668,7 +685,6 @@ public class BubbleData {
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 +694,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 +794,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;
@@ -1216,9 +1236,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..4e8afccee40f 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
@@ -668,6 +671,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 +706,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);
+ }
}
/**
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..a35a004cdace 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,6 +97,7 @@ public class BubblePositioner {
private PointF mRestingStackPosition;
private boolean mShowingInBubbleBar;
+ private BubbleBarLocation mBubbleBarLocation = BubbleBarLocation.DEFAULT;
private final Rect mBubbleBarBounds = new Rect();
public BubblePositioner(Context context, WindowManager windowManager) {
@@ -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 getIsImeVisible() {
+ 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,14 +830,36 @@ public class BubblePositioner {
mShowingInBubbleBar = showingInBubbleBar;
}
+ public void setBubbleBarLocation(BubbleBarLocation location) {
+ mBubbleBarLocation = location;
+ }
+
+ public BubbleBarLocation getBubbleBarLocation() {
+ return mBubbleBarLocation;
+ }
+
+ /**
+ * @return <code>true</code> when bubble bar is on the left and <code>false</code> when on right
+ */
+ public boolean isBubbleBarOnLeft() {
+ return mBubbleBarLocation.isOnLeft(mDeviceConfig.isRtl());
+ }
+
/**
* Sets the position of the bubble bar in display coordinates.
*/
- public void setBubbleBarPosition(Rect bubbleBarBounds) {
+ public void setBubbleBarBounds(Rect bubbleBarBounds) {
mBubbleBarBounds.set(bubbleBarBounds);
}
/**
+ * Returns the display coordinates of the bubble bar.
+ */
+ public Rect getBubbleBarBounds() {
+ return mBubbleBarBounds;
+ }
+
+ /**
* How wide the expanded view should be when showing from the bubble bar.
*/
public int getExpandedViewWidthForBubbleBar(boolean isOverflow) {
@@ -815,11 +870,42 @@ 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;
@@ -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..9fabd4247670 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;
@@ -449,17 +450,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 +479,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 +537,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 +672,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,7 +721,7 @@ public class BubbleStackView extends FrameLayout
mDismissView.hide();
}
- mIsDraggingStack = false;
+ onDraggingEnded();
// Hide the stack after a delay, if needed.
updateTemporarilyInvisibleAnimation(false /* hideImmediately */);
@@ -856,6 +864,7 @@ public class BubbleStackView extends FrameLayout
}
};
+ private boolean mShowingOverflow;
private BubbleOverflow mBubbleOverflow;
private StackEducationView mStackEduView;
private StackEducationView.Manager mStackEducationViewManager;
@@ -884,18 +893,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 +936,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 +994,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 +1021,7 @@ public class BubbleStackView extends FrameLayout
WindowManager.class)));
onDisplaySizeChanged();
mExpandedAnimationController.updateResources();
+ mExpandedAnimationController.onOrientationChanged();
mStackAnimationController.updateResources();
mBubbleOverflow.updateResources();
@@ -1089,6 +1092,7 @@ public class BubbleStackView extends FrameLayout
} else {
maybeShowStackEdu();
}
+ onDraggingEnded();
});
animate()
@@ -1146,6 +1150,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 +1216,19 @@ public class BubbleStackView extends FrameLayout
}
};
+ 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);
@@ -1442,24 +1467,56 @@ 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() {
+ mBubbleOverflow.setVisible(mShowingOverflow
+ && (mIsExpanded || mBubbleData.isShowingOverflow())
+ ? VISIBLE
+ : GONE);
+ }
+
+ 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 +1576,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 +1743,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 +1796,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();
@@ -1854,7 +1915,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 +1973,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 +2004,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());
@@ -2200,7 +2247,7 @@ public class BubbleStackView extends FrameLayout
mBubbleContainer.addView(bubble.getIconView(), index,
new LayoutParams(mPositioner.getBubbleSize(),
mPositioner.getBubbleSize()));
- updateBubbleShadows(false /* showForAllBubbles */);
+ updateBubbleShadows(mIsExpanded);
requestUpdate();
}
}
@@ -2333,9 +2380,9 @@ public class BubbleStackView extends FrameLayout
beforeExpandedViewAnimation();
showScrim(true, null /* runnable */);
- updateZOrder();
- updateBadges(false /* setBadgeForCollapsedStack */);
+ updateBubbleShadows(mIsExpanded);
mBubbleContainer.setActiveController(mExpandedAnimationController);
+ updateBadges(false /* setBadgeForCollapsedStack */);
updateOverflowVisibility();
updatePointerPosition(false /* forIme */);
mExpandedAnimationController.expandFromStack(() -> {
@@ -2351,9 +2398,9 @@ 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);
@@ -2364,40 +2411,42 @@ public class BubbleStackView extends FrameLayout
? 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);
@@ -2474,15 +2523,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,7 +2544,6 @@ public class BubbleStackView extends FrameLayout
mManageEduView.hide();
}
- updateZOrder();
updateBadges(true /* setBadgeForCollapsedStack */);
afterExpandedViewAnimation();
if (previouslySelected != null) {
@@ -2501,7 +2551,8 @@ public class BubbleStackView extends FrameLayout
}
mExpandedViewAnimationController.reset();
};
- mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after);
+ mExpandedViewAnimationController.animateCollapse(collapseBackToStack, after,
+ collapsePosition);
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != 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
@@ -3327,19 +3378,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 +3415,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++) {
@@ -3414,8 +3459,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;
}
/**
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..1d053f9aab35 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;
@@ -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..1eff149f2e91 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
@@ -32,14 +33,19 @@ interface IBubbles {
oneway void showBubble(in String key, in Rect bubbleBarBounds) = 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 setBubbleBarBounds(in Rect bubbleBarBounds) = 10;
+
+ oneway void stopBubbleDrag(in BubbleBarLocation location) = 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..45ad6319bbf8 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,16 @@ 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 bubbleBarBounds = mPositioner.getBubbleBarBounds();
+ matrix.setScale(
+ scale,
+ scale,
+ bubbleBarBounds.centerX(),
+ bubbleBarBounds.top);
+ }
+
/**
* Animate the expanded bubble when it is being dragged
*/
@@ -477,7 +482,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/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt
index 7d37d6068dfb..a51ac633ad86 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,32 @@ class BubbleBarExpandedViewDragController(
private inner class MagnetListener : MagnetizedObject.MagnetListener {
override fun onStuckToTarget(
- target: MagnetizedObject.MagneticTarget,
- draggedObject: MagnetizedObject<*>
+ target: MagnetizedObject.MagneticTarget,
+ draggedObject: MagnetizedObject<*>
) {
isStuckToDismiss = true
+ pinController.setDropTargetHidden(true)
}
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)
+ pinController.setDropTargetHidden(false)
}
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/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 42799d975e1b..123cc7e9d488 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,6 +117,21 @@ public class BubbleBarLayerView extends FrameLayout
setUpDismissView();
+ 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 -> hideMenuOrCollapse());
}
@@ -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();
@@ -207,15 +221,18 @@ public class BubbleBarLayerView extends FrameLayout
}
});
+ 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,10 +341,7 @@ 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 */
@@ -342,22 +356,17 @@ public class BubbleBarLayerView extends FrameLayout
}
/** 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 +395,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/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/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
index ca06024a9adb..55dc793cc3b6 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;
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/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..da414cc9ae70 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,7 +349,7 @@ 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,
@@ -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..e514f9d70599
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BaseBubblePinController.kt
@@ -0,0 +1,196 @@
+/*
+ * 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 onLeft = false
+ private var dismissZone: RectF? = null
+ 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) {
+ 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
+
+ if (onLeft && x > screenCenterX) {
+ onLeft = false
+ onLocationChange(RIGHT)
+ } else if (!onLeft && x < screenCenterX) {
+ onLeft = true
+ onLocationChange(LEFT)
+ }
+ }
+
+ /** Temporarily hide the drop target view */
+ fun setDropTargetHidden(hidden: Boolean) {
+ val targetView = getDropTargetView() ?: return
+ if (hidden) {
+ targetView.animateOut()
+ } else {
+ targetView.animateIn()
+ }
+ }
+
+ /** 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/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/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/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/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..579a7943829e 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,14 @@
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.os.RemoteException
+import android.os.SystemProperties
import android.util.DisplayMetrics
import android.util.Log
import android.util.Pair
@@ -135,7 +138,24 @@ object PipUtils {
}
}
+ 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/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java
index 424e5fa23615..2234041b8c9d 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);
@@ -345,9 +345,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, true /* shouldUseParallaxEffect */);
+ mSplitLayout.updateDividerBounds(position, true /* shouldUseParallaxEffect */);
}
break;
case MotionEvent.ACTION_UP:
@@ -363,7 +363,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 +472,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 3de8004b57fc..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;
@@ -118,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;
}
@@ -137,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);
}
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 d261e2435b5f..8331654839c9 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, boolean shouldUseParallaxEffect) {
+ void updateDividerBounds(int position, boolean shouldUseParallaxEffect) {
updateBounds(position);
mSplitLayoutHandler.onLayoutSizeChanging(this, mSurfaceEffectPolicy.mParallaxOffset.x,
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,19 @@ 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() {
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 */));
}
@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 +647,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange
.setDuration(duration);
mDividerFlingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
mDividerFlingAnimator.addUpdateListener(
- animation -> updateDivideBounds(
+ animation -> updateDividerBounds(
(int) animation.getAnimatedValue(), false /* shouldUseParallaxEffect */)
);
mDividerFlingAnimator.addListener(new AnimatorListenerAdapter() {
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 f989991ab004..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;
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 5359e9faec3d..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;
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 15c6cbc3f1c4..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.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;
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..991fbafed296 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,12 @@ 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.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 +107,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 +328,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());
}
//
@@ -673,6 +676,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 +702,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 +867,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 +891,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 +910,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();
@@ -930,6 +953,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..fb0a1ab3062e 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;
@@ -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,45 @@ 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) {
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);
}
@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 +563,8 @@ public abstract class WMShellModule {
@WMSingleton
@Provides
static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler(
- Transitions transitions) {
+ Transitions transitions,
+ Optional<DesktopTasksLimiter> desktopTasksLimiter) {
return new EnterDesktopTaskTransitionHandler(transitions);
}
@@ -562,6 +591,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 +662,7 @@ 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..d644006cde81 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,6 +55,7 @@ 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;
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..01364d1de279 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,29 @@ 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.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
@@ -62,9 +70,12 @@ 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
@@ -78,6 +89,9 @@ 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();
@@ -85,7 +99,7 @@ public abstract class Pip2Module {
return Optional.ofNullable(PipController.create(
context, shellInit, shellController, displayController, displayInsetsController,
pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState, pipScheduler,
- mainExecutor));
+ taskStackListener, shellTaskOrganizer, pipTransitionState, mainExecutor));
}
}
@@ -93,8 +107,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 +123,47 @@ public abstract class Pip2Module {
return new PhonePipMenuController(context, pipBoundsState, pipMediaController,
systemWindows, pipUiEventLogger, mainExecutor, mainHandler);
}
+
+
+ @WMSingleton
+ @Provides
+ static PipTouchHandler providePipTouchHandler(Context context,
+ ShellInit shellInit,
+ 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, 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 providePipStackListenerController() {
+ return new PipTransitionState();
+ }
}
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..df1b06225fda 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,7 @@ package com.android.wm.shell.desktopmode;
import android.graphics.Region;
-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;
@@ -49,8 +49,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);
/** Called when requested to go to fullscreen from the current focused desktop app. */
void moveFocusedTaskToFullscreen(int displayId);
+
+ /** 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/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
new file mode 100644
index 000000000000..0b7a3e838e88
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -0,0 +1,353 @@
+/*
+ * 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.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.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()
+
+ // 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.isRecentsTransition() && 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
+ )
+ }
+
+ 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 {
+ // TODO(b/326231756) - Add support for missing enter reasons
+ return when (transitionInfo.type) {
+ WindowManager.TRANSIT_WAKE -> EnterReason.SCREEN_ON
+ Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> EnterReason.APP_HANDLE_DRAG
+ Transitions.TRANSIT_MOVE_TO_DESKTOP -> EnterReason.APP_HANDLE_MENU_BUTTON
+ WindowManager.TRANSIT_OPEN -> EnterReason.APP_FREEFORM_INTENT
+ else -> EnterReason.UNKNOWN_ENTER
+ }
+ }
+
+ /** Get [ExitReason] for this session exit */
+ private fun getExitReason(transitionInfo: TransitionInfo): ExitReason {
+ // TODO(b/326231756) - Add support for missing exit reasons
+ return when {
+ transitionInfo.type == WindowManager.TRANSIT_SLEEP -> ExitReason.SCREEN_OFF
+ transitionInfo.type == WindowManager.TRANSIT_CLOSE -> ExitReason.TASK_FINISHED
+ transitionInfo.type == Transitions.TRANSIT_EXIT_DESKTOP_MODE -> ExitReason.DRAG_TO_EXIT
+ transitionInfo.isRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW
+ else -> ExitReason.UNKNOWN_EXIT
+ }
+ }
+
+ /** 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.isRecentsTransition(): Boolean {
+ return this.type == WindowManager.TRANSIT_TO_FRONT &&
+ this.flags == WindowManager.TRANSIT_FLAG_IS_RECENTS
+ }
+} \ No newline at end of file
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..f1a475a42452 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
@@ -36,7 +36,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
@@ -61,8 +68,28 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl
return controller.moveToDesktop(taskId, WindowContainerTransaction())
}
+ 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
+ }
+
+ 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/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt
index 7c8fcbb16711..7e0234ef8546 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
@@ -44,9 +47,11 @@ class DesktopModeTaskRepository {
*/
val activeTasks: ArraySet<Int> = ArraySet(),
val visibleTasks: ArraySet<Int> = ArraySet(),
- var stashed: Boolean = false
+ val minimizedTasks: ArraySet<Int> = ArraySet(),
)
+ // Token of the current wallpaper activity, used to remove it when the last task is removed
+ var wallpaperActivityToken: WindowContainerToken? = null
// Tasks currently in freeform mode, ordered from top to bottom (top is at index 0).
private val freeformTasksInZOrder = mutableListOf<Int>()
private val activeTasksListeners = ArraySet<ActiveTasksListener>()
@@ -54,6 +59,8 @@ class DesktopModeTaskRepository {
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
@@ -87,10 +94,8 @@ class DesktopModeTaskRepository {
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)
}
}
}
@@ -195,6 +200,22 @@ class DesktopModeTaskRepository {
}
}
+ /** 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]
*/
@@ -203,6 +224,25 @@ class DesktopModeTaskRepository {
}
/**
+ * 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.
+ */
+ fun getActiveNonMinimizedTasksOrderedFrontToBack(displayId: Int): List<Int> {
+ val activeTasks = getActiveTasks(displayId)
+ val allTasksInZOrder = getFreeformTasksInZOrder()
+ 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).
*/
// TODO(b/278084491): pass in display id
@@ -226,16 +266,26 @@ class DesktopModeTaskRepository {
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 +294,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",
@@ -291,6 +337,24 @@ class DesktopModeTaskRepository {
freeformTasksInZOrder.add(0, taskId)
}
+ /** 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.
*/
@@ -301,9 +365,10 @@ class DesktopModeTaskRepository {
taskId
)
freeformTasksInZOrder.remove(taskId)
+ boundsBeforeMaximizeByTaskId.remove(taskId)
KtProtoLog.d(
WM_SHELL_DESKTOP_MODE,
- "DesktopTaskRepo: remaining freeform tasks: " + freeformTasksInZOrder.toDumpString()
+ "DesktopTaskRepo: remaining freeform tasks: %s", freeformTasksInZOrder.toDumpString(),
)
}
@@ -332,30 +397,17 @@ class DesktopModeTaskRepository {
}
/**
- * Update stashed status on display with id [displayId]
+ * Removes and returns the bounds saved before maximizing the given task.
*/
- 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) }
- }
- }
+ fun removeBoundsBeforeMaximize(taskId: Int): Rect? {
+ return boundsBeforeMaximizeByTaskId.removeReturnOld(taskId)
}
/**
- * Check if display with id [displayId] has desktop tasks stashed
+ * Saves the bounds of the given task before maximizing.
*/
- fun isStashed(displayId: Int): Boolean {
- return displayData[displayId]?.stashed ?: false
+ fun saveBoundsBeforeMaximize(taskId: Int, bounds: Rect) {
+ boundsBeforeMaximizeByTaskId.set(taskId, Rect(bounds))
}
internal fun dump(pw: PrintWriter, prefix: String) {
@@ -373,7 +425,6 @@ 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}")
}
}
@@ -395,11 +446,6 @@ class DesktopModeTaskRepository {
* 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/DesktopModeUiEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt
new file mode 100644
index 000000000000..aa11a7d8a663
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt
@@ -0,0 +1,100 @@
+/*
+ * 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"
+ }
+} \ No newline at end of file
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..6da37419737b
--- /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..6a3c8d2f599a 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
@@ -98,6 +98,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 +137,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 +156,7 @@ public class DesktopModeVisualIndicator {
region.union(new Rect(0,
-captionHeight,
layout.width(),
- edgeTransitionHeight));
+ transitionHeight));
}
return region;
}
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..2dc4573b4921 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,13 +40,16 @@ 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
@@ -59,15 +63,18 @@ 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.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.RecentsTransitionHandler
import com.android.wm.shell.recents.RecentsTransitionStateListener
+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.splitscreen.SplitScreenController
import com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DESKTOP_MODE
import com.android.wm.shell.sysui.ShellCommandHandler
@@ -77,9 +84,12 @@ 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
@@ -101,10 +111,12 @@ class DesktopTasksController(
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
+ @ShellMainThread private val mainExecutor: ShellExecutor,
+ private val desktopTasksLimiter: Optional<DesktopTasksLimiter>,
) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler,
DragAndDropController.DragAndDropListener {
@@ -112,7 +124,6 @@ class DesktopTasksController(
private var visualIndicator: DesktopModeVisualIndicator? = null
private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler =
DesktopModeShellCommandHandler(this)
-
private val mOnAnimationFinishedCallback = Consumer<SurfaceControl.Transaction> {
t: SurfaceControl.Transaction ->
visualIndicator?.releaseVisualIndicator(t)
@@ -140,7 +151,7 @@ class DesktopTasksController(
private val transitionAreaHeight
get() = context.resources.getDimensionPixelSize(
- com.android.wm.shell.R.dimen.desktop_mode_transition_area_height
+ com.android.wm.shell.R.dimen.desktop_mode_fullscreen_from_desktop_height
)
private val transitionAreaWidth
@@ -148,12 +159,16 @@ class DesktopTasksController(
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 +176,11 @@ 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 +204,11 @@ class DesktopTasksController(
dragAndDropController.addListener(this)
}
+ @VisibleForTesting
+ fun getVisualIndicator(): DesktopModeVisualIndicator? {
+ return visualIndicator
+ }
+
fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) {
toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
@@ -205,7 +228,7 @@ 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)
@@ -218,41 +241,13 @@ 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) {
val allFocusedTasks =
shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo ->
taskInfo.isFocused &&
@@ -266,9 +261,11 @@ 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]
+ } else {
+ allFocusedTasks[0]
+ }
moveToDesktop(splitFocusedTask)
}
1 -> {
@@ -305,6 +302,22 @@ class DesktopTasksController(
task: RunningTaskInfo,
wct: WindowContainerTransaction = WindowContainerTransaction()
) {
+ if (!DesktopModeStatus.canEnterDesktopMode(context)) {
+ KtProtoLog.w(
+ WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: Cannot enter desktop, " +
+ "display does not meet minimum size requirements"
+ )
+ return
+ }
+ 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
+ }
KtProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"DesktopTasksController: moveToDesktop taskId=%d",
@@ -312,11 +325,13 @@ 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)
+ addPendingMinimizeTransition(transition, taskToMinimize)
} else {
shellTaskOrganizer.applyTransaction(wct)
}
@@ -354,10 +369,13 @@ class DesktopTasksController(
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) }
}
/**
@@ -370,9 +388,26 @@ class DesktopTasksController(
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) {
shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task ->
@@ -382,14 +417,8 @@ class DesktopTasksController(
/** 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) }
- }
+ getFocusedFreeformTask(displayId)
+ ?.let { moveToFullscreenWithAnimation(it, it.positionInParent) }
}
/** Move a desktop app to split screen. */
@@ -401,7 +430,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 +443,12 @@ 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()
}
}
@@ -443,7 +476,11 @@ class DesktopTasksController(
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
exitDesktopTaskTransitionHandler.startTransition(
- Transitions.TRANSIT_EXIT_DESKTOP_MODE, wct, position, mOnAnimationFinishedCallback)
+ Transitions.TRANSIT_EXIT_DESKTOP_MODE,
+ wct,
+ position,
+ mOnAnimationFinishedCallback
+ )
} else {
shellTaskOrganizer.applyTransaction(wct)
releaseVisualIndicator()
@@ -465,8 +502,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)
}
@@ -489,8 +528,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 +555,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 +582,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 +594,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 +630,53 @@ 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 +690,44 @@ 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)
-
- // First move home to front and then other tasks on top of it
- moveHomeTaskToFront(wct)
+ private fun bringDesktopAppsToFrontBeforeShowingNewTask(
+ displayId: Int,
+ wct: WindowContainerTransaction,
+ newTaskIdInFront: Int
+ ): RunningTaskInfo? = bringDesktopAppsToFront(displayId, wct, newTaskIdInFront)
+
+ private fun bringDesktopAppsToFront(
+ displayId: Int,
+ wct: WindowContainerTransaction,
+ newTaskIdInFront: Int? = null
+ ): RunningTaskInfo? {
+ KtProtoLog.v(WM_SHELL_DESKTOP_MODE,
+ "DesktopTasksController: bringDesktopAppsToFront, newTaskIdInFront=%s",
+ newTaskIdInFront ?: "null")
+
+ if (Flags.enableDesktopWindowingWallpaperActivity()) {
+ // Add translucent wallpaper activity to show the wallpaper underneath
+ addWallpaperActivity(wct)
+ } else {
+ // Move home to front
+ moveHomeTaskToFront(wct)
+ }
- 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) }
- .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) }
- .forEach { task -> wct.reorder(task.token, true /* onTop */) }
+ 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 +737,27 @@ 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 +804,9 @@ 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})"
@@ -729,12 +843,13 @@ class DesktopTasksController(
val result = triggerTask?.let { task ->
when {
- // If display has tasks stashed, handle as stashed launch
- desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task)
+ request.type == TRANSIT_TO_BACK -> handleBackNavigation(task)
+ // Check if the task has a top transparent activity
+ shouldLaunchAsModal(task) -> handleTransparentTaskLaunch(task)
// Check if fullscreen task should be updated
- task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task)
+ task.isFullscreen -> handleFullscreenTaskLaunch(task, transition)
// Check if freeform task should be updated
- task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task)
+ task.isFreeform -> handleFreeformTaskLaunch(task, transition)
else -> {
null
}
@@ -767,10 +882,23 @@ class DesktopTasksController(
.forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) }
}
- private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? {
+ 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" +
@@ -781,13 +909,23 @@ class DesktopTasksController(
addMoveToFullscreenChanges(wct, task)
}
}
+ // Desktop Mode is showing and we're launching a new Task - we might need to minimize
+ // a Task.
+ val wct = WindowContainerTransaction()
+ val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task)
+ if (taskToMinimize != null) {
+ addPendingMinimizeTransition(transition, taskToMinimize)
+ return wct
+ }
return null
}
- 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" +
@@ -796,35 +934,54 @@ class DesktopTasksController(
)
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
+ // Always launch transparent tasks in fullscreen.
+ private fun handleTransparentTaskLaunch(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) {
+ 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()) {
@@ -836,8 +993,9 @@ class DesktopTasksController(
wct: WindowContainerTransaction,
taskInfo: RunningTaskInfo
) {
- val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode
- val targetWindowingMode = if (displayWindowingMode == 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 {
@@ -858,28 +1016,72 @@ class DesktopTasksController(
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)
+ // 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)
+ splitScreenController.requestEnterSplitSelect(
+ taskInfo,
+ wct,
+ if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT,
+ taskInfo.configuration.windowConfiguration.bounds
+ )
}
}
@@ -927,20 +1129,18 @@ 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 +1151,45 @@ 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)
+ }
+ 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()
@@ -981,15 +1201,30 @@ class DesktopTasksController(
* @param taskInfo the task being dragged.
* @param y height of drag, to be checked against status bar height.
*/
- fun onDragPositioningEndThroughStatusBar(
- taskInfo: RunningTaskInfo,
- freeformBounds: Rect
- ) {
- finalizeDragToDesktop(taskInfo, freeformBounds)
- }
-
- private fun getStatusBarHeight(taskInfo: RunningTaskInfo): Int {
- return displayController.getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0
+ fun onDragPositioningEndThroughStatusBar(inputCoordinates: PointF, taskInfo: RunningTaskInfo) {
+ 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 -> {
+ finalizeDragToDesktop(taskInfo, getSnapBounds(taskInfo, SnapPosition.LEFT))
+ }
+ DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
+ finalizeDragToDesktop(taskInfo, getSnapBounds(taskInfo, SnapPosition.RIGHT))
+ }
+ }
}
/**
@@ -1055,7 +1290,8 @@ class DesktopTasksController(
pendingIntentLaunchFlags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
setPendingIntentBackgroundActivityStartMode(
- ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED
+ )
isPendingIntentBackgroundActivityLaunchAllowedByPermission = true
}
val wct = WindowContainerTransaction()
@@ -1100,9 +1336,9 @@ class DesktopTasksController(
}
}
- override fun enterDesktop(displayId: Int) {
+ override fun moveFocusedTaskToDesktop(displayId: Int) {
mainExecutor.execute {
- this@DesktopTasksController.enterDesktop(displayId)
+ this@DesktopTasksController.moveFocusedTaskToDesktop(displayId)
}
}
@@ -1111,6 +1347,12 @@ class DesktopTasksController(
this@DesktopTasksController.enterFullscreen(displayId)
}
}
+
+ override fun moveFocusedTaskToStageSplit(displayId: Int, leftOrTop: Boolean) {
+ mainExecutor.execute {
+ this@DesktopTasksController.enterSplit(displayId, leftOrTop)
+ }
+ }
}
/** The interface for calls from outside the host process. */
@@ -1133,16 +1375,6 @@ class DesktopTasksController(
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 {
@@ -1174,25 +1406,25 @@ class DesktopTasksController(
) { c -> c.showDesktopApps(displayId, remoteTransition) }
}
- override fun stashDesktopApps(displayId: Int) {
+ override fun showDesktopApp(taskId: Int) {
ExecutorUtils.executeRemoteCallWithTaskPermission(
controller,
- "stashDesktopApps"
- ) { c -> c.stashDesktopApps(displayId) }
+ "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 {
@@ -1224,6 +1456,13 @@ class DesktopTasksController(
"setTaskListener"
) { _ -> listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() }
}
+
+ override fun moveToDesktop(taskId: Int) {
+ ExecutorUtils.executeRemoteCallWithTaskPermission(
+ controller,
+ "moveToDesktop"
+ ) { c -> c.moveToDesktop(taskId) }
+ }
}
companion object {
@@ -1233,7 +1472,7 @@ class DesktopTasksController(
@JvmField
val DESKTOP_MODE_INITIAL_BOUNDS_SCALE = SystemProperties
- .getInt("persist.wm.debug.freeform_initial_bounds_scale", 75) / 100f
+ .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
/**
* Check if desktop density override is enabled
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..e5e435da48b2 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
@@ -6,6 +6,7 @@ import android.animation.RectEvaluator
import android.animation.ValueAnimator
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
@@ -15,6 +16,7 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.FILL_IN_COMPONENT
import android.graphics.Rect
+import android.os.Bundle
import android.os.IBinder
import android.os.SystemClock
import android.view.SurfaceControl
@@ -25,6 +27,9 @@ 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.protolog.ShellProtoLogGroup
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.splitscreen.SplitScreenController
@@ -67,7 +72,7 @@ class DragToDesktopTransitionHandler(
.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 +80,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
@@ -124,15 +132,19 @@ class DragToDesktopTransitionHandler(
options.toBundle()
)
val wct = WindowContainerTransaction()
- wct.sendPendingIntent(pendingIntent, launchHomeIntent, options.toBundle())
+ 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
+ startTransitionToken = startTransitionToken,
+ otherSplitTask = otherTask
)
} else {
TransitionState.FromFullscreen(
@@ -149,20 +161,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)
}
/**
@@ -346,6 +358,12 @@ class DragToDesktopTransitionHandler(
?: 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)
@@ -368,67 +386,49 @@ class DragToDesktopTransitionHandler(
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)
+ 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, 1f, 1f)
- setPosition(
- draggedTaskLeash,
- animBounds.left.toFloat(),
- animBounds.top.toFloat()
- )
+ setScale(draggedTaskLeash, animScale, animScale)
+ setPosition(
+ draggedTaskLeash,
+ animBounds.left.toFloat(),
+ animBounds.top.toFloat()
+ )
setWindowCrop(
- draggedTaskLeash,
- animBounds.width(),
- animBounds.height()
+ draggedTaskLeash,
+ animBounds.width(),
+ animBounds.height()
)
}
onTaskResizeAnimationListener.onBoundsChange(
@@ -492,10 +492,8 @@ class DragToDesktopTransitionHandler(
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
@@ -567,7 +565,18 @@ 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 {
@@ -616,6 +625,7 @@ class DragToDesktopTransitionHandler(
override var cancelled: Boolean = false,
override var startAborted: Boolean = false,
var splitRootChange: Change? = null,
+ var otherSplitTask: Int
) : TransitionState()
}
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..74b8f831cdc0 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
@@ -78,10 +78,12 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition
/**
* 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) {
+ public IBinder moveToDesktop(@NonNull WindowContainerTransaction wct) {
final IBinder token = mTransitions.startTransition(TRANSIT_MOVE_TO_DESKTOP, wct, this);
mPendingTransitionTokens.add(token);
+ return token;
}
@Override
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..c36f8deb6ecc 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
@@ -28,10 +28,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 +45,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);
} \ 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/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index 7da1b23dd5b1..165feec58455 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
@@ -67,8 +67,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;
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..6a7d297e83e5 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,6 +16,7 @@
package com.android.wm.shell.draganddrop;
+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;
@@ -301,16 +302,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/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..e0e2e706d649 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.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.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.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..c79eef7efb61 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);
+ }
}
/**
@@ -142,6 +157,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 +172,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 +293,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 +363,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..39b9000856f2 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
@@ -55,7 +55,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/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS
index ec09827fa4d1..afddfab99a2b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/OWNERS
@@ -1,3 +1,2 @@
# WM shell sub-module pip owner
hwwang@google.com
-mateuszc@google.com
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..eb845db409e3 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
@@ -743,11 +743,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/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index a9013b9c4fd6..e1657f99639d 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,6 +91,7 @@ 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;
@@ -597,6 +597,17 @@ 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(() -> {
+ exitPip(animationDurationMs, requestEnterSplit);
+ });
+ return;
+ }
+
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"exitPip: %s, state=%s", mTaskInfo.topActivity, mPipTransitionState);
final WindowContainerTransaction wct = new WindowContainerTransaction();
@@ -843,7 +854,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();
@@ -1976,6 +1988,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 d60f5a631044..2082756feda7 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
@@ -43,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;
@@ -348,9 +347,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();
}
}
@@ -818,8 +824,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);
}
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 5584f238e131..7730285c86c8 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
@@ -54,7 +54,6 @@ 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.
@@ -125,12 +124,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.
}
@@ -305,6 +300,14 @@ public abstract class PipTransitionController implements Transitions.TransitionH
public void end() {
}
+ /**
+ * 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..85f9194ac804 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
@@ -122,6 +122,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 +154,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 +847,7 @@ public class PipController implements PipTransitionController.PipTransitionCallb
}
}
- private void onSystemUiStateChanged(boolean isValidState, int flag) {
+ private void onSystemUiStateChanged(boolean isValidState, long flag) {
mTouchHandler.onSystemUiStateChanged(isValidState);
}
@@ -1043,6 +1047,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 +1078,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 +1195,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);
});
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/pip2/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS
index 6dabb3bf6f9a..79d1793819f4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/OWNERS
@@ -1,4 +1,3 @@
# WM shell sub-module pip owner
hwwang@google.com
-mateuszc@google.com
gabiyev@google.com
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..a12882f56eb7 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,40 @@
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;
@@ -54,18 +65,49 @@ import com.android.wm.shell.sysui.ShellInit;
* 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 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,
@@ -76,6 +118,9 @@ public class PipController implements ConfigurationChangeListener,
PipBoundsAlgorithm pipBoundsAlgorithm,
PipDisplayLayoutState pipDisplayLayoutState,
PipScheduler pipScheduler,
+ TaskStackListenerImpl taskStackListener,
+ ShellTaskOrganizer shellTaskOrganizer,
+ PipTransitionState pipTransitionState,
ShellExecutor mainExecutor) {
mContext = context;
mShellController = shellController;
@@ -85,6 +130,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,14 +141,31 @@ 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,
+ 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, shellController, displayController,
+ displayInsetsController, pipBoundsState, pipBoundsAlgorithm, pipDisplayLayoutState,
+ pipScheduler, taskStackListener, shellTaskOrganizer, pipTransitionState,
+ mainExecutor);
}
private void onInit() {
@@ -109,7 +175,6 @@ public class PipController implements ConfigurationChangeListener,
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 +188,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 +263,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) {
@@ -196,8 +281,61 @@ public class PipController implements ConfigurationChangeListener,
Rect destinationBounds, SurfaceControl overlay, Rect appBounds) {
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));
+ }
}
/**
@@ -206,9 +344,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 +375,7 @@ public class PipController implements ConfigurationChangeListener,
@Override
public void invalidate() {
mController = null;
+ mListener.unregister();
}
@Override
@@ -257,7 +416,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..be10151ca5aa
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java
@@ -0,0 +1,777 @@
+/*
+ * 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 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();
+
+ // Setting state to CHANGED_PIP_BOUNDS applies finishTx and notifies Core.
+ mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS);
+ 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..b55a41d8808f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java
@@ -0,0 +1,571 @@
+/*
+ * 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.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 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 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;
+ }
+ final Rect startBounds = new Rect(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;
+ mWaitingForBoundsChangeTransition = true;
+ mPipScheduler.scheduleAnimateResizePip(mLastResizeBounds);
+ 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.
+ cleanUpHighPerfSessionMaybe();
+
+ // Setting state to CHANGED_PIP_BOUNDS applies finishTx and notifies Core.
+ mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS);
+
+ mUpdateResizeBoundsCallback.accept(destinationBounds);
+ 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..49475077211f 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,47 @@ 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) {
+ if (mPipTransitionState.mPipTaskToken == null || !mPipTransitionState.isInPip()) {
return;
}
WindowContainerTransaction wct = new WindowContainerTransaction();
- wct.setBounds(mPipTaskToken, toBounds);
- mPipTransitionController.startResizeTransition(wct, onFinishResizeCallback);
+ wct.setBounds(mPipTransitionState.mPipTaskToken, toBounds);
+ mPipTransitionController.startResizeTransition(wct);
}
- void setInSwipePipToHomeTransition(boolean inSwipePipToHome) {
- mInSwipePipToHomeTransition = true;
+ /**
+ * 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 */);
}
- boolean isInSwipePipToHomeTransition() {
- return mInSwipePipToHomeTransition;
- }
+ /**
+ * 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());
- void onExitPip() {
- mPipTaskToken = null;
- mPinnedTaskLeash = null;
+ 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..319d1999a272
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
@@ -0,0 +1,1123 @@
+/*
+ * 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.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 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,
+ 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;
+ 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();
+
+ 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..7dddd2748f83 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,51 @@ 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.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 +96,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 +115,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 +134,10 @@ public class PipTransition extends PipTransitionController {
}
}
+ //
+ // Transition collection stage lifecycle hooks
+ //
+
@Override
public void startExitTransition(int type, WindowContainerTransaction out,
@Nullable Rect destinationBounds) {
@@ -106,13 +151,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 +178,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 +197,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 +227,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 +253,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 +285,85 @@ 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;
+ }
+ PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams;
+ Rect srcRectHint = params.getSourceRectHint();
+ Rect startBounds = pipChange.getStartAbsBounds();
+ Rect destinationBounds = pipChange.getEndAbsBounds();
+
+ WindowContainerTransaction finishWct = new WindowContainerTransaction();
+ SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+
+ if (PipBoundsAlgorithm.isSourceRectHintValidForEnterPip(srcRectHint, destinationBounds)) {
+ final float scale = (float) destinationBounds.width() / srcRectHint.width();
+ startTransaction.setWindowCrop(pipLeash, srcRectHint);
+ startTransaction.setPosition(pipLeash,
+ destinationBounds.left - srcRectHint.left * scale,
+ destinationBounds.top - srcRectHint.top * scale);
+
+ // Reset the scale in case we are in the multi-activity case.
+ // TO_FRONT transition already scales down the task in single-activity case, but
+ // in multi-activity case, reparenting yields new reset scales coming from pinned task.
+ startTransaction.setScale(pipLeash, scale, scale);
+ } else {
+ final float scaleX = (float) destinationBounds.width() / startBounds.width();
+ final float scaleY = (float) destinationBounds.height() / startBounds.height();
+ final int overlaySize = PipContentOverlay.PipAppIconOverlay.getOverlaySize(
+ mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds);
+ SurfaceControl overlayLeash = mPipTransitionState.getSwipePipToHomeOverlay();
+
+ startTransaction.setPosition(pipLeash, destinationBounds.left, destinationBounds.top)
+ .setScale(pipLeash, scaleX, scaleY)
+ .setWindowCrop(pipLeash, startBounds)
+ .reparent(overlayLeash, pipLeash)
+ .setLayer(overlayLeash, Integer.MAX_VALUE);
+
+ // Overlay needs to be adjusted once a new draw comes in resetting surface transform.
+ tx.setScale(overlayLeash, 1f, 1f);
+ 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 +372,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,10 +389,8 @@ 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();
finishCallback.onTransitionFinished(null);
@@ -273,11 +402,26 @@ public class PipTransition extends PipTransitionController {
@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);
- onExitPip();
+ mPipTransitionState.setState(PipTransitionState.EXITED_PIP);
return true;
}
+ private boolean removePipImmediately(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ startTransaction.apply();
+ finishCallback.onTransitionFinished(null);
+ 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 +447,7 @@ public class PipTransition extends PipTransitionController {
WindowContainerTransaction wct = new WindowContainerTransaction();
wct.movePipActivityToPinnedRootTask(pipTask.token, entryBounds);
+ wct.deferConfigToTransitionEnd(pipTask.token);
return wct;
}
@@ -334,14 +479,71 @@ public class PipTransition extends PipTransitionController {
&& info.getChanges().size() == 1;
}
- /**
- * 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;
+ }
+
+ 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);
+ }
+ }
- private void onExitPip() {
- mPipTaskToken = null;
- mPipScheduler.onExitPip();
+ @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;
+ case PipTransitionState.CHANGED_PIP_BOUNDS:
+ // Note: this might not be the end of the animation, rather animator just finished
+ // adjusting startTx and finishTx and is ready to finishTransition(). The animator
+ // can still continue playing the leash into the destination bounds after.
+ if (mFinishCallback != null) {
+ mFinishCallback.onTransitionFinished(null);
+ mFinishCallback = null;
+ }
+ break;
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
new file mode 100644
index 000000000000..8204d41a9833
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java
@@ -0,0 +1,283 @@
+/*
+ * 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.view.SurfaceControl;
+import android.window.WindowContainerToken;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.internal.util.Preconditions;
+
+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;
+
+ //
+ // 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<>();
+
+ /**
+ * @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;
+ }
+ }
+
+ 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..19af3d544b36 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
@@ -52,7 +52,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,
@@ -120,6 +120,7 @@ public enum ShellProtoLogGroup implements IProtoLogGroup {
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;
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..62d195efb381 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,9 @@ 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);
}
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..c53e7fe00598 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,6 +19,7 @@ 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.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
@@ -26,10 +27,10 @@ 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,11 +50,11 @@ 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;
@@ -86,7 +87,7 @@ public class RecentTasksController implements TaskStackListenerCallback,
private final ActivityTaskManager mActivityTaskManager;
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)
@@ -133,7 +134,7 @@ public class RecentTasksController implements TaskStackListenerCallback,
mShellController = shellController;
mShellCommandHandler = shellCommandHandler;
mActivityTaskManager = activityTaskManager;
- mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
+ mPcFeatureEnabled = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
mTaskStackListener = taskStackListener;
mDesktopModeTaskRepository = desktopModeTaskRepository;
mMainExecutor = mainExecutor;
@@ -252,8 +253,10 @@ public class RecentTasksController implements TaskStackListenerCallback,
notifyRunningTaskVanished(taskInfo);
}
- public void onTaskWindowingModeChanged(TaskInfo taskInfo) {
+ /** Notify listeners that the windowing mode of the given Task was updated. */
+ public void onTaskWindowingModeChanged(ActivityManager.RunningTaskInfo taskInfo) {
notifyRecentTasksChanged();
+ notifyRunningTaskChanged(taskInfo);
}
@Override
@@ -278,7 +281,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 +297,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 +309,28 @@ 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 boolean shouldEnableRunningTasksForDesktopMode() {
+ return mPcFeatureEnabled
+ || (DesktopModeStatus.canEnterDesktopMode(mContext)
+ && enableDesktopWindowingTaskbarRunningApps());
+ }
+
@VisibleForTesting
void registerRecentTasksListener(IRecentTasksListener listener) {
mListener = listener;
@@ -332,6 +361,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 +372,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 +401,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])));
}
@@ -444,6 +483,16 @@ public class RecentTasksController implements TaskStackListenerCallback,
});
});
}
+
+ @Override
+ public void setTransitionBackgroundColor(@Nullable Color color) {
+ mMainExecutor.execute(() -> {
+ if (mTransitionHandler == null) {
+ return;
+ }
+ mTransitionHandler.setTransitionBackgroundColor(color);
+ });
+ }
}
@@ -471,6 +520,11 @@ 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));
+ }
};
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..a7829c905c69 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;
@@ -418,6 +436,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.start", mInstanceId);
if (mListener == null || mTransition == null) {
+ Slog.e(TAG, "Missing listener or transition, hasListener=" + (mListener != null) +
+ " hasTransition=" + (mTransition != null));
cleanUp();
return false;
}
@@ -465,6 +485,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 +561,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 +604,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 +1112,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 +1140,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 +1183,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 +1194,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/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..b9d70e1a599d 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
@@ -90,7 +90,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 +98,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 +138,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 +152,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 +438,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 +487,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 +506,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 +1065,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 +1129,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 +1166,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 +1180,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));
+ }
}
/**
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..6e5b7673e206 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);
@@ -529,6 +537,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 6188e08b8396..4299088a51f0 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
@@ -29,6 +29,7 @@ import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
@@ -47,6 +48,7 @@ import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPos
import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN;
import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
+import static com.android.wm.shell.shared.TransitionUtil.isOpeningMode;
import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
@@ -59,6 +61,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 +69,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 +160,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
@@ -236,6 +241,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 +254,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;
@@ -534,7 +550,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) {
@@ -665,7 +681,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) {
@@ -1286,7 +1302,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 -> {
@@ -1307,7 +1323,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;
}
@@ -1524,6 +1540,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());
@@ -1543,6 +1560,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",
@@ -1620,6 +1638,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.
*/
@@ -1655,6 +1681,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
@@ -1685,6 +1721,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;
@@ -1812,10 +1850,8 @@ 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());
+ 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.
@@ -2672,6 +2708,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();
@@ -2802,7 +2845,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
@@ -2813,7 +2856,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
}
if (mMixedHandler.isEnteringPip(change, transitType)) {
- hasEnteringPip = true;
+ pipChange = change;
}
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
@@ -2856,7 +2899,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
+ " with " + taskInfo.taskId + " before startAnimation().");
record.addRecord(stage, true, taskInfo.taskId);
}
- } else if (isClosingType(change.getMode())) {
+ } else if (change.getMode() == TRANSIT_CLOSE) {
if (stage.containsTask(taskInfo.taskId)) {
record.addRecord(stage, false, taskInfo.taskId);
Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called"
@@ -2865,9 +2908,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;
}
@@ -2902,6 +2956,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
@@ -2915,6 +2970,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
mSplitLayout.update(startTransaction, true /* resetImePosition */);
startTransaction.apply();
}
+ notifySplitAnimationFinished();
return true;
}
}
@@ -3088,7 +3144,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
pendingEnter.mRemoteHandler.onTransitionConsumed(transition,
false /*aborted*/, finishT);
}
- mSplitUnsupportedToast.show();
+ handleUnsupportedSplitStart();
return true;
}
}
@@ -3117,6 +3173,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
final TransitionInfo.Change finalMainChild = mainChild;
final TransitionInfo.Change finalSideChild = sideChild;
enterTransition.setFinishedCallback((callbackWct, callbackT) -> {
+ notifySplitAnimationFinished();
if (finalMainChild != null) {
if (!mainNotContainOpenTask) {
mMainStage.evictOtherChildren(callbackWct, finalMainChild.getTaskInfo().taskId);
@@ -3402,6 +3459,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");
@@ -3533,6 +3595,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.
@@ -3595,7 +3670,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;
}
@@ -3615,7 +3690,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 130babe1d8ea..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;
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/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..11aa402aa283 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;
@@ -48,6 +49,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 +173,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 +201,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 +226,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 +389,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();
}
@@ -597,6 +643,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 +663,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 +689,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 422a2e06a722..bcacecbd8981 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,9 @@ 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 default animation for this mixed transition. */
static final int ANIM_TYPE_DEFAULT = 0;
@@ -116,7 +121,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 +147,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;
@@ -483,9 +488,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);
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..0ada74937df4 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) {
@@ -76,7 +76,12 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
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);
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 9adb67c8a65e..2d6ba6ee7217 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
@@ -594,7 +594,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
@@ -605,8 +604,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);
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..b1a1e5999aa9 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) {
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 5b402a5a7d53..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);
}
}
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/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 274183dd9e2e..6224543516fa 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,10 +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.systemui.shared.Flags.returnAnimationFrameworkLibrary;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
@@ -43,9 +43,11 @@ 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;
@@ -58,10 +60,12 @@ 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);
@@ -177,6 +183,13 @@ public class Transitions implements RemoteCallable<Transitions>,
/** 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;
+
private final ShellTaskOrganizer mOrganizer;
private final Context mContext;
private final ShellExecutor mMainExecutor;
@@ -416,12 +429,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 +520,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 +532,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 +566,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) {
@@ -1181,6 +1209,29 @@ public class Transitions implements RemoteCallable<Transitions>,
}
/**
+ * 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).
@@ -1322,6 +1373,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 +1499,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 +1520,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 +1563,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 +1664,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 +1718,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 fa331af267fa..6adbe4f7ce92 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,7 +16,12 @@
package com.android.wm.shell.transition.tracing;
-import android.internal.perfetto.protos.PerfettoTrace;
+import static android.tracing.perfetto.DataSourceParams.PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT;
+
+import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellHandlerMapping;
+import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellHandlerMappings;
+import android.internal.perfetto.protos.ShellTransitionOuterClass.ShellTransition;
+import android.internal.perfetto.protos.TracePacketOuterClass.TracePacket;
import android.os.SystemClock;
import android.os.Trace;
import android.tracing.perfetto.DataSourceParams;
@@ -44,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_STALL_AND_ABORT)
+ .build();
+ mDataSource.register(params);
}
/**
@@ -72,11 +82,11 @@ public class PerfettoTransitionTracer implements TransitionTracer {
final int handlerId = getHandlerId(handler);
final ProtoOutputStream os = ctx.newTracePacket();
- final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
- os.write(PerfettoTrace.ShellTransition.ID, transitionId);
- os.write(PerfettoTrace.ShellTransition.DISPATCH_TIME_NS,
+ final long token = os.start(TracePacket.SHELL_TRANSITION);
+ os.write(ShellTransition.ID, transitionId);
+ os.write(ShellTransition.DISPATCH_TIME_NS,
SystemClock.elapsedRealtimeNanos());
- os.write(PerfettoTrace.ShellTransition.HANDLER, handlerId);
+ os.write(ShellTransition.HANDLER, handlerId);
os.end(token);
});
}
@@ -117,11 +127,11 @@ public class PerfettoTransitionTracer implements TransitionTracer {
private void doLogMergeRequested(int mergeRequestedTransitionId, int playingTransitionId) {
mDataSource.trace(ctx -> {
final ProtoOutputStream os = ctx.newTracePacket();
- final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
- os.write(PerfettoTrace.ShellTransition.ID, mergeRequestedTransitionId);
- os.write(PerfettoTrace.ShellTransition.MERGE_REQUEST_TIME_NS,
+ final long token = os.start(TracePacket.SHELL_TRANSITION);
+ os.write(ShellTransition.ID, mergeRequestedTransitionId);
+ os.write(ShellTransition.MERGE_REQUEST_TIME_NS,
SystemClock.elapsedRealtimeNanos());
- os.write(PerfettoTrace.ShellTransition.MERGE_TARGET, playingTransitionId);
+ os.write(ShellTransition.MERGE_TARGET, playingTransitionId);
os.end(token);
});
}
@@ -149,11 +159,11 @@ public class PerfettoTransitionTracer implements TransitionTracer {
private void doLogMerged(int mergeRequestedTransitionId, int playingTransitionId) {
mDataSource.trace(ctx -> {
final ProtoOutputStream os = ctx.newTracePacket();
- final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
- os.write(PerfettoTrace.ShellTransition.ID, mergeRequestedTransitionId);
- os.write(PerfettoTrace.ShellTransition.MERGE_TIME_NS,
+ final long token = os.start(TracePacket.SHELL_TRANSITION);
+ os.write(ShellTransition.ID, mergeRequestedTransitionId);
+ os.write(ShellTransition.MERGE_TIME_NS,
SystemClock.elapsedRealtimeNanos());
- os.write(PerfettoTrace.ShellTransition.MERGE_TARGET, playingTransitionId);
+ os.write(ShellTransition.MERGE_TARGET, playingTransitionId);
os.end(token);
});
}
@@ -180,9 +190,9 @@ public class PerfettoTransitionTracer implements TransitionTracer {
private void doLogAborted(int transitionId) {
mDataSource.trace(ctx -> {
final ProtoOutputStream os = ctx.newTracePacket();
- final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION);
- os.write(PerfettoTrace.ShellTransition.ID, transitionId);
- os.write(PerfettoTrace.ShellTransition.SHELL_ABORT_TIME_NS,
+ final long token = os.start(TracePacket.SHELL_TRANSITION);
+ os.write(ShellTransition.ID, transitionId);
+ os.write(ShellTransition.SHELL_ABORT_TIME_NS,
SystemClock.elapsedRealtimeNanos());
os.end(token);
});
@@ -196,20 +206,18 @@ public class PerfettoTransitionTracer implements TransitionTracer {
mDataSource.trace(ctx -> {
final ProtoOutputStream os = ctx.newTracePacket();
- final long mappingsToken = os.start(PerfettoTrace.TracePacket.SHELL_HANDLER_MAPPINGS);
+ final long mappingsToken = os.start(TracePacket.SHELL_HANDLER_MAPPINGS);
for (Map.Entry<String, Integer> entry : mHandlerMapping.entrySet()) {
final String handler = entry.getKey();
final int handlerId = entry.getValue();
- final long mappingEntryToken = os.start(PerfettoTrace.ShellHandlerMappings.MAPPING);
- os.write(PerfettoTrace.ShellHandlerMapping.ID, handlerId);
- os.write(PerfettoTrace.ShellHandlerMapping.NAME, handler);
+ final long mappingEntryToken = os.start(ShellHandlerMappings.MAPPING);
+ os.write(ShellHandlerMapping.ID, handlerId);
+ os.write(ShellHandlerMapping.NAME, handler);
os.end(mappingEntryToken);
}
os.end(mappingsToken);
-
- ctx.flush();
});
}
}
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..e85cb6400000 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);
@@ -229,7 +279,10 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
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 +339,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 +356,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..6671391efdeb 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;
@@ -87,13 +94,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 +200,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 +227,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
mHandler,
mChoreographer,
mDisplay.getDisplayId(),
- 0 /* taskCornerRadius */,
mDecorationContainerSurface,
mDragPositioningCallback,
mSurfaceControlBuilderSupplier,
@@ -230,12 +238,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..9afb057ffbe5 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;
@@ -80,10 +84,12 @@ 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.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;
@@ -119,9 +125,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 +134,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;
@@ -197,7 +201,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
new DesktopModeWindowDecoration.Factory(),
new InputMonitorFactory(),
SurfaceControl.Transaction::new,
- rootTaskDisplayAreaOrganizer);
+ rootTaskDisplayAreaOrganizer,
+ new SparseArray<>());
}
@VisibleForTesting
@@ -219,7 +224,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 +237,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 +245,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mTransactionFactory = transactionFactory;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
mInputManager = mContext.getSystemService(InputManager.class);
+ mWindowDecorByTaskId = windowDecorByTaskId;
shellInit.addInitCallback(this::onInit, this);
}
@@ -248,8 +255,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,12 +276,13 @@ 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)
+ || decor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+ return;
}
+ mDesktopTasksController.moveToSplit(decor.mTaskInfo);
}
}
});
@@ -305,6 +313,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 +364,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 +372,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;
@@ -402,7 +429,9 @@ 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();
@@ -414,13 +443,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
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);
decoration.closeHandleMenu();
} else if (id == R.id.fullscreen_button) {
decoration.closeHandleMenu();
@@ -428,42 +455,31 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
mSplitScreenController.moveTaskToFullscreen(mTaskId,
SplitScreenController.EXIT_REASON_DESKTOP_MODE);
} else {
- mDesktopTasksController.ifPresent(c ->
- c.moveToFullscreen(mTaskId));
+ mDesktopTasksController.moveToFullscreen(mTaskId);
}
} 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 +511,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 +524,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,8 +575,14 @@ 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);
} else if (ev.getAction() == ACTION_HOVER_EXIT) {
if (!decoration.isMaximizeMenuActive() && id == R.id.maximize_window) {
decoration.onMaximizeWindowHoverExit();
@@ -564,6 +592,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
// menu view to another.
mMainHandler.postDelayed(mCloseMaximizeWindowRunnable,
CLOSE_MAXIMIZE_MENU_DELAY_MS);
+ } else if (MaximizeMenu.Companion.isMaximizeMenuView(id)) {
+ decoration.onMaximizeMenuHoverExit(id, ev);
}
return true;
}
@@ -572,7 +602,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 +614,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 +636,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 +646,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 +671,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 +701,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 +791,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 +800,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 +820,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 +837,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 +934,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 +1029,15 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
&& taskInfo.isFocused) {
return false;
}
- return DesktopModeStatus.isEnabled()
+ if (Flags.enableDesktopWindowingModalsPolicy()
+ && isSingleTopActivityTranslucent(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 +1062,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);
}
@@ -1125,7 +1106,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
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 +1163,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 +1207,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..4d4dc3c72420 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;
@@ -54,8 +66,8 @@ 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.windowdecor.extension.TaskInfoKt;
import com.android.wm.shell.windowdecor.viewholder.DesktopModeAppControlsWindowDecorationViewHolder;
import com.android.wm.shell.windowdecor.viewholder.DesktopModeFocusedWindowDecorationViewHolder;
@@ -97,9 +109,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;
@@ -144,13 +156,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
surfaceControlBuilderSupplier, surfaceControlTransactionSupplier,
windowContainerTransactionSupplier, surfaceControlSupplier,
surfaceControlViewHostFactory);
-
mHandler = handler;
mChoreographer = choreographer;
mSyncQueue = syncQueue;
mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer;
-
- loadAppInfo();
}
void setCaptionListeners(
@@ -196,6 +205,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
void relayout(ActivityManager.RunningTaskInfo taskInfo,
SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) {
+ Trace.beginSection("DesktopModeWindowDecoration#relayout");
if (isHandleMenuActive()) {
mHandleMenu.relayout(startT);
}
@@ -207,16 +217,22 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
final WindowContainerTransaction wct = new WindowContainerTransaction();
+ Trace.beginSection("DesktopModeWindowDecoration#relayout-inner");
relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult);
+ Trace.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#relayout
return;
}
+
if (oldRootView != mResult.mRootView) {
if (mRelayoutParams.mLayoutResId == R.layout.desktop_mode_focused_window_decor) {
mWindowDecorViewHolder = new DesktopModeFocusedWindowDecorationViewHolder(
@@ -226,6 +242,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
);
} else if (mRelayoutParams.mLayoutResId
== R.layout.desktop_mode_app_controls_window_decor) {
+ loadAppInfoIfNeeded();
mWindowDecorViewHolder = new DesktopModeAppControlsWindowDecorationViewHolder(
mResult.mRootView,
mOnCaptionTouchListener,
@@ -244,7 +261,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
throw new IllegalArgumentException("Unexpected layout resource id");
}
}
+ Trace.beginSection("DesktopModeWindowDecoration#relayout-binding");
mWindowDecorViewHolder.bindData(mTaskInfo);
+ Trace.endSection();
if (!mTaskInfo.isFocused) {
closeHandleMenu();
@@ -260,37 +279,37 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
updateExclusionRegion();
}
closeDragResizeListener();
+ Trace.endSection(); // DesktopModeWindowDecoration#relayout
return;
}
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();
}
@@ -302,6 +321,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(), startT);
}
}
+ Trace.endSection(); // DesktopModeWindowDecoration#relayout
}
@VisibleForTesting
@@ -318,11 +338,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
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 (captionLayoutId == R.layout.desktop_mode_app_controls_window_decor) {
+ 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 +359,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 (captionLayoutId == R.layout.desktop_mode_focused_window_decor) {
+ // 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
@@ -399,7 +426,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 +447,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,15 +505,18 @@ 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, mTaskInfo,
+ mTaskSurface, mSurfaceControlTransactionSupplier);
}
/**
* Show the resize veil.
*/
public void showResizeVeil(Rect taskBounds) {
+ createResizeVeilIfNeeded();
mResizeVeil.showVeil(mTaskSurface, taskBounds);
}
@@ -464,6 +524,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
* Show the resize veil.
*/
public void showResizeVeil(SurfaceControl.Transaction tx, Rect taskBounds) {
+ createResizeVeilIfNeeded();
mResizeVeil.showVeil(tx, mTaskSurface, taskBounds, false /* fadeIn */);
}
@@ -498,6 +559,7 @@ 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)
mWindowDecorViewHolder).getAppNameTextWidth();
@@ -582,13 +644,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
* Create and display handle menu window.
*/
void createHandleMenu() {
+ 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)
.build();
mWindowDecorViewHolder.onHandleMenuOpened();
@@ -606,10 +669,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
}
@Override
- void releaseViews() {
+ void releaseViews(WindowContainerTransaction wct) {
closeHandleMenu();
closeMaximizeMenu();
- super.releaseViews();
+ super.releaseViews(wct);
}
/**
@@ -706,27 +769,50 @@ 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()) {
+ mHandleMenu.checkMotionEvent(ev);
+ }
+ }
+
private boolean pointInView(View v, float x, float y) {
return v != null && v.getLeft() <= x && v.getRight() >= x
&& v.getTop() <= y && v.getBottom() >= y;
@@ -764,7 +850,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();
@@ -801,16 +887,34 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
.setAnimatingTaskResize(animatingTaskResize);
}
+ /** Called when there is a {@Link ACTION_HOVER_EXIT} on the maximize window button. */
void onMaximizeWindowHoverExit() {
((DesktopModeAppControlsWindowDecorationViewHolder) mWindowDecorViewHolder)
.onMaximizeWindowHoverExit();
}
+ /** Called when there is a {@Link ACTION_HOVER_ENTER} on the maximize window button. */
void onMaximizeWindowHoverEnter() {
((DesktopModeAppControlsWindowDecorationViewHolder) 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 "{"
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..82c399ad8152 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
@@ -131,7 +131,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,49 +140,32 @@ public class DragPositioningCallbackUtility {
}
/**
- * Calculates the new position of the top edge of the task and returns true if it is below the
- * disallowed 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
+ * If task bounds are outside of provided drag area, snap the bounds to be just inside the
+ * drag area.
+ * @param repositionTaskBounds bounds determined by task positioner
+ * @param validDragArea the area that task must be positioned inside
+ * @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,
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..5379ca6cd51d 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
@@ -24,13 +24,15 @@ 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 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 +40,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 +54,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 +70,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 +91,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,18 +114,19 @@ 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)
@@ -170,7 +150,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 +161,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 +195,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 +210,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 +237,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 +266,37 @@ 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) {
+ 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 +309,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 +388,29 @@ 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;
+ boolean isTouch = isTouchEvent(e);
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, isTouch,
+ new Point() /* offset */);
if (mShouldHandleEvents) {
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);
+ int ctrlType = mDragResizeWindowGeometry.calculateCtrlType(isTouch, 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;
}
@@ -447,7 +429,6 @@ class DragResizeInputListener implements AutoCloseable {
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
- mInputManager.pilferPointers(mInputChannel.getToken());
if (mShouldHandleEvents) {
int dragPointerIndex = e.findPointerIndex(mDragPointerId);
final Rect taskBounds = mCallback.onDragPositioningEnd(
@@ -455,7 +436,7 @@ class DragResizeInputListener implements AutoCloseable {
// 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 +461,20 @@ 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);
+ @DragPositioningCallback.CtrlType int ctrlType =
+ mDragResizeWindowGeometry.calculateCtrlType(/* isTouch= */ false, x, y);
int cursorType = PointerIcon.TYPE_DEFAULT;
switch (ctrlType) {
@@ -629,14 +505,20 @@ 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);
+ }
+
+ private boolean isTouchEvent(MotionEvent e) {
+ return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
new file mode 100644
index 000000000000..4f513f0a0fd8
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometry.java
@@ -0,0 +1,522 @@
+/*
+ * 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) {
+ return shouldHandleEvent(e, isTouchEvent(e), offset);
+ }
+
+ /**
+ * Returns if this MotionEvent should be handled, based on its source and position.
+ */
+ boolean shouldHandleEvent(@NonNull MotionEvent e, boolean isTouch, @NonNull Point offset) {
+ final float x = e.getX(0) + offset.x;
+ final float y = e.getY(0) + offset.y;
+
+ if (enableWindowingEdgeDragResize()) {
+ // First check if touch falls within a corner.
+ // Large corner bounds are used for course input like touch, otherwise fine bounds.
+ boolean result = isTouch
+ ? isInCornerBounds(mLargeTaskCorners, x, y)
+ : isInCornerBounds(mFineTaskCorners, x, y);
+ // Check if touch falls within the edge resize handle, since edge resizing can apply
+ // for any input source.
+ if (!result) {
+ result = isInEdgeResizeBounds(x, y);
+ }
+ return result;
+ } else {
+ // Legacy uses only fine corners for touch, and edges only for non-touch input.
+ return isTouch
+ ? isInCornerBounds(mFineTaskCorners, x, y)
+ : isInEdgeResizeBounds(x, y);
+ }
+ }
+
+ private boolean isTouchEvent(@NonNull MotionEvent e) {
+ return (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
+ }
+
+ 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.
+ */
+ @DragPositioningCallback.CtrlType
+ int calculateCtrlType(boolean isTouch, float x, float y) {
+ if (enableWindowingEdgeDragResize()) {
+ // First check if touch falls within a corner.
+ // Large corner bounds are used for course input like touch, otherwise fine bounds.
+ int ctrlType = isTouch
+ ? 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) {
+ ctrlType = calculateEdgeResizeCtrlType(x, y);
+ }
+ return ctrlType;
+ } else {
+ // Legacy uses only fine corners for touch, and edges only for non-touch input.
+ return isTouch
+ ? 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..65adcee1567c 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,8 @@ 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 android.annotation.NonNull;
import android.annotation.Nullable;
@@ -35,7 +37,6 @@ import android.graphics.PointF;
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;
@@ -53,6 +54,7 @@ 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;
@@ -140,7 +142,8 @@ 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);
@@ -172,9 +175,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 +188,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);
+ }
}
/**
@@ -245,24 +246,31 @@ class HandleMenu {
}
/**
- * 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) {
+ void checkMotionEvent(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);
+ 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();
}
}
+ // 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.
@@ -305,12 +313,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;
}
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..7898567b70e9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuImageButton.kt
@@ -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.windowdecor
+
+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.
+ */
+class HandleMenuImageButton(context: Context?, attrs: AttributeSet?) :
+ ImageButton(context, attrs) {
+ override fun onHoverEvent(motionEvent: MotionEvent): Boolean {
+ return false
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt
index b82f7ca47ef3..22f0adc42f5d 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,6 +16,9 @@
package com.android.wm.shell.windowdecor
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
import android.annotation.IdRes
import android.app.ActivityManager.RunningTaskInfo
import android.content.Context
@@ -30,12 +33,21 @@ import android.view.SurfaceControlViewHost
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.FrameLayout
+import android.widget.LinearLayout
+import android.widget.TextView
import android.window.TaskConstants
+import androidx.core.content.withStyledAttributes
+import com.android.internal.R.attr.colorAccentPrimary
import 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
@@ -61,14 +73,19 @@ class MaximizeMenu(
private var maximizeMenu: AdditionalWindow? = 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 openMenuAnimatorSet = AnimatorSet()
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)
+
+ private lateinit var snapRightButton: Button
+ private lateinit var snapLeftButton: Button
+ private lateinit var maximizeButton: Button
+ private lateinit var maximizeButtonLayout: FrameLayout
+ private lateinit var snapButtonsLayout: LinearLayout
/** Position the menu relative to the caption's position. */
fun positionMenu(position: PointF, t: Transaction) {
@@ -81,10 +98,12 @@ class MaximizeMenu(
if (maximizeMenu != null) return
createMaximizeMenu()
setupMaximizeMenu()
+ animateOpenMenu()
}
/** Closes the maximize window and releases its view. */
fun close() {
+ openMenuAnimatorSet.cancel()
maximizeMenu?.releaseView()
maximizeMenu = null
}
@@ -124,8 +143,6 @@ class MaximizeMenu(
// 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)
@@ -136,6 +153,77 @@ class MaximizeMenu(
}
}
+ private fun animateOpenMenu() {
+ val viewHost = maximizeMenu?.mWindowViewHost
+ val maximizeMenuView = viewHost?.view ?: return
+ val maximizeWindowText = maximizeMenuView.requireViewById<TextView>(
+ R.id.maximize_menu_maximize_window_text)
+ val snapWindowText = maximizeMenuView.requireViewById<TextView>(
+ R.id.maximize_menu_snap_window_text)
+
+ openMenuAnimatorSet.playTogether(
+ ObjectAnimator.ofFloat(maximizeMenuView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f)
+ .apply {
+ duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+ interpolator = EMPHASIZED_DECELERATE
+ },
+ ValueAnimator.ofFloat(STARTING_MENU_HEIGHT_SCALE, 1f)
+ .apply {
+ duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+ interpolator = EMPHASIZED_DECELERATE
+ addUpdateListener {
+ // Animate padding so that controls stay pinned to the bottom of
+ // the menu.
+ val value = animatedValue as Float
+ val topPadding = menuPadding -
+ ((1 - value) * menuHeight).toInt()
+ maximizeMenuView.setPadding(menuPadding, topPadding,
+ menuPadding, menuPadding)
+ }
+ },
+ ValueAnimator.ofFloat(1 / STARTING_MENU_HEIGHT_SCALE, 1f).apply {
+ duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+ interpolator = EMPHASIZED_DECELERATE
+ addUpdateListener {
+ // Scale up the children of the maximize menu so that the menu
+ // scale is cancelled out and only the background is scaled.
+ val value = animatedValue as Float
+ maximizeButtonLayout.scaleY = value
+ snapButtonsLayout.scaleY = value
+ maximizeWindowText.scaleY = value
+ snapWindowText.scaleY = value
+ }
+ },
+ ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Y,
+ (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply {
+ duration = MENU_HEIGHT_ANIMATION_DURATION_MS
+ interpolator = EMPHASIZED_DECELERATE
+ },
+ ObjectAnimator.ofInt(maximizeMenuView.background, "alpha",
+ MAX_DRAWABLE_ALPHA_VALUE).apply {
+ duration = ALPHA_ANIMATION_DURATION_MS
+ },
+ ValueAnimator.ofFloat(0f, 1f)
+ .apply {
+ duration = ALPHA_ANIMATION_DURATION_MS
+ startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
+ addUpdateListener {
+ val value = animatedValue as Float
+ maximizeButtonLayout.alpha = value
+ snapButtonsLayout.alpha = value
+ maximizeWindowText.alpha = value
+ snapWindowText.alpha = value
+ }
+ },
+ ObjectAnimator.ofFloat(maximizeMenuView, TRANSLATION_Z, MENU_Z_TRANSLATION)
+ .apply {
+ duration = ELEVATION_ANIMATION_DURATION_MS
+ startDelay = CONTROLS_ALPHA_ANIMATION_DELAY_MS
+ }
+ )
+ openMenuAnimatorSet.start()
+ }
+
private fun loadDimensionPixelSize(resourceId: Int): Int {
return if (resourceId == Resources.ID_NULL) {
0
@@ -150,23 +238,23 @@ class MaximizeMenu(
maximizeMenuView.setOnGenericMotionListener(onGenericMotionListener)
maximizeMenuView.setOnTouchListener(onTouchListener)
- val maximizeButton = maximizeMenuView.requireViewById<Button>(
- R.id.maximize_menu_maximize_button
- )
+ maximizeButtonLayout = maximizeMenuView.requireViewById(
+ R.id.maximize_menu_maximize_button_layout)
+
+ maximizeButton = maximizeMenuView.requireViewById(R.id.maximize_menu_maximize_button)
maximizeButton.setOnClickListener(onClickListener)
maximizeButton.setOnGenericMotionListener(onGenericMotionListener)
- val snapRightButton = maximizeMenuView.requireViewById<Button>(
- R.id.maximize_menu_snap_right_button
- )
+ snapRightButton = maximizeMenuView.requireViewById(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 = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_left_button)
snapLeftButton.setOnClickListener(onClickListener)
snapLeftButton.setOnGenericMotionListener(onGenericMotionListener)
+
+ snapButtonsLayout = maximizeMenuView.requireViewById(R.id.maximize_menu_snap_menu_layout)
+ snapButtonsLayout.setOnGenericMotionListener(onGenericMotionListener)
}
/**
@@ -190,11 +278,85 @@ class MaximizeMenu(
return maximizeMenu?.mWindowViewHost?.view?.isLaidOut ?: false
}
+ fun onMaximizeMenuHoverEnter(viewId: Int, ev: MotionEvent) {
+ setSnapButtonsColorOnHover(viewId, ev)
+ }
+
+ fun onMaximizeMenuHoverMove(viewId: Int, ev: MotionEvent) {
+ setSnapButtonsColorOnHover(viewId, ev)
+ }
+
+ fun onMaximizeMenuHoverExit(id: Int, ev: MotionEvent) {
+ val inSnapMenuBounds = ev.x >= 0 && ev.x <= snapButtonsLayout.width &&
+ ev.y >= 0 && ev.y <= snapButtonsLayout.height
+ val colorList = decorWindowContext.getColorStateList(
+ R.color.desktop_mode_maximize_menu_button_color_selector)
+
+ if (id == R.id.maximize_menu_maximize_button) {
+ maximizeButton.background?.setTintList(colorList)
+ maximizeButtonLayout.setBackgroundResource(
+ R.drawable.desktop_mode_maximize_menu_layout_background)
+ } else if (id == R.id.maximize_menu_snap_menu_layout && !inSnapMenuBounds) {
+ // After exiting the snap menu layout area, checks to see that user is not still
+ // hovering within the snap menu layout bounds which would indicate that the user is
+ // hovering over a snap button within the snap menu layout rather than having exited.
+ snapLeftButton.background?.setTintList(colorList)
+ snapLeftButton.background?.alpha = 255
+ snapRightButton.background?.setTintList(colorList)
+ snapRightButton.background?.alpha = 255
+ snapButtonsLayout.setBackgroundResource(
+ R.drawable.desktop_mode_maximize_menu_layout_background)
+ }
+ }
+
+ private fun setSnapButtonsColorOnHover(viewId: Int, ev: MotionEvent) {
+ decorWindowContext.withStyledAttributes(null, intArrayOf(colorAccentPrimary), 0, 0) {
+ val materialColor = getColor(0, 0)
+ val snapMenuCenter = snapButtonsLayout.width / 2
+ if (viewId == R.id.maximize_menu_maximize_button) {
+ // Highlight snap maximize window button
+ maximizeButton.background?.setTint(materialColor)
+ maximizeButtonLayout.setBackgroundResource(
+ R.drawable.desktop_mode_maximize_menu_layout_background_on_hover)
+ } else if (viewId == R.id.maximize_menu_snap_left_button ||
+ (viewId == R.id.maximize_menu_snap_menu_layout && ev.x <= snapMenuCenter)) {
+ // Highlight snap left button
+ snapRightButton.background?.setTint(materialColor)
+ snapLeftButton.background?.setTint(materialColor)
+ snapButtonsLayout.setBackgroundResource(
+ R.drawable.desktop_mode_maximize_menu_layout_background_on_hover)
+ snapRightButton.background?.alpha = 102
+ snapLeftButton.background?.alpha = 255
+ } else if (viewId == R.id.maximize_menu_snap_right_button ||
+ (viewId == R.id.maximize_menu_snap_menu_layout && ev.x > snapMenuCenter)) {
+ // Highlight snap right button
+ snapRightButton.background?.setTint(materialColor)
+ snapLeftButton.background?.setTint(materialColor)
+ snapButtonsLayout.setBackgroundResource(
+ R.drawable.desktop_mode_maximize_menu_layout_background_on_hover)
+ snapRightButton.background?.alpha = 255
+ snapLeftButton.background?.alpha = 102
+ }
+ }
+ }
+
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_maximize_button_layout ||
viewId == R.id.maximize_menu_snap_left_button ||
- viewId == R.id.maximize_menu_snap_right_button
+ viewId == R.id.maximize_menu_snap_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..74499c7e429e 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
@@ -31,17 +31,23 @@ 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()
+ 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 +83,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
index b0d3b5090ef0..93e2a21c6b02 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java
@@ -20,16 +20,21 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.ColorRes;
+import android.annotation.NonNull;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.Color;
import android.graphics.PixelFormat;
+import android.graphics.PointF;
import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
+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.View;
import android.view.WindowManager;
import android.view.WindowlessWindowManager;
@@ -37,6 +42,7 @@ import android.widget.ImageView;
import android.window.TaskConstants;
import com.android.wm.shell.R;
+import com.android.wm.shell.common.DisplayController;
import java.util.function.Supplier;
@@ -44,63 +50,147 @@ import java.util.function.Supplier;
* Creates and updates a veil that covers task contents on resize.
*/
public class ResizeVeil {
+ private static final String TAG = "ResizeVeil";
private static final int RESIZE_ALPHA_DURATION = 100;
+
+ private static final int 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 static final int VEIL_BACKGROUND_LAYER = 0;
+ /** The icon is a child of the veil container layer and goes in front of the background. */
+ private static final int VEIL_ICON_LAYER = 1;
+
private final Context mContext;
- private final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier;
+ private final DisplayController mDisplayController;
private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier;
- private final Drawable mAppIcon;
+ private final SurfaceControlBuilderFactory mSurfaceControlBuilderFactory;
+ private final WindowDecoration.SurfaceControlViewHostFactory mSurfaceControlViewHostFactory;
+ private final SurfaceSession mSurfaceSession = new SurfaceSession();
+ private final Bitmap mAppIcon;
private ImageView mIconView;
+ private int mIconSize;
private SurfaceControl mParentSurface;
+
+ /** A container surface to host the veil background and icon child surfaces. */
private SurfaceControl mVeilSurface;
+ /** A color surface for the veil background. */
+ private SurfaceControl mBackgroundSurface;
+ /** A surface that hosts a windowless window with the app icon. */
+ private SurfaceControl mIconSurface;
+
private final RunningTaskInfo mTaskInfo;
private SurfaceControlViewHost mViewHost;
- private final Display mDisplay;
+ private Display mDisplay;
private ValueAnimator mVeilAnimator;
- public ResizeVeil(Context context, Drawable appIcon, RunningTaskInfo taskInfo,
- Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, Display display,
+ private boolean mIsShowing = false;
+
+ private final DisplayController.OnDisplaysChangedListener mOnDisplaysChangedListener =
+ new DisplayController.OnDisplaysChangedListener() {
+ @Override
+ public void onDisplayAdded(int displayId) {
+ if (mTaskInfo.displayId != displayId) {
+ return;
+ }
+ mDisplayController.removeDisplayWindowListener(this);
+ setupResizeVeil();
+ }
+ };
+
+ public ResizeVeil(Context context,
+ @NonNull DisplayController displayController,
+ Bitmap appIcon, RunningTaskInfo taskInfo,
+ SurfaceControl taskSurface,
Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier) {
+ this(context,
+ displayController,
+ appIcon,
+ taskInfo,
+ taskSurface,
+ surfaceControlTransactionSupplier,
+ new SurfaceControlBuilderFactory() {},
+ new WindowDecoration.SurfaceControlViewHostFactory() {});
+ }
+
+ public ResizeVeil(Context context,
+ @NonNull DisplayController displayController,
+ Bitmap appIcon, RunningTaskInfo taskInfo,
+ SurfaceControl taskSurface,
+ Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
+ SurfaceControlBuilderFactory surfaceControlBuilderFactory,
+ WindowDecoration.SurfaceControlViewHostFactory surfaceControlViewHostFactory) {
mContext = context;
+ mDisplayController = displayController;
mAppIcon = appIcon;
- mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier;
mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
mTaskInfo = taskInfo;
- mDisplay = display;
+ mParentSurface = taskSurface;
+ mSurfaceControlBuilderFactory = surfaceControlBuilderFactory;
+ mSurfaceControlViewHostFactory = surfaceControlViewHostFactory;
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)
+ if (!obtainDisplayOrRegisterListener()) {
+ // Display may not be available yet, skip this until then.
+ return;
+ }
+ Trace.beginSection("ResizeVeil#setupResizeVeil");
+ mVeilSurface = mSurfaceControlBuilderFactory
+ .create("Resize veil of Task=" + mTaskInfo.taskId)
.setContainerLayer()
+ .setHidden(true)
+ .setParent(mParentSurface)
+ .setCallsite("ResizeVeil#setupResizeVeil")
+ .build();
+ mBackgroundSurface = mSurfaceControlBuilderFactory
+ .create("Resize veil background of Task=" + mTaskInfo.taskId, mSurfaceSession)
+ .setColorLayer()
+ .setHidden(true)
+ .setParent(mVeilSurface)
+ .setCallsite("ResizeVeil#setupResizeVeil")
.build();
- View v = LayoutInflater.from(mContext)
- .inflate(R.layout.desktop_mode_resize_veil, null);
+ mIconSurface = mSurfaceControlBuilderFactory
+ .create("Resize veil icon of Task=" + mTaskInfo.taskId)
+ .setContainerLayer()
+ .setHidden(true)
+ .setParent(mVeilSurface)
+ .setCallsite("ResizeVeil#setupResizeVeil")
+ .build();
+
+ mIconSize = mContext.getResources()
+ .getDimensionPixelSize(R.dimen.desktop_mode_resize_veil_icon_size);
+ final View root = LayoutInflater.from(mContext)
+ .inflate(R.layout.desktop_mode_resize_veil, null /* root */);
+ mIconView = root.findViewById(R.id.veil_application_icon);
+ mIconView.setImageBitmap(mAppIcon);
- 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(),
+ new WindowManager.LayoutParams(
+ mIconSize,
+ mIconSize,
WindowManager.LayoutParams.TYPE_APPLICATION,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSPARENT);
- lp.setTitle("Resize veil of Task=" + mTaskInfo.taskId);
+ lp.setTitle("Resize veil icon window 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);
+ final WindowlessWindowManager wwm = new WindowlessWindowManager(mTaskInfo.configuration,
+ mIconSurface, null /* hostInputToken */);
+
+ mViewHost = mSurfaceControlViewHostFactory.create(mContext, mDisplay, wwm, "ResizeVeil");
+ mViewHost.setView(root, lp);
+ Trace.endSection();
+ }
+
+ private boolean obtainDisplayOrRegisterListener() {
+ mDisplay = mDisplayController.getDisplay(mTaskInfo.displayId);
+ if (mDisplay == null) {
+ mDisplayController.addDisplayWindowListener(mOnDisplaysChangedListener);
+ return false;
+ }
+ return true;
}
/**
@@ -114,58 +204,95 @@ public class ResizeVeil {
*/
public void showVeil(SurfaceControl.Transaction t, SurfaceControl parentSurface,
Rect taskBounds, boolean fadeIn) {
+ if (!isReady() || isVisible()) {
+ t.apply();
+ return;
+ }
+ mIsShowing = true;
+
// 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));
+ t.show(mVeilSurface);
+ t.setLayer(mVeilSurface, VEIL_CONTAINER_LAYER);
+ t.setLayer(mIconSurface, VEIL_ICON_LAYER);
+ t.setLayer(mBackgroundSurface, VEIL_BACKGROUND_LAYER);
+ t.setColor(mBackgroundSurface,
+ Color.valueOf(mContext.getColor(getBackgroundColorId())).getComponents());
relayout(taskBounds, t);
if (fadeIn) {
cancelAnimation();
+ final SurfaceControl.Transaction veilAnimT = mSurfaceControlTransactionSupplier.get();
mVeilAnimator = new ValueAnimator();
mVeilAnimator.setFloatValues(0f, 1f);
mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION);
mVeilAnimator.addUpdateListener(animation -> {
- t.setAlpha(mVeilSurface, mVeilAnimator.getAnimatedFraction());
- t.apply();
+ veilAnimT.setAlpha(mBackgroundSurface, mVeilAnimator.getAnimatedFraction());
+ veilAnimT.apply();
});
mVeilAnimator.addListener(new AnimatorListenerAdapter() {
@Override
+ public void onAnimationStart(Animator animation) {
+ veilAnimT.show(mBackgroundSurface)
+ .setAlpha(mBackgroundSurface, 0)
+ .apply();
+ }
+
+ @Override
public void onAnimationEnd(Animator animation) {
- t.setAlpha(mVeilSurface, 1);
- t.apply();
+ veilAnimT.setAlpha(mBackgroundSurface, 1).apply();
}
});
+ final SurfaceControl.Transaction iconAnimT = mSurfaceControlTransactionSupplier.get();
final ValueAnimator iconAnimator = new ValueAnimator();
iconAnimator.setFloatValues(0f, 1f);
iconAnimator.setDuration(RESIZE_ALPHA_DURATION);
iconAnimator.addUpdateListener(animation -> {
- mIconView.setAlpha(animation.getAnimatedFraction());
+ iconAnimT.setAlpha(mIconSurface, animation.getAnimatedFraction());
+ iconAnimT.apply();
+ });
+ iconAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ iconAnimT.show(mIconSurface)
+ .setAlpha(mIconSurface, 0)
+ .apply();
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ iconAnimT.setAlpha(mIconSurface, 1).apply();
+ }
});
+ // Let the animators show it with the correct alpha value once the animation starts.
+ t.hide(mIconSurface);
+ t.hide(mBackgroundSurface);
+ t.apply();
- t.show(mVeilSurface)
- .addTransactionCommittedListener(
- mContext.getMainExecutor(), () -> {
- mVeilAnimator.start();
- iconAnimator.start();
- })
- .setAlpha(mVeilSurface, 0);
+ mVeilAnimator.start();
+ iconAnimator.start();
} else {
- // Show the veil immediately at full opacity.
- t.show(mVeilSurface).setAlpha(mVeilSurface, 1);
+ // Show the veil immediately.
+ t.show(mIconSurface);
+ t.show(mBackgroundSurface);
+ t.setAlpha(mIconSurface, 1);
+ t.setAlpha(mBackgroundSurface, 1);
+ t.apply();
}
- mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t);
}
/**
* Animate veil's alpha to 1, fading it in.
*/
public void showVeil(SurfaceControl parentSurface, Rect taskBounds) {
+ if (!isReady() || isVisible()) {
+ return;
+ }
SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
showVeil(t, parentSurface, taskBounds, true /* fadeIn */);
}
@@ -175,8 +302,9 @@ public class ResizeVeil {
* @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());
+ final PointF iconPosition = calculateAppIconPosition(newBounds);
+ t.setPosition(mIconSurface, iconPosition.x, iconPosition.y);
t.setPosition(mParentSurface, newBounds.left, newBounds.top);
t.setWindowCrop(mParentSurface, newBounds.width(), newBounds.height());
}
@@ -186,6 +314,9 @@ public class ResizeVeil {
* @param newBounds bounds to update veil to.
*/
public void updateResizeVeil(Rect newBounds) {
+ if (!isVisible()) {
+ return;
+ }
SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
updateResizeVeil(t, newBounds);
}
@@ -199,36 +330,46 @@ public class ResizeVeil {
* @param newBounds bounds to update veil to.
*/
public void updateResizeVeil(SurfaceControl.Transaction t, Rect newBounds) {
+ if (!isVisible()) {
+ t.apply();
+ return;
+ }
if (mVeilAnimator != null && mVeilAnimator.isStarted()) {
mVeilAnimator.removeAllUpdateListeners();
mVeilAnimator.end();
}
relayout(newBounds, t);
- mViewHost.getView().getViewRootImpl().applyTransactionOnDraw(t);
+ t.apply();
}
/**
* Animate veil's alpha to 0, fading it out.
*/
public void hideVeil() {
+ if (!isVisible()) {
+ return;
+ }
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.setAlpha(mBackgroundSurface, 1 - mVeilAnimator.getAnimatedFraction());
+ t.setAlpha(mIconSurface, 1 - mVeilAnimator.getAnimatedFraction());
t.apply();
});
mVeilAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
- t.hide(mVeilSurface);
+ t.hide(mBackgroundSurface);
+ t.hide(mIconSurface);
t.apply();
}
});
mVeilAnimator.start();
+ mIsShowing = false;
}
@ColorRes
@@ -242,6 +383,11 @@ public class ResizeVeil {
}
}
+ private PointF calculateAppIconPosition(Rect parentBounds) {
+ return new PointF((float) parentBounds.width() / 2 - (float) mIconSize / 2,
+ (float) parentBounds.height() / 2 - (float) mIconSize / 2);
+ }
+
private void cancelAnimation() {
if (mVeilAnimator != null) {
mVeilAnimator.removeAllUpdateListeners();
@@ -250,21 +396,56 @@ public class ResizeVeil {
}
/**
+ * Whether the resize veil is currently visible.
+ *
+ * Note: when animating a {@link ResizeVeil#hideVeil()}, the veil is considered visible as soon
+ * as the animation starts.
+ */
+ private boolean isVisible() {
+ return mIsShowing;
+ }
+
+ /** Whether the resize veil is ready to be shown. */
+ private boolean isReady() {
+ return mViewHost != null;
+ }
+
+ /**
* Dispose of veil when it is no longer needed, likely on close of its container decor.
*/
void dispose() {
cancelAnimation();
+ mIsShowing = false;
mVeilAnimator = null;
if (mViewHost != null) {
mViewHost.release();
mViewHost = null;
}
+ final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
+ if (mBackgroundSurface != null) {
+ t.remove(mBackgroundSurface);
+ mBackgroundSurface = null;
+ }
+ if (mIconSurface != null) {
+ t.remove(mIconSurface);
+ mIconSurface = null;
+ }
if (mVeilSurface != null) {
- final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get();
t.remove(mVeilSurface);
mVeilSurface = null;
- t.apply();
+ }
+ t.apply();
+ mDisplayController.removeDisplayWindowListener(mOnDisplaysChangedListener);
+ }
+
+ interface SurfaceControlBuilderFactory {
+ default SurfaceControl.Builder create(@NonNull String name) {
+ return new SurfaceControl.Builder().setName(name);
+ }
+ default SurfaceControl.Builder create(@NonNull String name,
+ @NonNull SurfaceSession surfaceSession) {
+ return new SurfaceControl.Builder(surfaceSession).setName(name);
}
}
}
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..53d4e2701849 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
@@ -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..541825437c86 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,6 +18,8 @@ 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.view.WindowInsets.Type.captionBar;
+import static android.view.WindowInsets.Type.mandatorySystemGestures;
import static android.view.WindowInsets.Type.statusBars;
import android.annotation.NonNull;
@@ -33,6 +35,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 +43,22 @@ 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.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 java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
import java.util.function.Supplier;
/**
@@ -131,8 +135,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(
@@ -203,7 +208,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
mLayoutResId = params.mLayoutResId;
if (!mTaskInfo.isVisible) {
- releaseViews();
+ releaseViews(wct);
finishT.hide(mTaskSurface);
return;
}
@@ -226,7 +231,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
|| mDisplay.getDisplayId() != mTaskInfo.displayId
|| oldLayoutResId != mLayoutResId
|| oldNightMode != newNightMode) {
- releaseViews();
+ releaseViews(wct);
if (!obtainDisplayOrRegisterListener()) {
outResult.mRootView = null;
@@ -293,60 +298,57 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
.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);
- }
+ outResult.mRootView.setTaskFocusState(mTaskInfo.isFocused);
+
+ // Caption insets
+ if (mIsCaptionVisible) {
+ // Caption inset is the full width of the task with the |captionHeight| and
+ // positioned at the top of the task bounds, also in absolute coordinates.
+ // So just reuse the task bounds and adjust the bottom coordinate.
+ final Rect captionInsetsRect = new Rect(taskBounds);
+ captionInsetsRect.bottom = captionInsetsRect.top + outResult.mCaptionHeight;
+
+ // Caption bounding rectangles: these are optional, and are used to present finer
+ // insets than traditional |Insets| to apps about where their content is occluded.
+ // These are also in absolute coordinates.
+ final Rect[] boundingRects;
+ final int numOfElements = params.mOccludingCaptionElements.size();
+ if (numOfElements == 0) {
+ boundingRects = null;
+ } else {
+ // The customizable region can at most be equal to the caption bar.
+ if (params.hasInputFeatureSpy()) {
+ outResult.mCustomizableCaptionRegion.set(captionInsetsRect);
+ }
+ boundingRects = new Rect[numOfElements];
+ for (int i = 0; i < numOfElements; i++) {
+ final OccludingCaptionElement element =
+ params.mOccludingCaptionElements.get(i);
+ final int elementWidthPx =
+ resources.getDimensionPixelSize(element.mWidthResId);
+ boundingRects[i] =
+ calculateBoundingRect(element, elementWidthPx, captionInsetsRect);
+ // Subtract the regions used by the caption elements, the rest is
+ // customizable.
+ if (params.hasInputFeatureSpy()) {
+ outResult.mCustomizableCaptionRegion.op(boundingRects[i],
+ Region.Op.DIFFERENCE);
}
}
- // 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());
+ }
+
+ final WindowDecorationInsets newInsets = new WindowDecorationInsets(
+ mTaskInfo.token, mOwner, captionInsetsRect, boundingRects);
+ if (!newInsets.equals(mWindowDecorationInsets)) {
+ // Add or update this caption as an insets source.
+ mWindowDecorationInsets = newInsets;
+ mWindowDecorationInsets.addOrUpdate(wct);
}
} else {
- startT.hide(mCaptionContainerSurface);
+ if (mWindowDecorationInsets != null) {
+ mWindowDecorationInsets.remove(wct);
+ mWindowDecorationInsets = null;
+ }
}
// Task surface itself
@@ -383,6 +385,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
startT.unsetColor(mTaskSurface);
}
+ 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.
@@ -399,24 +402,25 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
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);
}
mViewHost.setView(outResult.mRootView, lp);
+ Trace.endSection();
} else {
+ Trace.beginSection("CaptionViewHostLayout-relayout");
if (params.mApplyStartTransactionOnDraw) {
mViewHost.getRootSurfaceControl().applyTransactionOnDraw(startT);
}
mViewHost.relayout(lp);
+ Trace.endSection();
}
+ Trace.endSection(); // CaptionViewHostLayout
}
private Rect calculateBoundingRect(@NonNull OccludingCaptionElement element,
@@ -487,7 +491,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 +517,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) {
@@ -594,15 +600,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 +620,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 +635,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 +645,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.
@@ -674,6 +687,51 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
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);
+ }
+ }
+
+ 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);
+ }
+
+ void remove(WindowContainerTransaction wct) {
+ wct.removeInsetsSource(mToken, mOwner, INDEX, captionBar());
+ wct.removeInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures());
+ }
+
+ @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);
+ }
+
+ @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/extension/TaskInfo.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/TaskInfo.kt
index 5dd96aceaec7..ec204714c341 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?.systemBarsAppearance ?: 0
return (appearance and APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND) != 0
}
val TaskInfo.isLightCaptionBarAppearance: Boolean
get() {
- val appearance = taskDescription?.statusBarAppearance ?: 0
+ val appearance = taskDescription?.systemBarsAppearance ?: 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/DesktopModeFocusedWindowDecorationViewHolder.kt
index 6dcae2776847..96bc4a146ebd 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/DesktopModeFocusedWindowDecorationViewHolder.kt
@@ -65,7 +65,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/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS
index d718e157afdb..b8a19ad35307 100644
--- a/libs/WindowManager/Shell/tests/OWNERS
+++ b/libs/WindowManager/Shell/tests/OWNERS
@@ -12,3 +12,6 @@ jorgegil@google.com
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..f2f10aef4fd7 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,18 @@ class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) :
}
}
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 312446524)
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
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/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/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..90453640c91a 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
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/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..82c070cbf1c3 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,10 @@
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_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
@@ -435,7 +435,8 @@ public class ShellTaskOrganizerTests extends ShellTestCase {
public void testOnCameraCompatActivityChanged() {
final RunningTaskInfo taskInfo1 = createTaskInfo(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);
@@ -449,7 +450,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 +462,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 +475,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 +486,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 +497,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);
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..ea522cdf2509 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;
@@ -100,6 +102,20 @@ public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnim
}
@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());
+ }
+
+ @Test
public void testInvalidCustomAnimation() {
final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0)
.addChange(createChange(FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY))
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..f6f3aa49bc6e 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,11 +113,15 @@ 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;
@Before
@@ -131,12 +135,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(
@@ -178,7 +184,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(),
@@ -347,6 +355,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 +396,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 +415,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,
@@ -499,7 +535,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;
@@ -518,8 +554,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 +564,32 @@ 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)));
+ 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 +600,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 +643,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 +682,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..8932e60048e6 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
@@ -134,6 +134,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..0f433770777e 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;
@@ -1191,20 +1193,74 @@ 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);
+ }
+
+ @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/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 19ce2f3899c3..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
@@ -116,27 +116,27 @@ public class SplitLayoutTests extends ShellTestCase {
@Test
public void testUpdateDivideBounds() {
- mSplitLayout.updateDivideBounds(anyInt(), anyBoolean());
+ mSplitLayout.updateDividerBounds(anyInt(), anyBoolean());
verify(mSplitLayoutHandler).onLayoutSizeChanging(any(SplitLayout.class), 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);
}
@@ -153,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());
}
@@ -165,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());
}
@@ -189,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 2c85495ce5db..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;
@@ -701,7 +701,8 @@ 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;
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 d92e5aa0890a..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,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 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;
@@ -38,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;
@@ -544,11 +544,12 @@ public class CompatUIWindowManagerTest extends ShellTestCase {
}
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.appCompatTaskInfo.topActivityLetterboxHeight = 1000;
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/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
new file mode 100644
index 000000000000..2a2483df0792
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -0,0 +1,368 @@
+/*
+ * 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.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
+
+/**
+ * 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 taskCreated_notFreeformWindow_doesNotLogSessionEnterOrTaskAdded() {
+ val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
+ verify(desktopModeEventLogger, never()).logTaskAdded(any(), any())
+ }
+
+ @Test
+ fun taskCreated_FreeformWindowOpen_logSessionEnterAndTaskAdded() {
+ val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+ eq(EnterReason.APP_FREEFORM_INTENT))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ }
+
+ @Test
+ fun taskChanged_taskMovedToDesktopByDrag_logSessionEnterAndTaskAdded() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ // task change is finalised when drag ends
+ val transitionInfo = TransitionInfoBuilder(
+ Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+ eq(EnterReason.APP_HANDLE_DRAG))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ }
+
+ @Test
+ fun taskChanged_taskMovedToDesktopByButtonTap_logSessionEnterAndTaskAdded() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0)
+ .addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+ eq(EnterReason.APP_HANDLE_MENU_BUTTON))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ }
+
+ @Test
+ fun taskChanged_existingFreeformTaskMadeVisible_logSessionEnterAndTaskAdded() {
+ val taskInfo = createTaskInfo(1, WINDOWING_MODE_FREEFORM)
+ taskInfo.isVisibleRequested = true
+ val change = createChange(TRANSIT_CHANGE, taskInfo)
+ val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_MOVE_TO_DESKTOP, 0)
+ .addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+ eq(EnterReason.APP_HANDLE_MENU_BUTTON))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ }
+
+ @Test
+ fun taskToFront_screenWake_logSessionStartedAndTaskAdded() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0)
+ .addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(sessionId!!),
+ eq(EnterReason.SCREEN_ON))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ }
+
+ @Test
+ fun freeformTaskVisible_screenTurnOff_logSessionExitAndTaskRemoved_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+ eq(ExitReason.SCREEN_OFF))
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun freeformTaskVisible_exitDesktopUsingDrag_logSessionExitAndTaskRemoved_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // window mode changing from FREEFORM to FULLSCREEN
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo = TransitionInfoBuilder(Transitions.TRANSIT_EXIT_DESKTOP_MODE)
+ .addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+ eq(ExitReason.DRAG_TO_EXIT))
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun freeformTaskVisible_exitDesktopBySwipeUp_logSessionExitAndTaskRemoved_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // recents transition
+ val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
+ .addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+ eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun freeformTaskVisible_taskFinished_logSessionExitAndTaskRemoved_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // task closing
+ val change = createChange(TRANSIT_CLOSE, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+ eq(ExitReason.TASK_FINISHED))
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun sessionExitByRecents_cancelledAnimation_sessionRestored() {
+ val sessionId = 1
+ // add a freeform task to an existing session
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // recents transition sent freeform window to back
+ val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo1 =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change)
+ .build()
+ callOnTransitionReady(transitionInfo1)
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1)).logSessionExit(eq(sessionId),
+ eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+
+ val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build()
+ callOnTransitionReady(transitionInfo2)
+
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(any(), any())
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(any(), any())
+ }
+
+ @Test
+ fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() {
+ val sessionId = 1
+ // add an existing freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // new freeform task added
+ val change = createChange(TRANSIT_OPEN, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
+ }
+
+ @Test
+ fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() {
+ val sessionId = 1
+ // add two existing freeform tasks
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // new freeform task added
+ val change = createChange(TRANSIT_CLOSE, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, never()).logSessionExit(any(), any())
+ }
+
+ /**
+ * Simulate calling the onTransitionReady() method
+ */
+ private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
+ val transition = mock(IBinder::class.java)
+ val startT = mock(
+ SurfaceControl.Transaction::class.java)
+ val finishT = mock(
+ SurfaceControl.Transaction::class.java)
+
+ transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT)
+ }
+
+ companion object {
+ fun createTaskInfo(taskId: Int, windowMode: Int): ActivityManager.RunningTaskInfo {
+ val taskInfo = ActivityManager.RunningTaskInfo()
+ taskInfo.taskId = taskId
+ taskInfo.configuration.windowConfiguration.windowingMode = windowMode
+
+ return taskInfo
+ }
+
+ 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
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt
index 445f74a52b0d..8f59f30da697 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
@@ -326,64 +388,126 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() {
}
@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(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 = 0, taskId = 1)
+ repo.addActiveTask(displayId = 0, taskId = 2)
+ repo.addActiveTask(displayId = 0, taskId = 3)
+ // The front-most task will be the one added last through addOrMoveFreeformTaskToTop
+ repo.addOrMoveFreeformTaskToTop(taskId = 3)
+ repo.addOrMoveFreeformTaskToTop(taskId = 2)
+ repo.addOrMoveFreeformTaskToTop(taskId = 1)
+
+ assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0)).isEqualTo(
+ listOf(1, 2, 3))
+ }
+
+ @Test
+ fun getActiveNonMinimizedTasksOrderedFrontToBack_minimizedTaskNotIncluded() {
+ repo.addActiveTask(displayId = 0, taskId = 1)
+ repo.addActiveTask(displayId = 0, taskId = 2)
+ repo.addActiveTask(displayId = 0, taskId = 3)
+ // The front-most task will be the one added last through addOrMoveFreeformTaskToTop
+ repo.addOrMoveFreeformTaskToTop(taskId = 3)
+ repo.addOrMoveFreeformTaskToTop(taskId = 2)
+ repo.addOrMoveFreeformTaskToTop(taskId = 1)
+ repo.minimizeTask(displayId = 0, taskId = 2)
+
+ assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0)).isEqualTo(
+ listOf(1, 3))
+ }
+
+
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,20 +540,6 @@ 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 {
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..285e5b6a04a5
--- /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(10)
+
+
+ @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..f67da5573b7d 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
@@ -23,34 +23,55 @@ 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.Intent
+import android.content.pm.ActivityInfo
+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.RemoteTransition
import android.window.TransitionRequestInfo
+import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
+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 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.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
@@ -58,21 +79,23 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplit
import com.android.wm.shell.draganddrop.DragAndDropController
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 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,53 +107,95 @@ 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.kotlin.anyOrNull
+import org.mockito.kotlin.atLeastOnce
+import org.mockito.kotlin.capture
+import org.mockito.quality.Strictness
+import java.util.Optional
import org.mockito.Mockito.`when` as whenever
+/**
+ * Test class for {@link DesktopTasksController}
+ *
+ * Usage: atest WMShellUnitTests:DesktopTasksControllerTest
+ */
@SmallTest
@RunWith(AndroidTestingRunner::class)
class DesktopTasksControllerTest : ShellTestCase() {
+ @JvmField
+ @Rule
+ val setFlagsRule = SetFlagsRule()
+
@Mock lateinit var testExecutor: ShellExecutor
@Mock lateinit var shellCommandHandler: ShellCommandHandler
@Mock lateinit var shellController: ShellController
@Mock lateinit var displayController: DisplayController
+ @Mock lateinit var displayLayout: DisplayLayout
@Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer
@Mock lateinit var syncQueue: SyncTransactionQueue
@Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
@Mock lateinit var transitions: Transitions
@Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler
@Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler
- @Mock lateinit var mToggleResizeDesktopTaskTransitionHandler:
+ @Mock lateinit var toggleResizeDesktopTaskTransitionHandler:
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
+ @Mock lateinit var desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver
+ @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator
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().mockStatic(DesktopModeStatus::class.java).startMocking()
+ mockitoSession = mockitoSession().strictness(Strictness.LENIENT)
+ .spyStatic(DesktopModeStatus::class.java).startMocking()
whenever(DesktopModeStatus.isEnabled()).thenReturn(true)
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
- shellInit = Mockito.spy(ShellInit(testExecutor))
+ shellInit = spy(ShellInit(testExecutor))
desktopModeTaskRepository = DesktopModeTaskRepository()
+ desktopTasksLimiter =
+ DesktopTasksLimiter(transitions, desktopModeTaskRepository, shellTaskOrganizer)
whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks }
whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() }
+ whenever(enterDesktopTransitionHandler.moveToDesktop(any())).thenAnswer { Binder() }
+ whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout)
+ whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(STABLE_BOUNDS)
+ }
+
+ val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0)
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
+ whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda)
controller = createController()
controller.setSplitScreenController(splitScreenController)
@@ -156,13 +221,15 @@ class DesktopTasksControllerTest : ShellTestCase() {
transitions,
enterDesktopTransitionHandler,
exitDesktopTransitionHandler,
- mToggleResizeDesktopTaskTransitionHandler,
+ toggleResizeDesktopTaskTransitionHandler,
dragToDesktopTransitionHandler,
desktopModeTaskRepository,
+ desktopModeLoggerTransitionObserver,
launchAdjacentController,
recentsTransitionHandler,
multiInstanceHelper,
- shellExecutor
+ shellExecutor,
+ Optional.of(desktopTasksLimiter),
)
}
@@ -189,7 +256,8 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun showDesktopApps_allAppsInvisible_bringsToFront() {
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() {
val homeTask = setUpHomeTask()
val task1 = setUpFreeformTask()
val task2 = setUpFreeformTask()
@@ -208,7 +276,27 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun showDesktopApps_appsAlreadyVisible_bringsToFront() {
+ @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()
@@ -227,7 +315,27 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun showDesktopApps_someAppsInvisible_reordersAll() {
+ @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()
@@ -246,7 +354,27 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun showDesktopApps_noActiveTasks_reorderHomeToTop() {
+ @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()))
@@ -258,7 +386,18 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay() {
+ @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)
@@ -277,6 +416,44 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@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)
}
@@ -306,9 +483,139 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun moveToDesktop_displayFullscreen_windowingModeSetToFreeform() {
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask()
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
+ shouldLetterbox = true)
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(isResizable = false,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(isResizable = false,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
+ setUpLandscapeDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(isResizable = false,
+ deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun moveToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val task = setUpFullscreenTask(isResizable = false,
+ deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
+ setUpPortraitDisplay()
+
+ controller.moveToDesktop(task)
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() {
val task = setUpFullscreenTask()
- task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
controller.moveToDesktop(task)
val wct = getLatestMoveToDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
@@ -316,9 +623,10 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun moveToDesktop_displayFreeform_windowingModeSetToUndefined() {
+ fun moveToDesktop_tdaFreeform_windowingModeSetToUndefined() {
val task = setUpFullscreenTask()
- task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
controller.moveToDesktop(task)
val wct = getLatestMoveToDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
@@ -332,7 +640,59 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun moveToDesktop_otherFreeformTasksBroughtToFront() {
+ fun moveToDesktop_deviceNotSupported_doesNothing() {
+ val task = setUpFullscreenTask()
+
+ // Simulate non compatible device
+ doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+
+ controller.moveToDesktop(task)
+ verifyWCTNotExecuted()
+ }
+
+ @Test
+ fun moveToDesktop_topActivityTranslucent_doesNothing() {
+ setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+ val task = setUpFullscreenTask().apply {
+ isTopActivityTransparent = true
+ numActivities = 1
+ }
+
+ controller.moveToDesktop(task)
+ verifyWCTNotExecuted()
+ }
+
+ @Test
+ fun moveToDesktop_deviceNotSupported_deviceRestrictionsOverridden_taskIsMovedToDesktop() {
+ val task = setUpFullscreenTask()
+
+ // Simulate non compatible device
+ doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+
+ // Simulate enforce device restrictions system property overridden to false
+ whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false)
+
+ controller.moveToDesktop(task)
+
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
+ fun moveToDesktop_deviceSupported_taskIsMovedToDesktop() {
+ val task = setUpFullscreenTask()
+
+ controller.moveToDesktop(task)
+
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() {
val homeTask = setUpHomeTask()
val freeformTask = setUpFreeformTask()
val fullscreenTask = setUpFullscreenTask()
@@ -350,6 +710,26 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() {
+ val freeformTask = setUpFreeformTask()
+ val fullscreenTask = setUpFullscreenTask()
+ markTaskHidden(freeformTask)
+
+ controller.moveToDesktop(fullscreenTask)
+
+ with(getLatestMoveToDesktopWct()) {
+ // Operations should include wallpaper intent, freeform task, fullscreen task
+ assertThat(hierarchyOps).hasSize(3)
+ assertPendingIntentAt(index = 0, desktopWallpaperIntent)
+ assertReorderAt(index = 1, freeformTask)
+ assertReorderAt(index = 2, fullscreenTask)
+ assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_FREEFORM)
+ }
+ }
+
+ @Test
fun moveToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() {
setUpHomeTask(displayId = DEFAULT_DISPLAY)
val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
@@ -378,7 +758,9 @@ class DesktopTasksControllerTest : ShellTestCase() {
val wct = getLatestMoveToDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FREEFORM)
- verify(splitScreenController).prepareExitSplitScreen(any(), anyInt(),
+ verify(splitScreenController).prepareExitSplitScreen(
+ any(),
+ anyInt(),
eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)
)
}
@@ -390,15 +772,36 @@ class DesktopTasksControllerTest : ShellTestCase() {
val wct = getLatestMoveToDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FREEFORM)
- verify(splitScreenController, never()).prepareExitSplitScreen(any(), anyInt(),
+ verify(splitScreenController, never()).prepareExitSplitScreen(
+ any(),
+ anyInt(),
eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)
)
}
@Test
- fun moveToFullscreen_displayFullscreen_windowingModeSetToUndefined() {
+ fun moveToDesktop_bringsTasksOverLimit_dontShowBackTask() {
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ val homeTask = setUpHomeTask()
+ val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
+ val newTask = setUpFullscreenTask()
+
+ controller.moveToDesktop(newTask)
+
+ val wct = getLatestMoveToDesktopWct()
+ assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + home
+ wct.assertReorderAt(0, homeTask)
+ for (i in 1..<taskLimit) { // Skipping freeformTasks[0]
+ wct.assertReorderAt(index = i, task = freeformTasks[i])
+ }
+ wct.assertReorderAt(taskLimit, newTask)
+ }
+
+ @Test
+ fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() {
val task = setUpFreeformTask()
- task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN
controller.moveToFullscreen(task.taskId)
val wct = getLatestExitDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
@@ -406,9 +809,10 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun moveToFullscreen_displayFreeform_windowingModeSetToFullscreen() {
+ fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() {
val task = setUpFreeformTask()
- task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM
+ val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!!
+ tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM
controller.moveToFullscreen(task.taskId)
val wct = getLatestExitDesktopWct()
assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
@@ -447,6 +851,20 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@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)
@@ -510,6 +928,48 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@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)
@@ -523,6 +983,38 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@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)
@@ -553,36 +1045,30 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun handleRequest_fullscreenTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() {
+ fun handleRequest_freeformTask_freeformVisible_returnNull() {
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)
+ val freeformTask1 = setUpFreeformTask()
+ markTaskVisible(freeformTask1)
- // Stashed state should be cleared
- assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
+ val freeformTask2 = createFreeformTask()
+ assertThat(controller.handleRequest(Binder(), createTransition(freeformTask2))).isNull()
}
@Test
- fun handleRequest_freeformTask_freeformVisible_returnNull() {
+ fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() {
assumeTrue(ENABLE_SHELL_TRANSITIONS)
- val freeformTask1 = setUpFreeformTask()
- markTaskVisible(freeformTask1)
+ val taskLimit = desktopTasksLimiter.getMaxTaskLimit()
+ val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() }
+ freeformTasks.forEach { markTaskVisible(it) }
+ val newFreeformTask = createFreeformTask()
- val freeformTask2 = createFreeformTask()
- assertThat(controller.handleRequest(Binder(), createTransition(freeformTask2))).isNull()
+ 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
@@ -599,7 +1085,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
createTransition(freeformTask2, type = TRANSIT_TO_FRONT)
)
assertThat(result?.changes?.get(freeformTask2.token.asBinder())?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
}
@Test
@@ -609,7 +1095,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
val task = createFreeformTask()
val result = controller.handleRequest(Binder(), createTransition(task))
assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
}
@Test
@@ -621,27 +1107,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
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()
+ .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
}
@Test
@@ -698,26 +1164,65 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun stashDesktopApps_stateUpdates() {
- whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
+ fun handleRequest_shouldLaunchAsModal_returnSwitchToFullscreenWCT() {
+ setFlagsRule.enableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
+ val task = setUpFreeformTask().apply {
+ isTopActivityTransparent = true
+ numActivities = 1
+ }
- controller.stashDesktopApps(DEFAULT_DISPLAY)
+ val result = controller.handleRequest(Binder(), createTransition(task))
+ assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+ }
- assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isTrue()
- assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isFalse()
+ @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
- fun hideStashedDesktopApps_stateUpdates() {
- whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true)
+ @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
+ fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperDisabled() {
+ desktopModeTaskRepository.wallpaperActivityToken = MockToken().token()
- desktopModeTaskRepository.setStashed(DEFAULT_DISPLAY, true)
- desktopModeTaskRepository.setStashed(SECOND_DISPLAY, true)
- controller.hideStashedDesktopApps(DEFAULT_DISPLAY)
+ val task = setUpFreeformTask()
+ val result =
+ controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))
+ // Doesn't handle request
+ assertThat(result).isNull()
+ }
- assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse()
- // Check that second display is not affected
- assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isTrue()
+ @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
@@ -741,7 +1246,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
verify(launchAdjacentController).launchAdjacentEnabled = true
}
@Test
- fun enterDesktop_fullscreenTaskIsMovedToDesktop() {
+ fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() {
val task1 = setUpFullscreenTask()
val task2 = setUpFullscreenTask()
val task3 = setUpFullscreenTask()
@@ -750,7 +1255,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
task2.isFocused = false
task3.isFocused = false
- controller.enterDesktop(DEFAULT_DISPLAY)
+ controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY)
val wct = getLatestMoveToDesktopWct()
assertThat(wct.changes[task1.token.asBinder()]?.windowingMode)
@@ -758,7 +1263,7 @@ class DesktopTasksControllerTest : ShellTestCase() {
}
@Test
- fun enterDesktop_splitScreenTaskIsMovedToDesktop() {
+ fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() {
val task1 = setUpSplitScreenTask()
val task2 = setUpFullscreenTask()
val task3 = setUpFullscreenTask()
@@ -771,12 +1276,14 @@ class DesktopTasksControllerTest : ShellTestCase() {
task4.parentTaskId = task1.taskId
- controller.enterDesktop(DEFAULT_DISPLAY)
+ controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY)
val wct = getLatestMoveToDesktopWct()
assertThat(wct.changes[task4.token.asBinder()]?.windowingMode)
.isEqualTo(WINDOWING_MODE_FREEFORM)
- verify(splitScreenController).prepareExitSplitScreen(any(), anyInt(),
+ verify(splitScreenController).prepareExitSplitScreen(
+ any(),
+ anyInt(),
eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)
)
}
@@ -795,11 +1302,294 @@ class DesktopTasksControllerTest : ShellTestCase() {
val wct = getLatestExitDesktopWct()
assertThat(wct.changes[task2.token.asBinder()]?.windowingMode)
- .isEqualTo(WINDOWING_MODE_FULLSCREEN)
+ .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask()
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
}
- private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
- val task = createFreeformTask(displayId)
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT,
+ shouldLetterbox = true)
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(isResizable = false,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE)
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(isResizable = false,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true)
+ setUpLandscapeDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(isResizable = false,
+ deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_PORTRAIT)
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS)
+ fun dragToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() {
+ doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) }
+ val spyController = spy(controller)
+ whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator)
+ whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull(), anyOrNull()))
+ .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR)
+
+ val task = setUpFullscreenTask(isResizable = false,
+ deviceOrientation = ORIENTATION_PORTRAIT,
+ screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, shouldLetterbox = true)
+ setUpPortraitDisplay()
+
+ spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task)
+ val wct = getLatestDragToDesktopWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS)
+ }
+
+ @Test
+ fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() {
+ val task = setUpFreeformTask()
+ val mockSurface = mock(SurfaceControl::class.java)
+ val mockDisplayLayout = mock(DisplayLayout::class.java)
+ whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout)
+ whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000))
+ controller.onDragPositioningMove(task, mockSurface, 200f,
+ Rect(100, -100, 500, 1000))
+
+ controller.onDragPositioningEnd(task,
+ Point(100, -100), /* position */
+ PointF(200f, -200f), /* inputCoordinate */
+ Rect(100, -100, 500, 1000), /* taskBounds */
+ Rect(0, 50, 2000, 2000) /* validDragArea */
+ )
+ val rectAfterEnd = Rect(100, 50, 500, 1150)
+ verify(transitions).startTransition(
+ eq(TRANSIT_CHANGE), Mockito.argThat { wct ->
+ return@argThat wct.changes.any { (token, change) ->
+ change.configuration.windowConfiguration.bounds == rectAfterEnd
+ }
+ }, eq(null))
+ }
+
+ fun enterSplit_freeformTaskIsMovedToSplit() {
+ val task1 = setUpFreeformTask()
+ val task2 = setUpFreeformTask()
+ val task3 = setUpFreeformTask()
+
+ task1.isFocused = false
+ task2.isFocused = true
+ task3.isFocused = false
+
+ controller.enterSplit(DEFAULT_DISPLAY, false)
+
+ verify(splitScreenController).requestEnterSplitSelect(
+ task2,
+ any(),
+ SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT,
+ task2.configuration.windowConfiguration.bounds
+ )
+ }
+
+ @Test
+ fun toggleBounds_togglesToStableBounds() {
+ val bounds = Rect(0, 0, 100, 100)
+ val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
+
+ controller.toggleDesktopTaskSize(task)
+ // Assert bounds set to stable bounds
+ val wct = getLatestToggleResizeDesktopTaskWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS)
+ }
+
+ @Test
+ fun toggleBounds_lastBoundsBeforeMaximizeSaved() {
+ val bounds = Rect(0, 0, 100, 100)
+ val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds)
+
+ controller.toggleDesktopTaskSize(task)
+ assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId))
+ .isEqualTo(bounds)
+ }
+
+ @Test
+ fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize() {
+ val boundsBeforeMaximize = Rect(0, 0, 100, 100)
+ val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
+
+ // Maximize
+ controller.toggleDesktopTaskSize(task)
+ task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
+
+ // Restore
+ controller.toggleDesktopTaskSize(task)
+
+ // Assert bounds set to last bounds before maximize
+ val wct = getLatestToggleResizeDesktopTaskWct()
+ assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize)
+ }
+
+ @Test
+ fun toggleBounds_removesLastBoundsBeforeMaximizeAfterRestoringBounds() {
+ val boundsBeforeMaximize = Rect(0, 0, 100, 100)
+ val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize)
+
+ // Maximize
+ controller.toggleDesktopTaskSize(task)
+ task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS)
+
+ // Restore
+ controller.toggleDesktopTaskSize(task)
+
+ // Assert last bounds before maximize removed after use
+ assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
+ }
+
+ private val desktopWallpaperIntent: Intent
+ get() = Intent(context, DesktopWallpaperActivity::class.java)
+
+ private fun setUpFreeformTask(
+ displayId: Int = DEFAULT_DISPLAY,
+ bounds: Rect? = null
+ ): RunningTaskInfo {
+ val task = createFreeformTask(displayId, bounds)
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
desktopModeTaskRepository.addActiveTask(displayId, task.taskId)
desktopModeTaskRepository.addOrMoveFreeformTaskToTop(task.taskId)
@@ -814,15 +1604,68 @@ class DesktopTasksControllerTest : ShellTestCase() {
return task
}
- private fun setUpFullscreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
+ private fun setUpFullscreenTask(
+ displayId: Int = DEFAULT_DISPLAY,
+ isResizable: Boolean = true,
+ windowingMode: Int = WINDOWING_MODE_FULLSCREEN,
+ deviceOrientation: Int = ORIENTATION_LANDSCAPE,
+ screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED,
+ shouldLetterbox: Boolean = false
+ ): RunningTaskInfo {
val task = createFullscreenTask(displayId)
+ val activityInfo = ActivityInfo()
+ activityInfo.screenOrientation = screenOrientation
+ with(task) {
+ topActivityInfo = activityInfo
+ isResizeable = isResizable
+ configuration.orientation = deviceOrientation
+ configuration.windowConfiguration.windowingMode = windowingMode
+
+ if (shouldLetterbox) {
+ if (deviceOrientation == ORIENTATION_LANDSCAPE &&
+ screenOrientation == SCREEN_ORIENTATION_PORTRAIT) {
+ // Letterbox to portrait size
+ appCompatTaskInfo.topActivityBoundsLetterboxed = true
+ appCompatTaskInfo.topActivityLetterboxWidth = 1200
+ appCompatTaskInfo.topActivityLetterboxHeight = 1600
+ } else if (deviceOrientation == ORIENTATION_PORTRAIT &&
+ screenOrientation == SCREEN_ORIENTATION_LANDSCAPE) {
+ // Letterbox to landscape size
+ appCompatTaskInfo.topActivityBoundsLetterboxed = true
+ appCompatTaskInfo.topActivityLetterboxWidth = 1600
+ appCompatTaskInfo.topActivityLetterboxHeight = 1200
+ }
+ } else {
+ appCompatTaskInfo.topActivityBoundsLetterboxed = false
+ }
+
+ if (deviceOrientation == ORIENTATION_LANDSCAPE) {
+ configuration.windowConfiguration.appBounds = Rect(0, 0,
+ DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT)
+ } else {
+ configuration.windowConfiguration.appBounds = Rect(0, 0,
+ DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG)
+ }
+ }
+ whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
runningTasks.add(task)
return task
}
+ private fun setUpLandscapeDisplay() {
+ whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG)
+ whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT)
+ }
+
+ private fun setUpPortraitDisplay() {
+ whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT)
+ whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG)
+ }
+
private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo {
val task = createSplitScreenTask(displayId)
+ whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true)
whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true)
whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
runningTasks.add(task)
@@ -862,6 +1705,18 @@ class DesktopTasksControllerTest : ShellTestCase() {
return arg.value
}
+ private fun getLatestToggleResizeDesktopTaskWct(): WindowContainerTransaction {
+ val arg: ArgumentCaptor<WindowContainerTransaction> =
+ ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+ if (ENABLE_SHELL_TRANSITIONS) {
+ verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce())
+ .startTransition(capture(arg))
+ } else {
+ verify(shellTaskOrganizer).applyTransaction(capture(arg))
+ }
+ return arg.value
+ }
+
private fun getLatestMoveToDesktopWct(): WindowContainerTransaction {
val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
if (ENABLE_SHELL_TRANSITIONS) {
@@ -872,6 +1727,17 @@ class DesktopTasksControllerTest : ShellTestCase() {
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) {
@@ -883,6 +1749,10 @@ class DesktopTasksControllerTest : ShellTestCase() {
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())
@@ -900,16 +1770,26 @@ class DesktopTasksControllerTest : ShellTestCase() {
companion object {
const val SECOND_DISPLAY = 2
+ private val STABLE_BOUNDS = Rect(0, 0, 1000, 1000)
}
}
-private fun WindowContainerTransaction.assertReorderAt(index: Int, task: RunningTaskInfo) {
+private fun WindowContainerTransaction.assertIndexInBounds(index: Int) {
assertWithMessage("WCT does not have a hierarchy operation at index $index")
.that(hierarchyOps.size)
.isGreaterThan(index)
+}
+
+private fun WindowContainerTransaction.assertReorderAt(
+ index: Int,
+ task: RunningTaskInfo,
+ toTop: Boolean? = null
+) {
+ assertIndexInBounds(index)
val op = hierarchyOps[index]
assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER)
assertThat(op.container).isEqualTo(task.token.asBinder())
+ toTop?.let { assertThat(op.toTop).isEqualTo(it) }
}
private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) {
@@ -917,3 +1797,17 @@ private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: Runni
assertReorderAt(i, tasks[i])
}
}
+
+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 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)
+}
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..3c488cac6edd
--- /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(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..2ade3fba9b08 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
@@ -190,7 +190,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() {
handler.cancelDragToDesktopTransition()
// Cancel animation should run since it had already started.
- verify(dragAnimator).endAnimator()
+ verify(dragAnimator).cancelAnimator()
}
@Test
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..cd68c6996578 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),
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..bd8ac379b86f
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipTransitionStateTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.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;
+
+/**
+ * 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;
+
+ @Before
+ public void setUp() {
+ mPipTransitionState = new PipTransitionState();
+ 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..884cb6ec9f74 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,13 @@ public class RecentTasksControllerTest extends ShellTestCase {
private DesktopModeTaskRepository mDesktopModeTaskRepository;
@Mock
private ActivityTaskManager mActivityTaskManager;
+ @Mock
+ private DisplayInsetsController mDisplayInsetsController;
+ @Mock
+ private IRecentTasksListener mRecentTasksListener;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private ShellTaskOrganizer mShellTaskOrganizer;
private RecentTasksController mRecentTasksController;
@@ -103,14 +123,20 @@ 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);
@@ -121,6 +147,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 +291,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 +321,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);
- mockitoSession.finishMocking();
+ // 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));
+
+ // 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 +393,45 @@ public class RecentTasksControllerTest extends ShellTestCase {
assertEquals(t2, recentTasks.get(1).getTaskInfo1());
assertEquals(t3, recentTasks.get(2).getTaskInfo1());
assertEquals(t4, recentTasks.get(3).getTaskInfo1());
+ }
- mockitoSession.finishMocking();
+ @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);
+
+ // 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 +478,85 @@ 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.onTaskWindowingModeChanged(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.onTaskWindowingModeChanged(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
public void getNullSplitBoundsNonSplitTask() {
SplitBounds sb = mRecentTasksController.getSplitBoundsForTaskId(3);
assertNull("splitBounds should be null for non-split task", sb);
@@ -420,6 +602,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/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/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/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 917fd715f71f..aa2cee79fcfc 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
@@ -27,8 +27,14 @@ import android.graphics.Rect
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
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
@@ -41,6 +47,10 @@ import android.view.SurfaceView
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.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
@@ -51,16 +61,21 @@ 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.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.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 +84,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 +127,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 +136,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
@Before
fun setUp() {
shellInit = ShellInit(mockShellExecutor)
+ windowDecorByTaskIdSpy.clear()
desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel(
mContext,
mockShellExecutor,
@@ -128,7 +155,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
mockDesktopModeWindowDecorFactory,
mockInputMonitorFactory,
transactionFactory,
- mockRootTaskDisplayAreaOrganizer
+ mockRootTaskDisplayAreaOrganizer,
+ windowDecorByTaskIdSpy
)
whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout)
@@ -272,6 +300,20 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
}
@Test
+ fun testDescorationIsNotCreatedForTopTranslucentActivities() {
+ 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
+ @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 +334,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 +355,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 +376,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,
@@ -368,7 +495,8 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() {
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 +515,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..608f74b95280 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,6 +18,7 @@ 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.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND;
import static com.google.common.truth.Truth.assertThat;
@@ -44,6 +45,7 @@ import android.view.Choreographer;
import android.view.Display;
import android.view.SurfaceControl;
import android.view.SurfaceControlViewHost;
+import android.view.WindowManager;
import android.window.WindowContainerTransaction;
import androidx.test.filters.SmallTest;
@@ -173,10 +175,10 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
}
@Test
- public void updateRelayoutParams_freeformAndTransparent_allowsInputFallthrough() {
+ public void updateRelayoutParams_freeformAndTransparentAppearance_allowsInputFallthrough() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
- taskInfo.taskDescription.setStatusBarAppearance(
+ taskInfo.taskDescription.setSystemBarsAppearance(
APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND);
final RelayoutParams relayoutParams = new RelayoutParams();
@@ -187,14 +189,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.setSystemBarsAppearance(0);
final RelayoutParams relayoutParams = new RelayoutParams();
DesktopModeWindowDecoration.updateRelayoutParams(
@@ -204,7 +206,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* applyStartTransactionOnDraw= */ true,
/* shouldSetTaskPositionAndCrop */ false);
- assertThat(relayoutParams.mAllowCaptionInputFallthrough).isFalse();
+ assertThat(relayoutParams.hasInputFeatureSpy()).isFalse();
}
@Test
@@ -220,7 +222,55 @@ 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();
}
private void fillRoundedCornersResources(int fillValue) {
@@ -268,4 +318,9 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
return taskInfo;
}
+
+ private static boolean hasNoInputChannelFeature(RelayoutParams params) {
+ return (params.mInputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL)
+ != 0;
+ }
}
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..e6fabcfec58a 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
@@ -189,8 +189,9 @@ 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)
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..54645083eca8
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java
@@ -0,0 +1,348 @@
+/*
+ * 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.RequiresFlagsDisabled;
+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.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 CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ /**
+ * 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 (touch or cursor).
+ */
+ @Test
+ @RequiresFlagsEnabled(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
+ @RequiresFlagsDisabled(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
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ public void testCalculateControlType_edgeDragResizeEnabled_edges() {
+ // The input source (touch or cursor) shouldn't impact the edge resize size.
+ validateCtrlTypeForEdges(/* isTouch= */ false);
+ validateCtrlTypeForEdges(/* isTouch= */ true);
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ public void testCalculateControlType_edgeDragResizeDisabled_edges() {
+ // Edge resizing is not supported when the flag is disabled.
+ validateCtrlTypeForEdges(/* isTouch= */ false);
+ validateCtrlTypeForEdges(/* isTouch= */ false);
+ }
+
+ private void validateCtrlTypeForEdges(boolean isTouch) {
+ assertThat(GEOMETRY.calculateCtrlType(isTouch, LEFT_EDGE_POINT.x,
+ LEFT_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_LEFT);
+ assertThat(GEOMETRY.calculateCtrlType(isTouch, TOP_EDGE_POINT.x,
+ TOP_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_TOP);
+ assertThat(GEOMETRY.calculateCtrlType(isTouch, RIGHT_EDGE_POINT.x,
+ RIGHT_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_RIGHT);
+ assertThat(GEOMETRY.calculateCtrlType(isTouch, BOTTOM_EDGE_POINT.x,
+ BOTTOM_EDGE_POINT.y)).isEqualTo(CTRL_TYPE_BOTTOM);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ public void testCalculateControlType_edgeDragResizeEnabled_corners() {
+ final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2);
+ final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2);
+
+ // When the flag is enabled, points within fine corners should pass regardless of touch or
+ // not. Points outside fine corners should not pass when using a course input (non-touch).
+ fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true);
+ fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, true);
+ fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, true);
+ fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, false);
+
+ // When the flag is enabled, points near the large corners should only pass when the point
+ // is within the corner for large touch inputs.
+ largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true);
+ largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true,
+ false);
+ largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false);
+ largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false,
+ false);
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_ENABLE_WINDOWING_EDGE_DRAG_RESIZE)
+ public void testCalculateControlType_edgeDragResizeDisabled_corners() {
+ final TestPoints fineTestPoints = new TestPoints(TASK_SIZE, FINE_CORNER_SIZE / 2);
+ final TestPoints largeCornerTestPoints = new TestPoints(TASK_SIZE, LARGE_CORNER_SIZE / 2);
+
+ // When the flag is disabled, points within fine corners should pass only when touch.
+ fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, true);
+ fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true, false);
+ fineTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false);
+ fineTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false, false);
+
+ // When the flag is disabled, points near the large corners should never pass.
+ largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ true, false);
+ largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ true,
+ false);
+ largeCornerTestPoints.validateCtrlTypeForInnerPoints(GEOMETRY, /* isTouch= */ false, false);
+ largeCornerTestPoints.validateCtrlTypeForOutsidePoints(GEOMETRY, /* isTouch= */ false,
+ false);
+ }
+
+ /**
+ * 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 isTouch, boolean expectedWithinGeometry) {
+ assertThat(geometry.calculateCtrlType(isTouch, mTopLeftPoint.x,
+ mTopLeftPoint.y)).isEqualTo(
+ expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED);
+ assertThat(geometry.calculateCtrlType(isTouch, mTopRightPoint.x,
+ mTopRightPoint.y)).isEqualTo(
+ expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED);
+ assertThat(geometry.calculateCtrlType(isTouch, mBottomLeftPoint.x,
+ mBottomLeftPoint.y)).isEqualTo(
+ expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM
+ : CTRL_TYPE_UNDEFINED);
+ assertThat(geometry.calculateCtrlType(isTouch, 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 isTouch, boolean expectedWithinGeometry) {
+ assertThat(geometry.calculateCtrlType(isTouch, mTopLeftPointOutside.x,
+ mTopLeftPointOutside.y)).isEqualTo(
+ expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED);
+ assertThat(geometry.calculateCtrlType(isTouch, mTopRightPointOutside.x,
+ mTopRightPointOutside.y)).isEqualTo(
+ expectedWithinGeometry ? CTRL_TYPE_RIGHT | CTRL_TYPE_TOP : CTRL_TYPE_UNDEFINED);
+ assertThat(geometry.calculateCtrlType(isTouch, mBottomLeftPointOutside.x,
+ mBottomLeftPointOutside.y)).isEqualTo(
+ expectedWithinGeometry ? CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM
+ : CTRL_TYPE_UNDEFINED);
+ assertThat(geometry.calculateCtrlType(isTouch, 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..9174556d091b 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,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
@@ -11,6 +12,7 @@ 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
@@ -41,6 +43,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
/**
@@ -125,8 +129,7 @@ class FluidResizeTaskPositionerTest : ShellTestCase() {
mockWindowDecoration,
mockDisplayController,
mockDragStartListener,
- mockTransactionFactory,
- DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT
+ mockTransactionFactory
)
}
@@ -577,28 +580,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 +660,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(),
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..87425915fbf7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/ResizeVeilTest.kt
@@ -0,0 +1,216 @@
+/*
+ * 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.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()
+ }
+
+ @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()
+ val tx = mock<SurfaceControl.Transaction>()
+
+ veil.showVeil(tx, mock(), Rect(0, 0, 100, 100), false /* fadeIn */)
+
+ verify(tx).show(mockResizeVeilSurface)
+ verify(tx).show(mockBackgroundSurface)
+ verify(tx).show(mockIconSurface)
+ verify(tx).apply()
+ }
+
+ @Test
+ fun showVeil_displayUnavailable_doesNotShow() {
+ val veil = createResizeVeil(withDisplayAvailable = false)
+ val tx = mock<SurfaceControl.Transaction>()
+
+ veil.showVeil(tx, mock(), Rect(0, 0, 100, 100), false /* fadeIn */)
+
+ verify(tx, never()).show(mockResizeVeilSurface)
+ verify(tx, never()).show(mockBackgroundSurface)
+ verify(tx, never()).show(mockIconSurface)
+ verify(tx).apply()
+ }
+
+ @Test
+ fun showVeil_alreadyVisible_doesNotShowAgain() {
+ val veil = createResizeVeil()
+ val tx = mock<SurfaceControl.Transaction>()
+
+ veil.showVeil(tx, mock(), Rect(0, 0, 100, 100), false /* fadeIn */)
+ veil.showVeil(tx, mock(), Rect(0, 0, 100, 100), false /* fadeIn */)
+
+ verify(tx, times(1)).show(mockResizeVeilSurface)
+ verify(tx, times(1)).show(mockBackgroundSurface)
+ verify(tx, times(1)).show(mockIconSurface)
+ verify(tx, times(2)).apply()
+ }
+
+ @Test
+ fun showVeil_reparentsVeilToNewParent() {
+ val veil = createResizeVeil(parent = mock())
+ val tx = mock<SurfaceControl.Transaction>()
+
+ val newParent = mock<SurfaceControl>()
+ veil.showVeil(tx, newParent, Rect(0, 0, 100, 100), false /* fadeIn */)
+
+ verify(tx).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,
+ taskInfo,
+ parent,
+ { mockTransaction },
+ mockSurfaceControlBuilderFactory,
+ mockSurfaceControlViewHostFactory
+ )
+ }
+}
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..8b8cd119effd 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
@@ -42,6 +42,7 @@ 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;
@@ -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,7 +75,7 @@ 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 org.junit.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);
@@ -613,26 +612,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 +699,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)
@@ -766,6 +901,7 @@ 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);
diff --git a/libs/androidfw/ZipFileRO.cpp b/libs/androidfw/ZipFileRO.cpp
index d7b5914130ee..839c7b6fef37 100644
--- a/libs/androidfw/ZipFileRO.cpp
+++ b/libs/androidfw/ZipFileRO.cpp
@@ -119,30 +119,41 @@ ZipEntryRO ZipFileRO::findEntryByName(const char* entryName) const
* appear to be bogus.
*/
bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod,
+ uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset,
+ uint32_t* pModWhen, uint32_t* pCrc32) const
+{
+ return getEntryInfo(entry, pMethod, pUncompLen, pCompLen, pOffset, pModWhen,
+ pCrc32, nullptr);
+}
+
+bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod,
uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset,
- uint32_t* pModWhen, uint32_t* pCrc32) const
+ uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const
{
const _ZipEntryRO* zipEntry = reinterpret_cast<_ZipEntryRO*>(entry);
const ZipEntry& ze = zipEntry->entry;
- if (pMethod != NULL) {
+ if (pMethod != nullptr) {
*pMethod = ze.method;
}
- if (pUncompLen != NULL) {
+ if (pUncompLen != nullptr) {
*pUncompLen = ze.uncompressed_length;
}
- if (pCompLen != NULL) {
+ if (pCompLen != nullptr) {
*pCompLen = ze.compressed_length;
}
- if (pOffset != NULL) {
+ if (pOffset != nullptr) {
*pOffset = ze.offset;
}
- if (pModWhen != NULL) {
+ if (pModWhen != nullptr) {
*pModWhen = ze.mod_time;
}
- if (pCrc32 != NULL) {
+ if (pCrc32 != nullptr) {
*pCrc32 = ze.crc32;
}
+ if (pExtraFieldSize != nullptr) {
+ *pExtraFieldSize = ze.extra_field_size;
+ }
return true;
}
@@ -304,9 +315,13 @@ bool ZipFileRO::uncompressEntry(ZipEntryRO entry, int fd) const
_ZipEntryRO *zipEntry = reinterpret_cast<_ZipEntryRO*>(entry);
const int32_t error = ExtractEntryToFile(mHandle, &(zipEntry->entry), fd);
if (error) {
- ALOGW("ExtractToMemory failed with %s", ErrorCodeString(error));
+ ALOGW("ExtractToFile failed with %s", ErrorCodeString(error));
return false;
}
return true;
}
+
+const char* ZipFileRO::getZipFileName() {
+ return mFileName;
+}
diff --git a/libs/androidfw/fuzz/resourcefile_fuzzer/Android.bp b/libs/androidfw/fuzz/resourcefile_fuzzer/Android.bp
index b511244c4a30..619658923865 100644
--- a/libs/androidfw/fuzz/resourcefile_fuzzer/Android.bp
+++ b/libs/androidfw/fuzz/resourcefile_fuzzer/Android.bp
@@ -19,6 +19,7 @@ package {
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_libs_androidfw_license"],
+ default_team: "trendy_team_android_resources",
}
cc_fuzz {
@@ -31,7 +32,7 @@ cc_fuzz {
static_libs: ["libgmock"],
target: {
android: {
- shared_libs:[
+ shared_libs: [
"libandroidfw",
"libbase",
"libcutils",
@@ -52,4 +53,15 @@ cc_fuzz {
],
},
},
+ fuzz_config: {
+ cc: [
+ "android-resources@google.com",
+ ],
+ componentid: 568761,
+ description: "The fuzzer targets the APIs of libandroidfw",
+ vector: "local_no_privileges_required",
+ service_privilege: "privileged",
+ users: "multi_user",
+ fuzzed_code_usage: "shipped",
+ },
}
diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/Android.bp b/libs/androidfw/fuzz/resxmlparser_fuzzer/Android.bp
new file mode 100644
index 000000000000..4b008a7b4815
--- /dev/null
+++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/Android.bp
@@ -0,0 +1,51 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_libs_androidfw_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_libs_androidfw_license"],
+}
+
+cc_fuzz {
+ name: "resxmlparser_fuzzer",
+ srcs: [
+ "resxmlparser_fuzzer.cpp",
+ ],
+ host_supported: true,
+
+ static_libs: ["libgmock"],
+ target: {
+ android: {
+ shared_libs: [
+ "libandroidfw",
+ "libbase",
+ "libbinder",
+ "libcutils",
+ "liblog",
+ "libutils",
+ ],
+ },
+ host: {
+ static_libs: [
+ "libandroidfw",
+ "libbase",
+ "libbinder",
+ "libcutils",
+ "liblog",
+ "libutils",
+ ],
+ },
+ darwin: {
+ // libbinder is not supported on mac
+ enabled: false,
+ },
+ },
+
+ include_dirs: [
+ "system/incremental_delivery/incfs/util/include/",
+ ],
+
+ corpus: ["testdata/*"],
+ dictionary: "xmlparser_fuzzer.dict",
+}
diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp b/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp
new file mode 100644
index 000000000000..829a39617012
--- /dev/null
+++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/resxmlparser_fuzzer.cpp
@@ -0,0 +1,80 @@
+/*
+ * 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 <memory>
+#include <cstdint>
+#include <cstddef>
+#include <fuzzer/FuzzedDataProvider.h>
+#include "androidfw/ResourceTypes.h"
+
+static void populateDynamicRefTableWithFuzzedData(
+ android::DynamicRefTable& table,
+ FuzzedDataProvider& fuzzedDataProvider) {
+
+ const size_t numMappings = fuzzedDataProvider.ConsumeIntegralInRange<size_t>(1, 5);
+ for (size_t i = 0; i < numMappings; ++i) {
+ const uint8_t packageId = fuzzedDataProvider.ConsumeIntegralInRange<uint8_t>(0x02, 0x7F);
+
+ // Generate a package name
+ std::string packageName;
+ size_t packageNameLength = fuzzedDataProvider.ConsumeIntegralInRange<size_t>(1, 128);
+ for (size_t j = 0; j < packageNameLength; ++j) {
+ // Consume characters only in the ASCII range (0x20 to 0x7E) to ensure valid UTF-8
+ char ch = fuzzedDataProvider.ConsumeIntegralInRange<char>(0x20, 0x7E);
+ packageName.push_back(ch);
+ }
+
+ // Convert std::string to String16 for compatibility
+ android::String16 androidPackageName(packageName.c_str(), packageName.length());
+
+ // Add the mapping to the table
+ table.addMapping(androidPackageName, packageId);
+ }
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ FuzzedDataProvider fuzzedDataProvider(data, size);
+
+ auto dynamic_ref_table = std::make_shared<android::DynamicRefTable>();
+
+ // Populate the DynamicRefTable with fuzzed data
+ populateDynamicRefTableWithFuzzedData(*dynamic_ref_table, fuzzedDataProvider);
+
+ 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
+ }
+
+ tree.restart();
+
+ size_t len = 0;
+ auto code = tree.next();
+ if (code == android::ResXMLParser::START_TAG) {
+ // Access element name
+ auto name = tree.getElementName(&len);
+
+ // Access attributes of the current element
+ for (size_t i = 0; i < tree.getAttributeCount(); i++) {
+ // Access attribute name
+ auto attrName = tree.getAttributeName(i, &len);
+ }
+ } else if (code == android::ResXMLParser::TEXT) {
+ const auto text = tree.getText(&len);
+ }
+ return 0; // Non-zero return values are reserved for future use.
+}
diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/attributes.xml b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/attributes.xml
new file mode 100644
index 000000000000..417fec72be6a
--- /dev/null
+++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/attributes.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<root>
+ <child id="1">
+ <subchild type="A">Content A</subchild>
+ <subchild type="B">Content B</subchild>
+ </child>
+ <child id="2" extra="data">
+ <subchild type="C">Content C</subchild>
+ </child>
+</root>
diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/basic.xml b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/basic.xml
new file mode 100644
index 000000000000..7e13db536fc9
--- /dev/null
+++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/basic.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<root>
+ <child1>Value 1</child1>
+ <child2>Value 2</child2>
+</root>
diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/cdata.xml b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/cdata.xml
new file mode 100644
index 000000000000..90cdf3513be9
--- /dev/null
+++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/testdata/cdata.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<root>
+ <!-- Example with special characters and CDATA -->
+ <data><![CDATA[Some <encoded> data & other "special" characters]]></data>
+ <message>Hello &amp; Welcome!</message>
+</root>
diff --git a/libs/androidfw/fuzz/resxmlparser_fuzzer/xmlparser_fuzzer.dict b/libs/androidfw/fuzz/resxmlparser_fuzzer/xmlparser_fuzzer.dict
new file mode 100644
index 000000000000..745ded4810f3
--- /dev/null
+++ b/libs/androidfw/fuzz/resxmlparser_fuzzer/xmlparser_fuzzer.dict
@@ -0,0 +1,11 @@
+root_tag=<root>
+child_tag=<child>
+end_child_tag=</child>
+id_attr=id="
+type_attr=type="
+cdata_start=<![CDATA[
+cdata_end=]]>
+ampersand_entity=&amp;
+xml_header=<?xml version="1.0" encoding="UTF-8"?>
+comment_start=<!--
+comment_end= -->
diff --git a/libs/androidfw/include/androidfw/ZipFileRO.h b/libs/androidfw/include/androidfw/ZipFileRO.h
index be1f98f4843d..f7c5007c80d2 100644
--- a/libs/androidfw/include/androidfw/ZipFileRO.h
+++ b/libs/androidfw/include/androidfw/ZipFileRO.h
@@ -151,6 +151,10 @@ public:
uint32_t* pCompLen, off64_t* pOffset, uint32_t* pModWhen,
uint32_t* pCrc32) const;
+ bool getEntryInfo(ZipEntryRO entry, uint16_t* pMethod,
+ uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset,
+ uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const;
+
/*
* Create a new FileMap object that maps a subset of the archive. For
* an uncompressed entry this effectively provides a pointer to the
@@ -187,6 +191,8 @@ public:
*/
bool uncompressEntry(ZipEntryRO entry, int fd) const;
+ const char* getZipFileName();
+
~ZipFileRO();
private:
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 54f94f5c4b14..7c1c5b4e7e5f 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"],
}
@@ -31,6 +32,7 @@ license {
aconfig_declarations {
name: "hwui_flags",
package: "com.android.graphics.hwui.flags",
+ container: "system",
srcs: [
"aconfig/hwui_flags.aconfig",
],
@@ -78,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",
],
@@ -92,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)=",
],
},
},
@@ -140,7 +144,6 @@ cc_defaults {
"libsync",
"libui",
"aconfig_text_flags_c_lib",
- "server_configurable_flags",
],
static_libs: [
"libEGL_blobCache",
@@ -265,6 +268,7 @@ cc_defaults {
cppflags: ["-Wno-conversion-null"],
srcs: [
+ "apex/android_canvas.cpp",
"apex/android_matrix.cpp",
"apex/android_paint.cpp",
"apex/android_region.cpp",
@@ -277,7 +281,6 @@ cc_defaults {
android: {
srcs: [ // sources that depend on android only libraries
"apex/android_bitmap.cpp",
- "apex/android_canvas.cpp",
"apex/jni_runtime.cpp",
],
},
@@ -333,9 +336,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",
@@ -345,6 +351,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",
@@ -418,17 +425,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: [
@@ -447,6 +450,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",
@@ -530,16 +539,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",
@@ -548,6 +565,7 @@ cc_defaults {
"hwui/MinikinUtils.cpp",
"hwui/PaintImpl.cpp",
"hwui/Typeface.cpp",
+ "thread/CommonPool.cpp",
"utils/Blur.cpp",
"utils/Color.cpp",
"utils/LinearAllocator.cpp",
@@ -564,8 +582,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",
@@ -582,6 +603,7 @@ cc_defaults {
"SkiaCanvas.cpp",
"SkiaInterpolator.cpp",
"Tonemapper.cpp",
+ "TreeInfo.cpp",
"VectorDrawable.cpp",
],
@@ -598,43 +620,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",
],
@@ -652,6 +663,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..325bdd63ab22 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__
@@ -47,16 +45,6 @@ constexpr bool hdr_10bit_plus() {
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;
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index ec53070f6cb8..c1510d96461f 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -242,7 +242,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
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 3d7e559bebe0..50f8b3929e1e 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -1,7 +1,9 @@
package: "com.android.graphics.hwui.flags"
+container: "system"
flag {
name: "clip_shader"
+ is_exported: true
namespace: "core_graphics"
description: "API for canvas shader clipping operations"
bug: "280116960"
@@ -9,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"
@@ -16,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"
@@ -44,6 +48,7 @@ flag {
flag {
name: "gainmap_animations"
+ is_exported: true
namespace: "core_graphics"
description: "APIs to help enable animations involving gainmaps"
bug: "296482289"
@@ -51,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"
@@ -65,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"
@@ -76,3 +83,10 @@ 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"
+}
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..6ace3967ecf3 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),
diff --git a/libs/hwui/effects/GainmapRenderer.cpp b/libs/hwui/effects/GainmapRenderer.cpp
index 3ebf7d19202d..0a30c6c14c4c 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 {
@@ -206,12 +208,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 +250,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/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index 8315c4c0dd4d..07e97f85d588 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;
@@ -789,13 +785,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/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp
index b87002371775..8e07a2f31de1 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.cpp
+++ b/libs/hwui/pipeline/skia/ShaderCache.cpp
@@ -15,14 +15,18 @@
*/
#include "ShaderCache.h"
+
#include <GrDirectContext.h>
#include <SkData.h>
#include <gui/TraceUtils.h>
#include <log/log.h>
#include <openssl/sha.h>
+
#include <algorithm>
#include <array>
+#include <mutex>
#include <thread>
+
#include "FileBlobCache.h"
#include "Properties.h"
diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h
index 6ccb212fe6ca..40dfc9d4309b 100644
--- a/libs/hwui/pipeline/skia/ShaderCache.h
+++ b/libs/hwui/pipeline/skia/ShaderCache.h
@@ -16,6 +16,7 @@
#pragma once
+#include <FileBlobCache.h>
#include <GrContextOptions.h>
#include <SkRefCnt.h>
#include <cutils/compiler.h>
@@ -32,7 +33,6 @@ class SkData;
namespace android {
class BlobCache;
-class FileBlobCache;
namespace uirenderer {
namespace skiapipeline {
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/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp
index 6a465442c2b4..f1ee3256dbee 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;
}
@@ -467,10 +467,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..dc7e8ca16c8a 100644
--- a/libs/input/MouseCursorController.h
+++ b/libs/input/MouseCursorController.h
@@ -47,7 +47,7 @@ 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);
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index f9dc5fac7e21..cca1b07c3118 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,42 @@ 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::setSkipScreenshot(ui::LogicalDisplayId displayId, bool skip) {
+ std::scoped_lock lock(getLock());
+ if (skip) {
+ mLocked.displaysToSkipScreenshot.insert(displayId);
+ } else {
+ mLocked.displaysToSkipScreenshot.erase(displayId);
+ }
+}
+
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 +326,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 +336,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 +357,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 +370,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 +383,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..c6430f7f36ff 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,18 @@ 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 setSkipScreenshot(ui::LogicalDisplayId displayId, bool skip) override;
virtual void setInactivityTimeout(InactivityTimeout inactivityTimeout);
void doInactivityTimeout();
@@ -86,12 +86,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 +103,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 +132,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 +142,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 +161,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 +177,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 +206,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..2dcb1f1d1650 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
@@ -178,7 +166,7 @@ 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;
@@ -217,7 +205,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 +255,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 +265,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,28 +316,43 @@ TEST_F(PointerControllerTest, doesNotGetResourcesBeforeSettingViewport) {
ensureDisplayViewportIsSet();
}
-TEST_F(PointerControllerTest, notifiesPolicyWhenPointerDisplayChanges) {
- EXPECT_FALSE(mPolicy->getLastReportedPointerDisplayId())
- << "A pointer display change does not occur when PointerController is created.";
-
- ensureDisplayViewportIsSet(ADISPLAY_ID_DEFAULT);
-
- 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.";
-
- ensureDisplayViewportIsSet(42);
-
- EXPECT_EQ(42, *mPolicy->getLastReportedPointerDisplayId())
- << "The policy is notified when the pointer display changes.";
-
- // Release the PointerController.
- mPointerController = nullptr;
+TEST_F(PointerControllerTest, updatesSkipScreenshotFlagForTouchSpots) {
+ ensureDisplayViewportIsSet();
- EXPECT_EQ(ADISPLAY_ID_NONE, *mPolicy->getLastReportedPointerDisplayId())
- << "The pointer display changes to invalid when PointerController is destroyed.";
+ 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->setSkipScreenshot(ui::LogicalDisplayId::DEFAULT, true);
+ 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->setSkipScreenshot(ui::LogicalDisplayId::DEFAULT, false);
+ EXPECT_CALL(*testSpotSprite, setSkipScreenshot).With(testing::Args<0>(false));
+ mPointerController->setSpots(&testSpotCoords, testIdToIndex.cbegin(), testIdBits,
+ ui::LogicalDisplayId::DEFAULT);
+ testing::Mock::VerifyAndClearExpectations(testSpotSprite.get());
}
class PointerControllerWindowInfoListenerTest : public Test {};
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));